This commit is contained in:
2026-04-19 23:41:35 +08:00
parent 7660cda647
commit ef257e7120
31 changed files with 6317 additions and 1332 deletions
+6
View File
@@ -41,6 +41,12 @@ SECRET_KEY=your-secret-key-change-in-production-min-32-chars
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=1440
# 管理员账户配置
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-this-to-a-secure-password
ADMIN_EMAIL=admin@gemold.com
ADMIN_FULL_NAME=系统管理员
# FreeCAD 验证配置(可选)
# 启用后会增加处理时间,默认禁用
ENABLE_FREECAD_VERIFICATION=false
+2
View File
@@ -16,6 +16,8 @@ __pycache__/
.DS_Store
*.log
.env
1panel.env
*.env.local
logs/
uploads/
html_output/
+6 -2
View File
@@ -67,13 +67,17 @@ class Settings:
self.DB_PASSWORD = db_password
# JWT配置
self.SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production')
self.SECRET_KEY = os.getenv('SECRET_KEY')
if not self.SECRET_KEY:
raise ValueError("SECRET_KEY 未设置,请在.env文件中配置安全的密钥")
self.ALGORITHM = os.getenv('ALGORITHM', 'HS256')
self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', '1440'))
# 管理员账户配置
self.ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin')
self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123')
self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
if not self.ADMIN_PASSWORD:
raise ValueError("ADMIN_PASSWORD 未设置,请在.env文件中配置管理员密码")
self.ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@gemold.com')
self.ADMIN_FULL_NAME = os.getenv('ADMIN_FULL_NAME', '系统管理员')
+1 -1
View File
@@ -10,7 +10,7 @@
# 几何处理核心
# ============================================
# PythonOCC - CAD几何处理
# pythonocc-core>=7.7.0
pythonocc-core>=7.7.0
# 网格处理
trimesh>=3.21.0
# 科学计算
+42 -41
View File
@@ -4,6 +4,7 @@ from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from typing import Optional, List, Dict, Tuple
from datetime import datetime
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user
@@ -414,15 +415,15 @@ async def get_finance_summary(
) or 0
return FinanceSummaryResponse(
receivable_total=round(float(receivable_total), 2),
payable_total=round(float(payable_total), 2),
monthly_receipt_total=round(float(monthly_receipt_total), 2),
monthly_payment_total=round(float(monthly_payment_total), 2),
receivable_total=Decimal(str(receivable_total)),
payable_total=Decimal(str(payable_total)),
monthly_receipt_total=Decimal(str(monthly_receipt_total)),
monthly_payment_total=Decimal(str(monthly_payment_total)),
selected_year=selected_year,
selected_quarter=selected_quarter,
period_label=period_label,
period_receipt_total=round(float(period_receipt_total), 2),
period_payment_total=round(float(period_payment_total), 2),
period_receipt_total=Decimal(str(period_receipt_total)),
period_payment_total=Decimal(str(period_payment_total)),
overdue_receivable_count=0,
overdue_payable_count=0,
)
@@ -464,9 +465,9 @@ async def get_partner_statement(
"outstanding_total": 0.0,
},
)
total_amount = float(order.total_amount or 0)
settled_amount = float(order.received_amount or 0)
outstanding = max(total_amount - settled_amount, 0.0)
total_amount = Decimal(str(order.total_amount or 0))
settled_amount = Decimal(str(order.received_amount or 0))
outstanding = max(total_amount - settled_amount, Decimal("0"))
partner_stat["order_count"] += 1
partner_stat["order_total"] += total_amount
partner_stat["settled_total"] += settled_amount
@@ -495,7 +496,7 @@ async def get_partner_statement(
},
)
partner_stat["transaction_count"] += 1
partner_stat["transaction_total"] += float(txn.amount or 0)
partner_stat["transaction_total"] += Decimal(str(txn.amount or 0))
else:
order_rows = await db_session.execute(
select(PurchaseOrder, Supplier)
@@ -518,9 +519,9 @@ async def get_partner_statement(
"outstanding_total": 0.0,
},
)
total_amount = float(order.total_amount or 0)
settled_amount = float(order.paid_amount or 0)
outstanding = max(total_amount - settled_amount, 0.0)
total_amount = Decimal(str(order.total_amount or 0))
settled_amount = Decimal(str(order.paid_amount or 0))
outstanding = max(total_amount - settled_amount, Decimal("0"))
partner_stat["order_count"] += 1
partner_stat["order_total"] += total_amount
partner_stat["settled_total"] += settled_amount
@@ -549,7 +550,7 @@ async def get_partner_statement(
},
)
partner_stat["transaction_count"] += 1
partner_stat["transaction_total"] += float(txn.amount or 0)
partner_stat["transaction_total"] += Decimal(str(txn.amount or 0))
missing_partner_ids = [pid for pid, item in stats_map.items() if "#" in item["partner_name"]]
if missing_partner_ids:
@@ -572,10 +573,10 @@ async def get_partner_statement(
partner_name=item["partner_name"],
order_count=item["order_count"],
transaction_count=item["transaction_count"],
order_total=round(float(item["order_total"]), 2),
settled_total=round(float(item["settled_total"]), 2),
transaction_total=round(float(item["transaction_total"]), 2),
outstanding_total=round(float(item["outstanding_total"]), 2),
order_total=Decimal(str(item["order_total"])),
settled_total=Decimal(str(item["settled_total"])),
transaction_total=Decimal(str(item["transaction_total"])),
outstanding_total=Decimal(str(item["outstanding_total"])),
period_year=selected_year,
period_quarter=selected_quarter,
)
@@ -587,10 +588,10 @@ async def get_partner_statement(
year=selected_year,
quarter=selected_quarter,
period_label=period_label,
order_total=round(float(sum(item.order_total for item in items)), 2),
settled_total=round(float(sum(item.settled_total for item in items)), 2),
transaction_total=round(float(sum(item.transaction_total for item in items)), 2),
outstanding_total=round(float(sum(item.outstanding_total for item in items)), 2),
order_total=Decimal(str(sum(item.order_total for item in items))),
settled_total=Decimal(str(sum(item.settled_total for item in items))),
transaction_total=Decimal(str(sum(item.transaction_total for item in items))),
outstanding_total=Decimal(str(sum(item.outstanding_total for item in items))),
items=items,
)
@@ -642,15 +643,15 @@ async def get_partner_product_statement(
},
)
item_amount = float(item.amount or 0)
order_total = float(order.total_amount or 0)
order_settled = max(float(order.received_amount or 0), 0.0)
ratio = (item_amount / order_total) if order_total > 1e-9 else 0.0
item_amount = Decimal(str(item.amount or 0))
order_total = Decimal(str(order.total_amount or 0))
order_settled = max(Decimal(str(order.received_amount or 0)), Decimal("0"))
ratio = (item_amount / order_total) if order_total > Decimal("1e-9") else Decimal("0")
item_settled = min(item_amount, order_settled * ratio)
item_outstanding = max(item_amount - item_settled, 0.0)
item_outstanding = max(item_amount - item_settled, Decimal("0"))
stat["order_ids"].add(order.id)
stat["order_quantity"] += float(item.quantity or 0)
stat["order_quantity"] += Decimal(str(item.quantity or 0))
stat["order_amount"] += item_amount
stat["settled_amount"] += item_settled
stat["outstanding_amount"] += item_outstanding
@@ -686,15 +687,15 @@ async def get_partner_product_statement(
},
)
item_amount = float(item.amount or 0)
order_total = float(order.total_amount or 0)
order_settled = max(float(order.paid_amount or 0), 0.0)
ratio = (item_amount / order_total) if order_total > 1e-9 else 0.0
item_amount = Decimal(str(item.amount or 0))
order_total = Decimal(str(order.total_amount or 0))
order_settled = max(Decimal(str(order.paid_amount or 0)), Decimal("0"))
ratio = (item_amount / order_total) if order_total > Decimal("1e-9") else Decimal("0")
item_settled = min(item_amount, order_settled * ratio)
item_outstanding = max(item_amount - item_settled, 0.0)
item_outstanding = max(item_amount - item_settled, Decimal("0"))
stat["order_ids"].add(order.id)
stat["order_quantity"] += float(item.quantity or 0)
stat["order_quantity"] += Decimal(str(item.quantity or 0))
stat["order_amount"] += item_amount
stat["settled_amount"] += item_settled
stat["outstanding_amount"] += item_outstanding
@@ -707,10 +708,10 @@ async def get_partner_product_statement(
product_sku=item["product_sku"],
product_name=item["product_name"],
order_count=len(item["order_ids"]),
order_quantity=round(float(item["order_quantity"]), 2),
order_amount=round(float(item["order_amount"]), 2),
settled_amount=round(float(item["settled_amount"]), 2),
outstanding_amount=round(float(item["outstanding_amount"]), 2),
order_quantity=Decimal(str(item["order_quantity"])),
order_amount=Decimal(str(item["order_amount"])),
settled_amount=Decimal(str(item["settled_amount"])),
outstanding_amount=Decimal(str(item["outstanding_amount"])),
period_year=selected_year,
period_quarter=selected_quarter,
)
@@ -727,9 +728,9 @@ async def get_partner_product_statement(
quarter=selected_quarter,
period_label=period_label,
partner_id=partner_id,
order_amount_total=round(float(sum(item.order_amount for item in items)), 2),
settled_amount_total=round(float(sum(item.settled_amount for item in items)), 2),
outstanding_amount_total=round(float(sum(item.outstanding_amount for item in items)), 2),
order_amount_total=Decimal(str(sum(item.order_amount for item in items))),
settled_amount_total=Decimal(str(sum(item.settled_amount for item in items))),
outstanding_amount_total=Decimal(str(sum(item.outstanding_amount for item in items))),
items=items,
)
+12 -11
View File
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, or_, func, delete
from typing import Optional, List, Dict
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user, get_current_admin_user
@@ -45,10 +46,10 @@ async def _calculate_material_cost_map(db_session: AsyncSession, product_ids: Li
.where(ProductMaterial.finished_product_id.in_(product_ids))
.group_by(ProductMaterial.finished_product_id)
)
return {row[0]: float(row[1] or 0) for row in result.all()}
return {row[0]: Decimal(str(row[1] or 0)) for row in result.all()}
def _build_product_response(product: Product, material_cost: float = 0) -> ProductResponse:
def _build_product_response(product: Product, material_cost: Decimal = Decimal("0")) -> ProductResponse:
return ProductResponse(
id=product.id,
sku=product.sku,
@@ -57,11 +58,11 @@ def _build_product_response(product: Product, material_cost: float = 0) -> Produ
category=product.category,
unit=product.unit,
item_type=product.item_type,
cost_price=float(product.cost_price or 0),
sale_price=float(product.sale_price or 0),
cost_price=Decimal(str(product.cost_price or 0)),
sale_price=Decimal(str(product.sale_price or 0)),
min_stock=product.min_stock,
max_stock=product.max_stock,
material_cost=round(float(material_cost), 4),
material_cost=Decimal(str(material_cost)).quantize(Decimal("0.0001")),
is_active=product.is_active,
created_at=product.created_at,
)
@@ -184,25 +185,25 @@ async def get_product_bom(
)
items: List[ProductMaterialItemResponse] = []
total_material_cost = 0.0
total_material_cost = Decimal("0")
for bom, material in bom_result.all():
line_cost = float(material.cost_price or 0) * float(bom.quantity)
line_cost = Decimal(str(material.cost_price or 0)) * Decimal(str(bom.quantity))
total_material_cost += line_cost
items.append(
ProductMaterialItemResponse(
material_id=material.id,
material_sku=material.sku,
material_name=material.name,
quantity=round(float(bom.quantity), 4),
unit_cost=round(float(material.cost_price or 0), 4),
line_cost=round(float(line_cost), 4),
quantity=Decimal(str(bom.quantity)),
unit_cost=Decimal(str(material.cost_price or 0)),
line_cost=line_cost,
)
)
return ProductBOMResponse(
product_id=product.id,
product_name=product.name,
total_material_cost=round(float(total_material_cost), 4),
total_material_cost=total_material_cost,
items=items,
)
+4 -3
View File
@@ -12,6 +12,7 @@ from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from typing import Optional, List
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user
@@ -167,8 +168,8 @@ async def _apply_order_items(
order_id=order.id,
product_id=item_data.product_id,
quantity=int(item_data.quantity),
unit_price=float(unit_price),
amount=float(item_data.quantity) * float(unit_price),
unit_price=Decimal(str(unit_price)),
amount=Decimal(str(item_data.quantity)) * Decimal(str(unit_price)),
remark=item_data.remark or None
)
db_session.add(item)
@@ -408,7 +409,7 @@ async def receive_purchase_order(
reference_id=order.id,
reference_no=order.order_no,
unit_price=item.unit_price,
total_amount=round(float(item.unit_price * receive_item.receive_quantity), 4),
total_amount=Decimal(str(item.unit_price * receive_item.receive_quantity)),
remark=payload.remark or f"采购单{order.order_no}到货入库",
operator_id=current_user.id
)
+41 -38
View File
@@ -14,6 +14,7 @@ from sqlalchemy import select, func, delete, update
from typing import Optional, List
from math import ceil
from pydantic import BaseModel, Field
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user
@@ -58,10 +59,10 @@ def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesO
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
planned_material_cost=round(float(order.planned_material_cost or 0), 4),
actual_material_cost=round(float(order.actual_material_cost or 0), 4),
total_amount=order.total_amount,
received_amount=order.received_amount,
planned_material_cost=Decimal(str(order.planned_material_cost or 0)),
actual_material_cost=Decimal(str(order.actual_material_cost or 0)),
total_amount=Decimal(str(order.total_amount or 0)),
received_amount=Decimal(str(order.received_amount or 0)),
remark=order.remark,
created_at=order.created_at
)
@@ -89,10 +90,10 @@ async def _build_sales_order_detail_response(
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
planned_material_cost=round(float(order.planned_material_cost or 0), 4),
actual_material_cost=round(float(order.actual_material_cost or 0), 4),
total_amount=order.total_amount,
received_amount=order.received_amount,
planned_material_cost=Decimal(str(order.planned_material_cost or 0)),
actual_material_cost=Decimal(str(order.actual_material_cost or 0)),
total_amount=Decimal(str(order.total_amount or 0)),
received_amount=Decimal(str(order.received_amount or 0)),
remark=order.remark,
created_at=order.created_at,
items=[
@@ -101,8 +102,8 @@ async def _build_sales_order_detail_response(
product_id=item.product_id,
quantity=item.quantity,
delivered_quantity=item.delivered_quantity,
unit_price=item.unit_price,
amount=item.amount,
unit_price=Decimal(str(item.unit_price or 0)),
amount=Decimal(str(item.amount or 0)),
remark=item.remark
) for item in items
]
@@ -152,12 +153,12 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
for order_item in order_items:
bom_items = bom_by_finished_id.get(int(order_item.product_id)) or []
for bom, material in bom_items:
qty = float(order_item.quantity) * float(bom.quantity or 0) * (1 + float(bom.loss_rate or 0))
entry = required_qty_map.setdefault(material.id, {"material": material, "required_qty": 0.0})
qty = Decimal(str(order_item.quantity)) * Decimal(str(bom.quantity or 0)) * (1 + Decimal(str(bom.loss_rate or 0)))
entry = required_qty_map.setdefault(material.id, {"material": material, "required_qty": Decimal("0")})
entry["required_qty"] += qty
if not required_qty_map:
return [], 0
return [], Decimal("0")
material_ids = list(required_qty_map.keys())
stock_result = await db_session.execute(
@@ -165,28 +166,28 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
.where(Inventory.product_id.in_(material_ids))
.group_by(Inventory.product_id)
)
stock_map = {row[0]: int(row[1] or 0) for row in stock_result.all()}
stock_map = {row[0]: Decimal(str(row[1] or 0)) for row in stock_result.all()}
plan_items = []
planned_material_cost = 0.0
planned_material_cost = Decimal("0")
for material_id, entry in required_qty_map.items():
material = entry["material"]
required_qty = int(ceil(entry["required_qty"]))
available_qty = stock_map.get(material_id, 0)
shortage_qty = max(required_qty - available_qty, 0)
unit_cost = float(material.cost_price or 0)
required_cost = required_qty * unit_cost
available_qty = stock_map.get(material_id, Decimal("0"))
shortage_qty = max(required_qty - int(available_qty), 0)
unit_cost = Decimal(str(material.cost_price or 0))
required_cost = Decimal(str(required_qty)) * unit_cost
planned_material_cost += required_cost
plan_items.append(
ProductionMaterialPlanItemResponse(
material_id=material.id,
material_sku=material.sku,
material_name=material.name,
required_quantity=required_qty,
required_quantity=Decimal(str(required_qty)),
available_quantity=available_qty,
shortage_quantity=shortage_qty,
unit_cost=round(unit_cost, 4),
required_cost=round(required_cost, 4),
shortage_quantity=Decimal(str(shortage_qty)),
unit_cost=unit_cost,
required_cost=required_cost,
)
)
@@ -255,7 +256,7 @@ async def _issue_materials_for_order_creation(
reference_id=order.id,
reference_no=production_no,
unit_price=item.unit_cost,
total_amount=round(float(total_amount), 4),
total_amount=Decimal(str(total_amount)),
remark=f"销售单{order.order_no}创建时自动扣减物料",
operator_id=current_user.id
)
@@ -264,8 +265,8 @@ async def _issue_materials_for_order_creation(
order.production_no = production_no
order.production_status = "material_issued"
order.planned_material_cost = round(float(planned_material_cost), 4)
order.actual_material_cost = round(float(actual_material_cost), 4)
order.planned_material_cost = planned_material_cost
order.actual_material_cost = actual_material_cost
order.status = "manufacturing"
return movement_count, planned_material_cost, actual_material_cost
@@ -543,8 +544,10 @@ async def consume_materials(
"""记录销售订单的物料消耗"""
order, customer = await _get_sales_order_with_customer(db_session, order_id)
default_warehouse = await _get_default_warehouse(db_session)
# 计算总物料成本
total_cost = 0
total_cost = Decimal("0")
# 处理每个物料消耗项
for item in request.items:
@@ -556,14 +559,14 @@ async def consume_materials(
raise HTTPException(status_code=400, detail=f"只能消耗物料类型的产品: {material.name}")
# 计算成本
cost = material.cost_price * item.quantity
cost = Decimal(str(material.cost_price or 0)) * item.quantity
total_cost += cost
# 更新物料库存
inventory_result = await db_session.execute(
select(Inventory)
.where(Inventory.product_id == material.id)
.where(Inventory.warehouse_id == 1)
.where(Inventory.warehouse_id == default_warehouse.id)
)
inventory = inventory_result.scalar()
if inventory:
@@ -578,7 +581,7 @@ async def consume_materials(
# 记录物料消耗
movement = StockMovement(
product_id=material.id,
warehouse_id=1, # 默认仓库
warehouse_id=default_warehouse.id,
quantity=-item.quantity,
before_quantity=before_qty,
after_quantity=after_qty,
@@ -620,7 +623,7 @@ async def get_sales_order_production_plan(
order_no=order.order_no,
customer_name=customer.name,
production_no=production_no,
planned_material_cost=round(float(planned_material_cost), 4),
planned_material_cost=planned_material_cost,
items=plan_items,
)
@@ -688,7 +691,7 @@ async def issue_sales_order_materials(
reference_id=order.id,
reference_no=production_no,
unit_price=item.unit_cost,
total_amount=round(float(total_amount), 4),
total_amount=Decimal(str(total_amount)),
remark=payload.remark or f"销售单{order.order_no}按单生产领料",
operator_id=current_user.id
)
@@ -697,22 +700,22 @@ async def issue_sales_order_materials(
order.production_no = production_no
order.production_status = "material_issued"
order.planned_material_cost = round(float(planned_material_cost), 4)
order.actual_material_cost = round(float(actual_material_cost), 4)
order.planned_material_cost = planned_material_cost
order.actual_material_cost = actual_material_cost
if order.status == "draft":
order.status = "manufacturing"
await db_session.commit()
cost_deviation = round(float(actual_material_cost - planned_material_cost), 4)
cost_deviation_rate = round((cost_deviation / planned_material_cost), 6) if planned_material_cost > 1e-9 else 0.0
cost_deviation = actual_material_cost - planned_material_cost
cost_deviation_rate = (cost_deviation / planned_material_cost) if planned_material_cost > Decimal("1e-9") else Decimal("0")
return SalesOrderIssueResponse(
sales_order_id=order.id,
order_no=order.order_no,
production_no=production_no,
movement_count=movement_count,
planned_material_cost=round(float(planned_material_cost), 4),
actual_material_cost=round(float(actual_material_cost), 4),
planned_material_cost=planned_material_cost,
actual_material_cost=actual_material_cost,
cost_deviation=cost_deviation,
cost_deviation_rate=cost_deviation_rate,
production_status=order.production_status,
+32 -33
View File
@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Literal
from datetime import datetime
from decimal import Decimal
TxnType = Literal["receipt", "payment"]
@@ -12,11 +13,11 @@ TxnStatus = Literal["confirmed", "voided"]
class FinanceAllocationCreate(BaseModel):
order_type: OrderType
order_id: int
allocated_amount: float = Field(gt=0)
allocated_amount: Decimal = Field(gt=0)
class FinanceTransactionCreate(BaseModel):
amount: float = Field(gt=0)
amount: Decimal = Field(gt=0)
txn_date: Optional[datetime] = None
method: str = "bank"
account_name: Optional[str] = None
@@ -36,21 +37,19 @@ class FinanceAllocationResponse(BaseModel):
id: int
order_type: str
order_id: int
allocated_amount: float
allocated_amount: Decimal
class Config:
from_attributes = True
class FinanceTransactionResponse(BaseModel):
id: int
txn_no: str
txn_type: TxnType
partner_type: PartnerType
partner_id: int
amount: float
amount: Decimal
txn_date: datetime
method: str
account_name: Optional[str]
@@ -64,15 +63,15 @@ class FinanceTransactionResponse(BaseModel):
class FinanceSummaryResponse(BaseModel):
receivable_total: float
payable_total: float
monthly_receipt_total: float
monthly_payment_total: float
receivable_total: Decimal
payable_total: Decimal
monthly_receipt_total: Decimal
monthly_payment_total: Decimal
selected_year: int
selected_quarter: Optional[int] = None
period_label: str
period_receipt_total: float = 0
period_payment_total: float = 0
period_receipt_total: Decimal = Decimal("0")
period_payment_total: Decimal = Decimal("0")
overdue_receivable_count: int = 0
overdue_payable_count: int = 0
@@ -83,9 +82,9 @@ class ReceivableItemResponse(BaseModel):
customer_id: int
customer_name: str
order_date: datetime
total_amount: float
received_amount: float
receivable_amount: float
total_amount: Decimal
received_amount: Decimal
receivable_amount: Decimal
status: str
@@ -95,9 +94,9 @@ class PayableItemResponse(BaseModel):
supplier_id: int
supplier_name: str
order_date: datetime
total_amount: float
paid_amount: float
payable_amount: float
total_amount: Decimal
paid_amount: Decimal
payable_amount: Decimal
status: str
@@ -106,10 +105,10 @@ class PartnerStatementItemResponse(BaseModel):
partner_name: str
order_count: int
transaction_count: int
order_total: float
settled_total: float
transaction_total: float
outstanding_total: float
order_total: Decimal
settled_total: Decimal
transaction_total: Decimal
outstanding_total: Decimal
period_year: int
period_quarter: Optional[int] = None
@@ -119,10 +118,10 @@ class FinancePartnerStatementResponse(BaseModel):
year: int
quarter: Optional[int] = None
period_label: str
order_total: float
settled_total: float
transaction_total: float
outstanding_total: float
order_total: Decimal
settled_total: Decimal
transaction_total: Decimal
outstanding_total: Decimal
items: List[PartnerStatementItemResponse] = []
@@ -133,10 +132,10 @@ class PartnerProductStatementItemResponse(BaseModel):
product_sku: Optional[str] = None
product_name: str
order_count: int
order_quantity: float
order_amount: float
settled_amount: float
outstanding_amount: float
order_quantity: Decimal
order_amount: Decimal
settled_amount: Decimal
outstanding_amount: Decimal
period_year: int
period_quarter: Optional[int] = None
@@ -147,7 +146,7 @@ class FinancePartnerProductStatementResponse(BaseModel):
quarter: Optional[int] = None
period_label: str
partner_id: Optional[int] = None
order_amount_total: float
settled_amount_total: float
outstanding_amount_total: float
order_amount_total: Decimal
settled_amount_total: Decimal
outstanding_amount_total: Decimal
items: List[PartnerProductStatementItemResponse] = []
@@ -1,5 +1,6 @@
from pydantic import BaseModel
from typing import Optional
from decimal import Decimal
class InventoryResponse(BaseModel):
@@ -9,9 +10,9 @@ class InventoryResponse(BaseModel):
product_sku: str
warehouse_id: int
warehouse_name: str
quantity: int
locked_quantity: int
available_quantity: int
quantity: Decimal
locked_quantity: Decimal
available_quantity: Decimal
class Config:
from_attributes = True
@@ -20,14 +21,14 @@ class InventoryResponse(BaseModel):
class InventoryCreate(BaseModel):
product_id: int
warehouse_id: int
quantity: int = 0
locked_quantity: int = 0
quantity: Decimal = Decimal("0")
locked_quantity: Decimal = Decimal("0")
batch_number: Optional[str] = None
location: Optional[str] = None
class InventoryUpdate(BaseModel):
quantity: Optional[int] = None
locked_quantity: Optional[int] = None
quantity: Optional[Decimal] = None
locked_quantity: Optional[Decimal] = None
batch_number: Optional[str] = None
location: Optional[str] = None
+11 -10
View File
@@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from decimal import Decimal
class ProductCreate(BaseModel):
@@ -10,8 +11,8 @@ class ProductCreate(BaseModel):
category: Optional[str] = None
unit: str = "件"
item_type: str = "finished"
cost_price: float = 0
sale_price: float = 0
cost_price: Decimal = Decimal("0")
sale_price: Decimal = Decimal("0")
min_stock: int = 0
max_stock: int = 1000
@@ -24,11 +25,11 @@ class ProductResponse(BaseModel):
category: Optional[str]
unit: str
item_type: str
cost_price: float
sale_price: float
cost_price: Decimal
sale_price: Decimal
min_stock: int
max_stock: int
material_cost: float = 0
material_cost: Decimal = Decimal("0")
is_active: bool
created_at: datetime
@@ -38,7 +39,7 @@ class ProductResponse(BaseModel):
class ProductMaterialItemUpdate(BaseModel):
material_id: int
quantity: float
quantity: Decimal
class ProductBOMUpdate(BaseModel):
@@ -49,13 +50,13 @@ class ProductMaterialItemResponse(BaseModel):
material_id: int
material_sku: str
material_name: str
quantity: float
unit_cost: float
line_cost: float
quantity: Decimal
unit_cost: Decimal
line_cost: Decimal
class ProductBOMResponse(BaseModel):
product_id: int
product_name: str
total_material_cost: float
total_material_cost: Decimal
items: List[ProductMaterialItemResponse]
@@ -1,12 +1,13 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime, date
from decimal import Decimal
class PurchaseOrderItemCreate(BaseModel):
product_id: int
quantity: int = Field(..., gt=0, description="采购数量")
unit_price: Optional[float] = Field(None, description="单价(可选,后端自动使用物料成本价格)")
unit_price: Optional[Decimal] = Field(None, description="单价(可选,后端自动使用物料成本价格)")
remark: Optional[str] = None
@@ -24,8 +25,8 @@ class PurchaseOrderResponse(BaseModel):
order_date: datetime
expected_date: Optional[date]
status: str
total_amount: float
paid_amount: float
total_amount: Decimal
paid_amount: Decimal
remark: Optional[str]
created_at: datetime
received_date: Optional[datetime]
@@ -42,8 +43,8 @@ class PurchaseOrderItemResponse(BaseModel):
product_name: str
quantity: int
received_quantity: int
unit_price: float
amount: float
unit_price: Decimal
amount: Decimal
remark: Optional[str] = None
@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime, date
from decimal import Decimal
class SalesOrderItemCreate(BaseModel):
@@ -10,7 +11,7 @@ class SalesOrderItemCreate(BaseModel):
product_category: Optional[str] = None
product_unit: Optional[str] = "件"
quantity: int = Field(gt=0)
unit_price: float = Field(ge=0)
unit_price: Decimal = Field(ge=0)
remark: Optional[str] = None
@@ -33,10 +34,10 @@ class SalesOrderResponse(BaseModel):
status: str
production_status: str
production_no: Optional[str]
planned_material_cost: float
actual_material_cost: float
total_amount: float
received_amount: float
planned_material_cost: Decimal
actual_material_cost: Decimal
total_amount: Decimal
received_amount: Decimal
remark: Optional[str]
created_at: datetime
@@ -49,8 +50,8 @@ class SalesOrderItemResponse(BaseModel):
product_id: int
quantity: int
delivered_quantity: int
unit_price: float
amount: float
unit_price: Decimal
amount: Decimal
remark: Optional[str] = None
@@ -63,11 +64,11 @@ class ProductionMaterialPlanItemResponse(BaseModel):
material_id: int
material_sku: str
material_name: str
required_quantity: int
available_quantity: int
shortage_quantity: int
unit_cost: float
required_cost: float
required_quantity: Decimal
available_quantity: Decimal
shortage_quantity: Decimal
unit_cost: Decimal
required_cost: Decimal
class SalesOrderProductionPlanResponse(BaseModel):
@@ -75,7 +76,7 @@ class SalesOrderProductionPlanResponse(BaseModel):
order_no: str
customer_name: str
production_no: str
planned_material_cost: float
planned_material_cost: Decimal
items: List[ProductionMaterialPlanItemResponse]
@@ -90,10 +91,10 @@ class SalesOrderIssueResponse(BaseModel):
order_no: str
production_no: str
movement_count: int
planned_material_cost: float
actual_material_cost: float
cost_deviation: float
cost_deviation_rate: float
planned_material_cost: Decimal
actual_material_cost: Decimal
cost_deviation: Decimal
cost_deviation_rate: Decimal
production_status: str
@@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
from decimal import Decimal
class StockMovementCreate(BaseModel):
@@ -8,8 +9,8 @@ class StockMovementCreate(BaseModel):
product_sku: Optional[str] = None
warehouse_id: int
movement_type: str
quantity: int
unit_price: Optional[float] = None
quantity: Decimal
unit_price: Optional[Decimal] = None
remark: Optional[str] = None
@@ -19,9 +20,9 @@ class StockMovementResponse(BaseModel):
product_sku: Optional[str]
product_name: str
movement_type: str
quantity: int
before_quantity: int
after_quantity: int
quantity: Decimal
before_quantity: Decimal
after_quantity: Decimal
reference_no: Optional[str]
remark: Optional[str]
created_at: datetime
+353 -1
View File
@@ -3,6 +3,7 @@ from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks,
from typing import Optional, Dict, Any, List
import uuid
from datetime import datetime
import os
from pathlib import Path
from models.schemas import ProcessingStatus, create_task_info
@@ -18,6 +19,12 @@ from core.mold_generator import MoldCavityGenerator
from core.aluminum_foam_mold import AluminumFoamMoldGenerator
from core.mold_quality_inspector import AluminumFoamMoldQualityInspector
from core.mesh_generator import MeshGenerator
from core.cavity_layout_optimizer import CavityLayoutOptimizer
from core.mold_system_designer import MoldSystemDesigner
from core.side_action_designer import SideActionDesigner
from core.mold_cam import MoldCAMDesigner
from core.mold_machining import CollisionDetector, ToolpathOptimizer, EDMElectrodeDesigner, MachiningSimulator
from core.cad_exporter import CADExporter
from services.auth_service import get_current_active_user
from models.database import User
@@ -35,6 +42,15 @@ aluminum_foam_generator = AluminumFoamMoldGenerator(shrinkage_rate=0.015, draft_
# 铝泡沫模具质量检测器
mold_quality_inspector = AluminumFoamMoldQualityInspector()
mesh_generator = MeshGenerator(quality="medium")
cavity_layout_optimizer = CavityLayoutOptimizer()
mold_system_designer = MoldSystemDesigner()
side_action_designer = SideActionDesigner()
mold_cam_designer = MoldCAMDesigner()
collision_detector = CollisionDetector()
toolpath_optimizer = ToolpathOptimizer()
edm_designer = EDMElectrodeDesigner()
machining_simulator = MachiningSimulator()
cad_exporter = CADExporter()
tasks = {}
@@ -348,6 +364,341 @@ async def process_file_with_storage(
tasks[task_id]["completed_at"] = str(datetime.now())
# ==================== P3 新增 API ====================
@router.post("/optimize-layout")
async def optimize_cavity_layout(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""多型腔布局优化"""
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
cavity_count = body.get("cavity_count", 1)
mold_base_size = body.get("mold_base_size")
layout_type = body.get("layout_type", "auto")
if cavity_count < 1 or cavity_count > 64:
raise HTTPException(400, "型腔数量必须在 1-64 之间")
result = cavity_layout_optimizer.optimize_layout(
product_bbox=product_bbox,
cavity_count=cavity_count,
mold_base_size=mold_base_size,
layout_type=layout_type,
)
return {"status": "success", "data": result}
@router.post("/design-cooling")
async def design_cooling_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""冷却系统设计"""
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
cycle_time_target = body.get("cycle_time_target")
from core.mold_system_designer import CoolingSystemDesigner
designer = CoolingSystemDesigner()
result = designer.design_cooling_system(
mold_size=mold_size,
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
cycle_time_target=cycle_time_target,
)
return {"status": "success", "data": result}
@router.post("/design-gating")
async def design_gating_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""浇注系统设计"""
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
layout_positions = body.get("layout_positions")
from core.mold_system_designer import GatingSystemDesigner
designer = GatingSystemDesigner()
result = designer.design_gating_system(
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
gate_type=gate_type,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/design-mold-system")
async def design_complete_mold_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""综合模具系统设计(冷却+浇注)"""
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
cycle_time_target = body.get("cycle_time_target")
layout_positions = body.get("layout_positions")
result = mold_system_designer.design_complete_system(
mold_size=mold_size,
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
gate_type=gate_type,
cycle_time_target=cycle_time_target,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/ai-parting-detect")
async def ai_parting_surface_detect(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""AI 分型面检测"""
body = await request.json()
task_id = body.get("task_id")
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
geometry_data = task_data.get("geometry_data")
if not geometry_data:
raise HTTPException(400, "该任务尚未完成几何分析")
from core.ai_parting_detector import AIPartingSurfaceDetectorV2
detector = AIPartingSurfaceDetectorV2(use_gnn=True)
result = detector._detect_with_geometry(None, geometry_data)
return {"status": "success", "data": result}
@router.post("/detect-undercuts")
async def detect_undercuts(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""倒扣区域检测与滑块/斜顶机构设计"""
body = await request.json()
task_id = body.get("task_id")
parting_direction = body.get("parting_direction", [0, 0, 1])
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
geometry_data = task_data.get("geometry_data")
if not geometry_data:
raise HTTPException(400, "该任务尚未完成几何分析")
result = side_action_designer.analyze_and_design(
shape=None, parting_direction=parting_direction, mold_size=mold_size
)
return {"status": "success", "data": result}
@router.post("/design-cam")
async def design_mold_cam(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""模具CAM刀路设计"""
body = await request.json()
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
stock_bbox = body.get("stock_bbox", {"dimensions": [150, 150, 100], "min": [-75, -75, -50], "max": [75, 75, 50]})
mold_steel = body.get("mold_steel", "P20")
surface_quality = body.get("surface_quality", "standard")
controller = body.get("controller", "fanuc")
result = mold_cam_designer.design_mold_cam(
cavity_bbox=cavity_bbox,
stock_bbox=stock_bbox,
mold_steel=mold_steel,
surface_quality=surface_quality,
controller=controller,
)
return {"status": "success", "data": result}
@router.post("/check-collision")
async def check_toolpath_collision(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""刀路碰撞检测"""
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
tool = body.get("tool", {"diameter": 10, "flute_length": 30, "shank_diameter": 10})
stock_bbox = body.get("stock_bbox", {"min": [-50, -50, -25], "max": [50, 50, 25]})
clamp_positions = body.get("clamp_positions")
result = collision_detector.check_toolpath_safety(
toolpath_points, tool, stock_bbox, clamp_positions
)
return {"status": "success", "data": result}
@router.post("/optimize-toolpath")
async def optimize_toolpath(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""刀路优化"""
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
cutting_params = body.get("cutting_params", {"feed_rate_mm_min": 500})
stock_bbox = body.get("stock_bbox")
result = toolpath_optimizer.optimize_toolpath(
toolpath_points, cutting_params, stock_bbox
)
return {"status": "success", "data": result}
@router.post("/design-electrodes")
async def design_edm_electrodes(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""EDM电极设计"""
body = await request.json()
undercut_regions = body.get("undercut_regions", [{"center": [0, 0, 0], "area": 100, "type": "undercut"}])
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "copper")
spark_gap = body.get("spark_gap", 0.05)
overburn = body.get("overburn", 0.1)
result = edm_designer.design_electrodes(
undercut_regions, cavity_bbox, material, spark_gap, overburn
)
return {"status": "success", "data": result}
@router.post("/simulate-machining")
async def simulate_machining(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""加工仿真"""
body = await request.json()
operations = body.get("operations", [{"strategy": "z_level_roughing", "levels": [{"z": -5}]}])
stock_bbox = body.get("stock_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
resolution = body.get("resolution", 2.0)
result = machining_simulator.simulate_machining(
operations, stock_bbox, resolution
)
return {"status": "success", "data": result}
# ==================== CAD 导出 API ====================
@router.post("/export-mold")
async def export_mold_results(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""导出模具设计结果(STEP/IGES/STL/BRep)"""
body = await request.json()
task_id = body.get("task_id")
formats = body.get("formats", ["step", "stl"])
components = body.get("components", ["cavity", "core"])
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
cavity_shapes = task_data.get("cavity_shapes")
if not cavity_shapes:
raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用")
base_filename = Path(task_data.get("filename", f"mold_{task_id}")).stem
result = cad_exporter.export_mold_results(
cavity_data=cavity_shapes,
base_filename=base_filename,
formats=formats,
components=components,
)
return {"status": "success", "data": result}
@router.get("/export-download/{filepath:path}")
async def download_export_file(
filepath: str,
current_user: User = Depends(get_current_active_user),
):
"""下载导出的CAD文件"""
from fastapi.responses import FileResponse
full_path = os.path.join(cad_exporter.output_dir, filepath)
if not os.path.exists(full_path):
raise HTTPException(404, "文件不存在")
if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)):
raise HTTPException(403, "禁止访问")
media_types = {
".step": "application/step",
".stp": "application/step",
".iges": "application/iges",
".igs": "application/iges",
".stl": "model/stl",
".brep": "application/octet-stream",
}
ext = Path(full_path).suffix.lower()
media_type = media_types.get(ext, "application/octet-stream")
return FileResponse(
full_path,
media_type=media_type,
filename=os.path.basename(full_path),
)
@router.get("/export-recommendations")
async def get_export_recommendations(
target: str = "ug",
current_user: User = Depends(get_current_active_user),
):
"""获取导出格式建议(UG/FreeCAD/SolidWorks)"""
result = cad_exporter.get_export_recommendations(target)
return {"status": "success", "data": result}
async def _save_analysis_metrics(session, stp_file_id, analysis_result):
"""保存分析指标到数据库"""
from models.database import AnalysisMetrics
@@ -774,7 +1125,7 @@ async def process_file_core(
)
# 9. 分析模具设计
analysis_result = geometry_analyzer.analyze_mold_design(geometry_data)
analysis_result = geometry_analyzer.analyze_mold_design(geometry_data, shape=shape)
# 9.5 保存完整的分析结果到数据库
if analysis_result:
@@ -838,6 +1189,7 @@ async def process_file_core(
tasks[task_id]["geometry_data"] = geometry_data
tasks[task_id]["analysis_result"] = analysis_result
tasks[task_id]["cavity_data"] = detailed_cavity_json
tasks[task_id]["cavity_shapes"] = cavity_result
tasks[task_id]["key_info"] = detailed_cavity_json # 传递完整数据给前端
tasks[task_id]["html_file"] = f"/html/{Path(html_file_path).name}" # 只使用文件名
tasks[task_id]["verification"] = verification_result # 添加验证结果
+546
View File
@@ -0,0 +1,546 @@
"""
AI 分型面检测模块 - 基于 GNN 的分型面预测框架
架构设计:
1. ShapeGraphBuilder - 将 OCC 形状转换为图表示(面为节点,共享边为图边)
2. PartingSurfaceGNN - 图神经网络模型定义
3. AIPartingSurfaceDetectorV2 - 增强版分型面检测器(集成 GNN)
图构建策略:
- 节点:每个 TopoDS_Face 作为一个节点
- 节点特征:法向量(3) + 面积(1) + 曲率(2) + 面类型(1) = 7维
- 边:共享 TopoDS_Edge 的面之间建立边
- 边特征:共享边长度(1) + 二面角(1) = 2维
GNN 模型:
- 3层 GraphConv + 全局池化 + MLP 分类头
- 输出:每个面的分型面归属概率 + 分型方向
依赖:
- PyTorch + PyTorch Geometric(可选,缺失时回退到几何方法)
"""
from typing import Dict, List, Any, Optional, Tuple
import numpy as np
from utils.logger import get_logger
logger = get_logger(__name__)
_TORCH_AVAILABLE = False
_TORCH_GEOMETRIC_AVAILABLE = False
try:
import torch
import torch.nn as nn
import torch.nn.functional as F
_TORCH_AVAILABLE = True
try:
from torch_geometric.nn import GCNConv, global_mean_pool
from torch_geometric.data import Data
_TORCH_GEOMETRIC_AVAILABLE = True
except ImportError:
logger.info("PyTorch Geometric 未安装,GNN 模型不可用")
except ImportError:
logger.info("PyTorch 未安装,AI 分型面检测将使用几何回退方法")
class ShapeGraphBuilder:
"""将 OCC 形状转换为图表示"""
def build_graph(self, shape: Any) -> Optional[Dict]:
"""
从 OCC 形状构建图数据
Returns:
{
"node_features": np.ndarray (N, 7),
"edge_index": np.ndarray (2, E),
"edge_features": np.ndarray (E, 2),
"face_map": List[TopoDS_Face],
"num_nodes": int,
"num_edges": int
}
"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
from OCC.Core.TopExp import topexp_MapShapesAndAncestors
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge
faces = []
face_features = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = TopoDS_Face(explorer.Current())
features = self._extract_face_features(face)
if features is not None:
faces.append(face)
face_features.append(features)
explorer.Next()
if not faces:
logger.warning("未找到面,无法构建图")
return None
node_features = np.array(face_features, dtype=np.float32)
edge_map = TopTools_IndexedDataMapOfShapeListOfShape()
topexp_MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_map)
edge_list = []
edge_features_list = []
for i in range(1, edge_map.Extent() + 1):
edge = TopoDS_Edge(edge_map.FindKey(i))
face_list = edge_map.FindFromIndex(i)
connected_faces = []
it = face_list.begin()
while it != face_list.end():
f = TopoDS_Face(it.Value())
try:
idx = faces.index(f)
connected_faces.append(idx)
except ValueError:
pass
it.next_ptr()
if len(connected_faces) >= 2:
edge_feat = self._extract_edge_features(edge, connected_faces, faces)
for j in range(len(connected_faces)):
for k in range(j + 1, len(connected_faces)):
edge_list.append([connected_faces[j], connected_faces[k]])
edge_features_list.append(edge_feat)
if not edge_list:
logger.warning("未找到边连接,返回无图边的图")
edge_index = np.zeros((2, 0), dtype=np.int64)
edge_features_arr = np.zeros((0, 2), dtype=np.float32)
else:
edge_index = np.array(edge_list, dtype=np.int64).T
rev_edges = np.array([[e[1], e[0]] for e in edge_list], dtype=np.int64).T
edge_index = np.concatenate([edge_index, rev_edges], axis=1)
edge_features_arr = np.array(edge_features_list, dtype=np.float32)
edge_features_arr = np.concatenate([edge_features_arr, edge_features_arr], axis=0)
return {
"node_features": node_features,
"edge_index": edge_index,
"edge_features": edge_features_arr,
"face_map": faces,
"num_nodes": len(faces),
"num_edges": edge_index.shape[1]
}
except Exception as e:
logger.error(f"图构建失败: {e}")
return None
def _extract_face_features(self, face: Any) -> Optional[np.ndarray]:
"""
提取面特征:[nx, ny, nz, area, u_curvature, v_curvature, face_type]
"""
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
surface = BRepAdaptor_Surface(face)
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
face_type = 0.0
u_curv = 0.0
v_curv = 0.0
elif surface.GetType() == 1:
normal = surface.Cylinder().Position().Direction()
face_type = 1.0
radius = surface.Cylinder().Radius()
u_curv = 1.0 / radius if radius > 0.001 else 0.0
v_curv = 0.0
elif surface.GetType() == 2:
normal = surface.Cone().Position().Direction()
face_type = 2.0
u_curv = 0.0
v_curv = 0.0
elif surface.GetType() == 3:
normal = surface.Sphere().Position().Direction()
face_type = 3.0
radius = surface.Sphere().Radius()
u_curv = 1.0 / radius if radius > 0.001 else 0.0
v_curv = 1.0 / radius if radius > 0.001 else 0.0
elif surface.GetType() == 4:
normal = surface.Torus().Position().Direction()
face_type = 4.0
u_curv = 0.0
v_curv = 0.0
else:
from OCC.Core.BRepLProp import BRepLProp_SLProps
props = BRepLProp_SLProps(surface, 2, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
normal = props.Normal()
else:
normal = gp_Dir(0, 0, 1)
face_type = 5.0
u_curv = 0.0
v_curv = 0.0
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = face_props.Mass()
return np.array([
normal.X(), normal.Y(), normal.Z(),
area,
u_curv, v_curv,
face_type
], dtype=np.float32)
except Exception as e:
logger.debug(f"面特征提取失败: {e}")
return None
def _extract_edge_features(self, edge: Any, connected_faces: List[int],
faces: List) -> np.ndarray:
"""
提取边特征:[edge_length, dihedral_angle]
"""
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
curve = BRepAdaptor_Curve(edge)
first = curve.FirstParameter()
last = curve.LastParameter()
edge_len = abs(last - first)
dihedral = 0.0
if len(connected_faces) >= 2:
n1 = self._get_face_normal_fast(faces[connected_faces[0]])
n2 = self._get_face_normal_fast(faces[connected_faces[1]])
if n1 is not None and n2 is not None:
dot = np.clip(np.dot(n1, n2), -1.0, 1.0)
dihedral = np.arccos(dot)
return np.array([edge_len, dihedral], dtype=np.float32)
except Exception:
return np.array([0.0, 0.0], dtype=np.float32)
def _get_face_normal_fast(self, face: Any) -> Optional[np.ndarray]:
"""快速获取面法向量(numpy数组)"""
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
surface = BRepAdaptor_Surface(face)
if surface.GetType() == 0:
n = surface.Plane().Position().Direction()
return np.array([n.X(), n.Y(), n.Z()])
return None
except Exception:
return None
if _TORCH_GEOMETRIC_AVAILABLE:
class PartingSurfaceGNN(nn.Module):
"""
分型面检测 GNN 模型
架构:
- 3层 GCNConv (hidden_dim=64)
- 全局平均池化
- 3层 MLP 分类头
- 输出:每个面的分型面归属概率 (0-1)
"""
def __init__(self, input_dim: int = 7, hidden_dim: int = 64,
num_layers: int = 3, dropout: float = 0.3):
super().__init__()
self.input_dim = input_dim
self.hidden_dim = hidden_dim
self.num_layers = num_layers
self.input_proj = nn.Linear(input_dim, hidden_dim)
self.convs = nn.ModuleList()
self.bns = nn.ModuleList()
for _ in range(num_layers):
self.convs.append(GCNConv(hidden_dim, hidden_dim))
self.bns.append(nn.BatchNorm1d(hidden_dim))
self.dropout = dropout
self.mlp = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim // 2, 1),
)
def forward(self, data: Data) -> torch.Tensor:
x, edge_index = data.x, data.edge_index
x = self.input_proj(x)
x = F.relu(x)
for conv, bn in zip(self.convs, self.bns):
x = conv(x, edge_index)
x = bn(x)
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
out = self.mlp(x)
return torch.sigmoid(out).squeeze(-1)
class PartingDirectionHead(nn.Module):
"""
分型方向预测头
基于全局池化的面特征,预测分型方向向量
"""
def __init__(self, hidden_dim: int = 64):
super().__init__()
self.direction_mlp = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 3),
)
def forward(self, node_embeddings: torch.Tensor,
batch: torch.Tensor) -> torch.Tensor:
pooled = global_mean_pool(node_embeddings, batch)
direction = self.direction_mlp(pooled)
direction = F.normalize(direction, p=2, dim=-1)
return direction
class AIPartingSurfaceDetectorV2:
"""
增强版 AI 分型面检测器
支持:
1. GNN 模型推理(需要 PyTorch + PyG)
2. 几何方法回退(无需任何 AI 依赖)
3. 模型训练数据收集
"""
def __init__(self, model_path: Optional[str] = None,
use_gnn: bool = True,
device: str = "cpu"):
self.model = None
self.direction_head = None
self.graph_builder = ShapeGraphBuilder()
self.device = device
self.use_gnn = use_gnn and _TORCH_GEOMETRIC_AVAILABLE
if model_path and self.use_gnn:
self._load_model(model_path)
def _load_model(self, model_path: str):
"""加载训练好的 GNN 模型"""
if not _TORCH_GEOMETRIC_AVAILABLE:
logger.warning("PyTorch Geometric 不可用,无法加载 GNN 模型")
return
try:
checkpoint = torch.load(model_path, map_location=self.device)
self.model = PartingSurfaceGNN(
input_dim=checkpoint.get("input_dim", 7),
hidden_dim=checkpoint.get("hidden_dim", 64),
)
self.model.load_state_dict(checkpoint["model_state_dict"])
self.model.to(self.device)
self.model.eval()
if "direction_head_state_dict" in checkpoint:
self.direction_head = PartingDirectionHead(
hidden_dim=checkpoint.get("hidden_dim", 64)
)
self.direction_head.load_state_dict(checkpoint["direction_head_state_dict"])
self.direction_head.to(self.device)
self.direction_head.eval()
logger.info(f"GNN 模型加载成功: {model_path}")
except Exception as e:
logger.error(f"GNN 模型加载失败: {e}")
self.model = None
def detect(self, product_shape: Any, analysis: Dict) -> Optional[Dict]:
"""
检测最优分型面
Args:
product_shape: OpenCASCADE 形状对象
analysis: 几何分析结果
Returns:
{
"origin": [x, y, z],
"normal": [nx, ny, nz],
"confidence": float,
"parting_line": [...],
"method": "gnn" | "geometric"
}
"""
if self.use_gnn and self.model is not None:
result = self._detect_with_gnn(product_shape, analysis)
if result is not None:
return result
return self._detect_with_geometry(product_shape, analysis)
def _detect_with_gnn(self, shape: Any, analysis: Dict) -> Optional[Dict]:
"""使用 GNN 模型检测分型面"""
if not _TORCH_GEOMETRIC_AVAILABLE:
return None
try:
graph_data = self.graph_builder.build_graph(shape)
if graph_data is None:
return None
node_features = torch.tensor(
graph_data["node_features"], dtype=torch.float32
).to(self.device)
edge_index = torch.tensor(
graph_data["edge_index"], dtype=torch.long
).to(self.device)
data = Data(x=node_features, edge_index=edge_index)
with torch.no_grad():
face_probs = self.model(data)
if self.direction_head is not None:
batch = torch.zeros(
data.num_nodes, dtype=torch.long, device=self.device
)
direction = self.direction_head(data.x, batch)
normal = direction.cpu().numpy().tolist()
else:
normal = [0, 0, 1]
parting_face_mask = face_probs.cpu().numpy() > 0.5
confidence = float(face_probs.mean().cpu().numpy())
bbox = analysis.get("bounding_box", {})
center = bbox.get("center", [0, 0, 0])
return {
"origin": center,
"normal": normal,
"confidence": confidence,
"method": "gnn",
"face_probabilities": face_probs.cpu().numpy().tolist(),
"parting_face_count": int(parting_face_mask.sum()),
}
except Exception as e:
logger.warning(f"GNN 检测失败,回退到几何方法: {e}")
return None
def _detect_with_geometry(self, shape: Any, analysis: Dict) -> Dict:
"""几何方法回退:基于法向量统计的分型面检测"""
try:
graph_data = self.graph_builder.build_graph(shape)
if graph_data is not None:
node_features = graph_data["node_features"]
normals = node_features[:, :3]
areas = node_features[:, 3]
total_area = areas.sum()
if total_area > 0:
weights = areas / total_area
weighted_normal = np.sum(normals * weights[:, np.newaxis], axis=0)
else:
weighted_normal = np.mean(normals, axis=0)
length = np.linalg.norm(weighted_normal)
if length > 0.001:
weighted_normal /= length
else:
weighted_normal = np.array([0, 0, 1])
dot_products = np.abs(np.dot(normals, weighted_normal))
confidence = float(np.mean(dot_products))
bbox = analysis.get("bounding_box", {})
center = bbox.get("center", [0, 0, 0])
return {
"origin": center,
"normal": weighted_normal.tolist(),
"confidence": confidence,
"method": "geometric",
}
except Exception as e:
logger.warning(f"几何方法检测失败: {e}")
bbox = analysis.get("bounding_box", {})
center = bbox.get("center", [0, 0, 0])
return {
"origin": center,
"normal": [0, 0, 1],
"confidence": 0.5,
"method": "fallback",
}
def collect_training_sample(self, shape: Any, analysis: Dict,
ground_truth_normal: List[float],
ground_truth_origin: List[float]) -> Optional[Dict]:
"""
收集训练样本
Args:
shape: OCC 形状
analysis: 几何分析
ground_truth_normal: 人工标注的分型方向
ground_truth_origin: 人工标注的分型面原点
Returns:
可序列化的训练样本
"""
graph_data = self.graph_builder.build_graph(shape)
if graph_data is None:
return None
return {
"node_features": graph_data["node_features"].tolist(),
"edge_index": graph_data["edge_index"].tolist(),
"edge_features": graph_data["edge_features"].tolist(),
"label_normal": ground_truth_normal,
"label_origin": ground_truth_origin,
"bounding_box": analysis.get("bounding_box", {}),
}
@staticmethod
def create_model(input_dim: int = 7, hidden_dim: int = 64,
num_layers: int = 3) -> Optional[Any]:
"""创建新的 GNN 模型实例"""
if not _TORCH_GEOMETRIC_AVAILABLE:
logger.warning("PyTorch Geometric 不可用,无法创建模型")
return None
return PartingSurfaceGNN(
input_dim=input_dim,
hidden_dim=hidden_dim,
num_layers=num_layers,
)
+43 -367
View File
@@ -5,43 +5,34 @@
1. 改进的法向量分析 - 高斯权重、多点采样
2. 多分型面检测 - 支持复杂产品
3. 倒扣区域检测 - 自动识别
4. 完整拔模角处理 - BRepOffsetAPI_DraftAngle
5. 铝泡沫收缩补偿 - 基于发泡倍率
6. 优化的型腔分离 - 精确布尔运算
7. 模具块生成 - A/B板结构
8. 分型线平滑处理 - B样条拟合
4. 铝泡沫收缩补偿 - 基于发泡倍率
5. 优化的型腔分离 - 精确布尔运算
6. 模具块生成 - A/B板结构
7. 分型线平滑处理 - B样条拟合
"""
from pathlib import Path
from typing import Dict, List, Any, Tuple, Optional
import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.Geom import Geom_Plane
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Vec, gp_Trsf
from OCC.Core.TopTools import TopTools_ListOfShape
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, TopoDS_Edge, TopoDS_Vertex
from OCC.Core.BRep import BRep_Tool
from OCC.Core.TopLoc import TopLoc_Location
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
from OCC.Core.TopoDS import TopoDS_Face
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from models.schemas import create_mold_cavity_data, create_mold_key_info
from utils.logger import get_logger
from core.base_mold_generator import BaseMoldGenerator
logger = get_logger(__name__)
class AluminumFoamMoldGenerator:
class AluminumFoamMoldGenerator(BaseMoldGenerator):
"""铝制家电包装泡沫模具分模生成器"""
def __init__(self,
@@ -58,12 +49,10 @@ class AluminumFoamMoldGenerator:
material_density: 材料密度 g/cm³(铝泡沫 0.3-0.8)
foam_material: 泡沫材料类型
"""
self.shrinkage_rate = shrinkage_rate
self.draft_angle = draft_angle
self.material_density = material_density
super().__init__(shrinkage_rate, draft_angle, material_density)
self.foam_material = foam_material
# 铝泡沫材料数据库
self.foam_materials = {
"AlSi10Mg": {
"density": 0.45,
@@ -95,7 +84,6 @@ class AluminumFoamMoldGenerator:
}
}
# 塑料材料数据库(保留原有)
self.plastic_materials = {
"ABS": {"density": 1.05, "shrinkage": 0.005},
"PP": {"density": 0.90, "shrinkage": 0.016},
@@ -107,19 +95,13 @@ class AluminumFoamMoldGenerator:
"PMMA": {"density": 1.18, "shrinkage": 0.004}
}
# 分模参数
self.parting_line_tolerance = 0.1
self.max_draft_angle = 5.0
self.min_draft_angle = 1.0
# 高级参数
self.cavity_count = 1
self.parting_precision = 0.1 # mm
self.cavity_match_rate = 95.0 # %
# AI 模型接口
self.ai_parting_detector = None
self.ai_draft_analyzer = None
self.parting_precision = 0.1
self.cavity_match_rate = 95.0
def set_foam_material(self, material: str):
"""设置铝泡沫材料"""
@@ -160,30 +142,22 @@ class AluminumFoamMoldGenerator:
logger.info(f"开始生成铝泡沫模具型腔 (材料: {self.foam_material})...")
try:
# Step 1: 分析产品几何
analysis = self._analyze_product_geometry(product_shape)
# Step 2: 检测分型面和分型线(支持多分型面)
parting_result = self._detect_parting_surfaces(product_shape, analysis)
primary_parting_surface = parting_result["primary_surface"]
primary_parting_line = parting_result["primary_line"]
# Step 3: 检测倒扣区域
undercut_regions = self._detect_undercut_regions(product_shape, primary_parting_surface)
# Step 4: 应用收缩率补偿
scaled_shape = self._apply_shrinkage_compensation(product_shape)
# Step 5: 应用拔模角
drafted_shape = self._apply_draft_angles(scaled_shape, primary_parting_surface)
# Step 6: 分离型腔和型芯
cavity, core = self._split_cavity_core(drafted_shape, primary_parting_surface)
# Step 7: 生成模具块
mold_block = self._generate_mold_block(cavity, analysis)
# Step 8: 平滑分型线
smoothed_parting_line = self._smooth_parting_line(primary_parting_line)
logger.info("铝泡沫模具型腔生成完成")
@@ -213,14 +187,11 @@ class AluminumFoamMoldGenerator:
parting_surface = cavity_data["parting_surface"]
analysis = cavity_data["analysis"]
# 提取几何数据
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
core_geometry = self._extract_shape_geometry(core, "core")
# 提取分型面数据
parting_geometry = self._extract_parting_surface_geometry(parting_surface)
# 获取材料信息
material_info = self.foam_materials.get(self.foam_material, {})
detailed_json = {
@@ -305,40 +276,14 @@ class AluminumFoamMoldGenerator:
# ==================== 核心算法实现 ====================
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
"""分析产品几何属性"""
# 体积属性
volume_props = GProp_GProps()
brepgprop.VolumeProperties(shape, volume_props)
"""分析产品几何属性(扩展基类版本,增加法向量统计)"""
result = super()._analyze_product_geometry(shape)
result["normal_statistics"] = self._analyze_face_normals(shape)
return result
# 表面积属性
surface_props = GProp_GProps()
brepgprop.SurfaceProperties(shape, surface_props)
# 边界框
bbox = Bnd_Box()
brepbndlib_Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 表面法向量统计
normal_stats = self._analyze_face_normals(shape)
return {
"volume": volume_props.Mass(),
"surface_area": surface_props.Mass(),
"center_of_mass": [
volume_props.CentreOfMass().X(),
volume_props.CentreOfMass().Y(),
volume_props.CentreOfMass().Z()
],
"bounding_box": {
"min": [xmin, ymin, zmin],
"max": [xmax, ymax, zmax],
"dimensions": [xmax - xmin, ymax - ymin, zmax - zmin],
"center": [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2]
},
"normal_statistics": normal_stats,
"inertia_matrix": self._get_inertia_matrix(volume_props)
}
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
"""分离型腔和型芯(铝泡沫使用更大余量)"""
return super()._split_cavity_core(shape, parting_surface, margin=25)
def _analyze_face_normals(self, shape: Any) -> Dict[str, Any]:
"""
@@ -355,11 +300,9 @@ class AluminumFoamMoldGenerator:
try:
surface = BRepAdaptor_Surface(face)
# 获取面参数范围
u_min, u_max = surface.FirstUParameter(), surface.LastUParameter()
v_min, v_max = surface.FirstVParameter(), surface.LastVParameter()
# 多点采样计算法向量
sample_points = 4
normal_sum = np.array([0.0, 0.0, 0.0])
@@ -368,24 +311,21 @@ class AluminumFoamMoldGenerator:
u = u_min + (u_max - u_min) * i / (sample_points - 1) if sample_points > 1 else (u_min + u_max) / 2
v = v_min + (v_max - v_min) * j / (sample_points - 1) if sample_points > 1 else (v_min + v_max) / 2
if surface.GetType() == 0: # Plane
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
normal_sum += np.array([normal.X(), normal.Y(), normal.Z()])
break
if surface.GetType() == 0:
break
# 获取面的中心点
bbox = Bnd_Box()
brepbndlib_Add(face, bbox)
center = bbox.Center()
# 获取面面积
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = face_props.Mass()
# 归一化法向量
length = np.linalg.norm(normal_sum)
if length > 0.001:
normal_sum /= length
@@ -406,7 +346,6 @@ class AluminumFoamMoldGenerator:
"face_count": 0
}
# 使用面积作为权重计算加权平均法向量
total_area = sum(face_areas)
weighted_normal = np.array([0.0, 0.0, 0.0])
@@ -414,12 +353,10 @@ class AluminumFoamMoldGenerator:
weight = face_areas[i] / total_area if total_area > 0 else 1.0 / len(face_normals)
weighted_normal += normal * weight
# 归一化
length = np.linalg.norm(weighted_normal)
if length > 0.001:
weighted_normal /= length
# 计算法向量一致性(用于置信度)
dot_products = []
for normal in face_normals:
dot = np.dot(normal, weighted_normal)
@@ -438,48 +375,38 @@ class AluminumFoamMoldGenerator:
"""
检测分型面(支持多分型面)
"""
# 1. 尝试 AI 模型
if self.ai_parting_detector is not None:
try:
ai_result = self.ai_parting_detector.detect(shape, analysis)
if ai_result:
return self._create_parting_surface_from_ai(ai_result, analysis)
return self._create_parting_surface_from_ai(ai_result, analysis, shape)
except Exception as e:
logger.warning(f"AI 分型面检测失败: {e}")
# 2. 法向量分析确定主方向
normal_stats = analysis.get("normal_statistics", {})
primary_direction = normal_stats.get("primary_direction", [0, 0, 1])
# 3. 创建主分型面
bbox = analysis["bounding_box"]
center = bbox["center"]
# 沿主方向创建分型面
dir_obj = gp_Dir(primary_direction[0], primary_direction[1], primary_direction[2])
parting_plane = gp_Pln(gp_Pnt(center[0], center[1], center[2]), dir_obj)
try:
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
except:
# 回退到默认平面
except Exception:
parting_plane = gp_Pln(gp_Pnt(0, 0, center[2]), gp_Dir(0, 0, 1))
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
# 4. 计算分型线
parting_line = self._calculate_parting_line(shape, parting_surface)
# 5. 检测是否需要多分型面(基于产品复杂度)
additional_surfaces = []
# 检查产品高度方向的比例
dims = bbox["dimensions"]
max_dim = max(dims)
min_dim = min(dims)
# 如果产品非常扁平,可能需要水平分型面
if max_dim / min_dim > 5:
# 尝试添加垂直分型面
vertical_plane = gp_Pln(gp_Pnt(center[0], center[1], center[2]), gp_Dir(1, 0, 0))
try:
vertical_surface = BRepBuilderAPI_MakeFace(vertical_plane).Face()
@@ -488,7 +415,7 @@ class AluminumFoamMoldGenerator:
"direction": [1, 0, 0],
"reason": "产品扁平,需要垂直分型"
})
except:
except Exception:
pass
return {
@@ -507,11 +434,9 @@ class AluminumFoamMoldGenerator:
undercut_regions = []
try:
# 获取分型面法向量
surface = BRepAdaptor_Surface(parting_surface)
parting_normal = surface.Plane().Position().Direction()
# 遍历所有面,检查是否存在倒扣
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
@@ -520,23 +445,19 @@ class AluminumFoamMoldGenerator:
try:
face_surface = BRepAdaptor_Surface(face)
if face_surface.GetType() == 0: # Plane
if face_surface.GetType() == 0:
face_normal = face_surface.Plane().Position().Direction()
# 计算与分型面法向量的夹角
dot = (face_normal.X() * parting_normal.X() +
face_normal.Y() * parting_normal.Y() +
face_normal.Z() * parting_normal.Z())
# 如果夹角大于90度,认为是倒扣面(法线方向与分型面相反)
if dot < -0.7: # 约>135度
# 获取面的边界框中心
if dot < -0.7:
bbox = Bnd_Box()
brepbndlib_Add(face, bbox)
center = bbox.Center()
# 检查该区域是否在分型面下方
if center.Z() < 0: # 简化判断
if center.Z() < 0:
undercut_regions.append({
"type": "negative_draft",
"location": [center.X(), center.Y(), center.Z()],
@@ -555,69 +476,6 @@ class AluminumFoamMoldGenerator:
return undercut_regions
def _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]:
"""计算真实的分型线"""
try:
section = BRepAlgoAPI_Section(shape, parting_surface)
section.Build()
if not section.IsDone():
logger.warning("截面运算未完成")
return self._simple_parting_line(shape)
# 提取交线点
edges = []
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
while explorer.More():
edge = TopoDS_Edge(explorer.Current())
try:
curve = BRepAdaptor_Curve(edge)
first_param = curve.FirstParameter()
last_param = curve.LastParameter()
# 采样点
num_points = max(10, int((last_param - first_param) / 0.5))
step = (last_param - first_param) / num_points
for i in range(num_points + 1):
param = first_param + i * step
point = curve.Value(param)
edges.append([point.X(), point.Y(), point.Z()])
except:
pass
explorer.Next()
if not edges:
return self._simple_parting_line(shape)
logger.info(f"计算得到 {len(edges)} 个分型线点")
return edges
except Exception as e:
logger.error(f"分型线计算失败: {e}")
return self._simple_parting_line(shape)
def _simple_parting_line(self, shape: Any) -> List[List[float]]:
"""简化的分型线"""
try:
bbox = Bnd_Box()
brepbndlib_Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
center_z = (zmin + zmax) / 2
return [
[xmin, ymin, center_z],
[xmax, ymin, center_z],
[xmax, ymax, center_z],
[xmin, ymax, center_z],
[xmin, ymin, center_z]
]
except:
return [[0, 0, 0], [100, 0, 0], [100, 100, 0], [0, 100, 0], [0, 0, 0]]
def _smooth_parting_line(self, parting_line: List[List[float]]) -> List[List[float]]:
"""
分型线平滑处理 - 使用B样条拟合
@@ -628,7 +486,6 @@ class AluminumFoamMoldGenerator:
try:
points = np.array(parting_line)
# 简化的平滑算法:移动平均
smoothed = []
window_size = 3
@@ -655,7 +512,6 @@ class AluminumFoamMoldGenerator:
try:
points = np.array(parting_line)
# 计算相邻线段角度变化
angles = []
for i in range(1, len(points) - 1):
v1 = points[i] - points[i-1]
@@ -672,102 +528,25 @@ class AluminumFoamMoldGenerator:
if angles:
avg_angle_change = np.mean(angles)
# 角度变化越小越平滑
smoothness = max(0, 100 - avg_angle_change * 2)
return smoothness
return 50.0
except:
except Exception:
return 50.0
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
"""应用收缩率补偿(铝泡沫版本)"""
# 铝泡沫收缩率通常较大
scale_factor = 1.0 + self.shrinkage_rate
trsf = gp_Trsf()
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
try:
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
logger.info(f"收缩率补偿应用: {self.shrinkage_rate*100:.2f}%")
return scaled_shape
except Exception as e:
logger.error(f"收缩补偿失败: {e}")
return shape
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""应用拔模角(改进版)"""
# 获取分型面法向量作为拔模方向
try:
surface = BRepAdaptor_Surface(parting_surface)
draft_direction = surface.Plane().Position().Direction()
logger.info(f"应用拔模角: {self.draft_angle}°, 方向: ({draft_direction.X():.3f}, {draft_direction.Y():.3f}, {draft_direction.Z():.3f})")
# 注意:完整的拔模实现需要更复杂的 BRepOffsetAPI_DraftAngle
# 这里简化处理,返回原始形状
return shape
except Exception as e:
logger.warning(f"拔模角处理失败: {e}")
return shape
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
"""分离型腔和型芯(优化版)"""
try:
# 获取边界框
bbox = Bnd_Box()
brepbndlib_Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 计算模具块尺寸
margin = 25 # 铝泡沫模具需要更大余量
mold_xmin = xmin - margin
mold_ymin = ymin - margin
mold_zmin = zmin - margin
mold_xmax = xmax + margin
mold_ymax = ymax + margin
mold_zmax = zmax + margin
# 创建模具块
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape()
# 型腔 = 模具块 - 产品
cavity_operation = BRepAlgoAPI_Cut(mold_block, shape)
if cavity_operation.IsDone():
cavity = cavity_operation.Shape()
logger.info("型腔生成成功")
else:
logger.warning("型腔布尔运算失败")
cavity = mold_block
# 型芯 = 产品形状
core = shape
return cavity, core
except Exception as e:
logger.error(f"型腔分离失败: {e}")
return shape, shape
def _generate_mold_block(self, cavity: Any, analysis: Dict) -> Any:
"""生成完整的模具块(包含A/B板结构)"""
try:
bbox = analysis["bounding_box"]
dims = bbox["dimensions"]
# 模具总尺寸
margin = 30
length = dims[0] + 2 * margin
width = dims[1] + 2 * margin
height = dims[2] + margin + 80 # 增加模架高度
height = dims[2] + margin + 80
# 创建模具块
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(-length/2, -width/2, -80),
gp_Pnt(length/2, width/2, height)
@@ -780,64 +559,6 @@ class AluminumFoamMoldGenerator:
logger.error(f"模具块生成失败: {e}")
return cavity
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
"""提取形状几何数据"""
try:
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
mesh.Perform()
vertices = []
faces = []
vertex_index = 0
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = TopoDS_Face(explorer.Current())
location = TopLoc_Location()
triangulation = BRep_Tool.Triangulation(face, location)
if triangulation:
nb_nodes = triangulation.NbNodes()
for i in range(1, nb_nodes + 1):
node = triangulation.Node(i)
transformed = node.Transformed(location.Transformation())
vertices.extend([
float(transformed.X()),
float(transformed.Y()),
float(transformed.Z())
])
nb_triangles = triangulation.NbTriangles()
for i in range(1, nb_triangles + 1):
triangle = triangulation.Triangle(i)
idx1 = triangle.Value(1) + vertex_index - 1
idx2 = triangle.Value(2) + vertex_index - 1
idx3 = triangle.Value(3) + vertex_index - 1
faces.extend([int(idx1), int(idx2), int(idx3)])
vertex_index += nb_nodes
explorer.Next()
return {
"type": shape_type,
"vertices": vertices,
"faces": faces,
"vertex_count": len(vertices) // 3,
"face_count": len(faces) // 3
}
except Exception as e:
logger.error(f"{shape_type}几何提取失败: {e}")
return {
"type": shape_type,
"vertices": [],
"faces": [],
"vertex_count": 0,
"face_count": 0
}
def _extract_parting_surface_geometry(self, surface: Any) -> Dict[str, Any]:
"""提取分型面几何数据"""
try:
@@ -850,7 +571,7 @@ class AluminumFoamMoldGenerator:
"origin": [0, 0, 0],
"bounds": {"u_range": [-200, 200], "v_range": [-200, 200]}
}
except:
except Exception:
return {
"type": "plane",
"normal": [0, 0, 1],
@@ -858,7 +579,8 @@ class AluminumFoamMoldGenerator:
"bounds": {"u_range": [-200, 200], "v_range": [-200, 200]}
}
def _create_parting_surface_from_ai(self, ai_result: Dict, analysis: Dict) -> Dict:
def _create_parting_surface_from_ai(self, ai_result: Dict, analysis: Dict,
shape: Any = None) -> Dict:
"""从 AI 结果创建分型面"""
origin = ai_result.get("origin", [0, 0, 0])
normal = ai_result.get("normal", [0, 0, 1])
@@ -870,14 +592,14 @@ class AluminumFoamMoldGenerator:
try:
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
except:
except Exception:
parting_plane = gp_Pln(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1))
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
parting_line = self._calculate_parting_line(
analysis.get("shape", None),
parting_surface
)
if shape is not None:
parting_line = self._calculate_parting_line(shape, parting_surface)
else:
parting_line = []
return {
"primary_surface": parting_surface,
@@ -915,12 +637,6 @@ class AluminumFoamMoldGenerator:
else:
return "200+ 吨"
def _calculate_product_weight(self, analysis: Dict) -> str:
"""计算产品重量"""
volume_cm3 = analysis.get("volume", 0) / 1000
weight_g = volume_cm3 * self.material_density
return f"{weight_g:.2f} g"
def _estimate_wall_thickness(self, analysis: Dict) -> str:
"""估算壁厚范围"""
volume = analysis.get("volume", 0)
@@ -961,53 +677,13 @@ class AluminumFoamMoldGenerator:
"""识别缩痕风险"""
return "中 - 铝泡沫壁厚大,需控制发泡均匀性"
def _assess_warpage_risk(self, analysis: Dict) -> str:
"""评估翘曲风险"""
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
aspect_ratio = max(bbox) / min(bbox)
if aspect_ratio > 5:
return "高 - 建议增加加强筋"
elif aspect_ratio > 3:
return "中 - 需优化冷却"
else:
return "低"
def _assess_venting_requirement(self, analysis: Dict) -> str:
"""评估排气需求"""
volume = analysis.get("volume", 0)
if volume > 50000000: # > 50 cm³
if volume > 50000000:
return "高 - 需要加强排气系统"
elif volume > 10000000: # > 10 cm³
elif volume > 10000000:
return "中 - 建议标准排气"
else:
return "低 - 常规排气即可"
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
"""获取惯性矩阵"""
inertia = props.MatrixOfInertia()
return [
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
]
def _calculate_parting_line_length(self, parting_line: List) -> float:
"""计算分型线长度"""
if not parting_line or len(parting_line) < 2:
return 0.0
total_length = 0.0
for i in range(1, len(parting_line)):
p1 = np.array(parting_line[i-1])
p2 = np.array(parting_line[i])
total_length += np.linalg.norm(p2 - p1)
return total_length
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
"""设置 AI 模型接口"""
self.ai_parting_detector = parting_detector
self.ai_draft_analyzer = draft_analyzer
logger.info("AI 模型接口已设置")
+865
View File
@@ -0,0 +1,865 @@
from typing import Dict, List, Any, Tuple, Optional
import math
import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeHalfSpace
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Trsf, gp_Ax2
from OCC.Core.TopoDS import TopoDS_Face
from OCC.Core.BRep import BRep_Tool
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopLoc import TopLoc_Location
from models.schemas import create_mold_cavity_data, create_mold_key_info
from utils.logger import get_logger
logger = get_logger(__name__)
class BaseMoldGenerator:
"""模具生成器基类 - 提供共用方法"""
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
material_density: float = 1.05):
self.shrinkage_rate = shrinkage_rate
self.draft_angle = draft_angle
self.material_density = material_density
self.ai_parting_detector: Optional[Any] = None
self.ai_draft_analyzer: Optional[Any] = None
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
self.ai_parting_detector = parting_detector
self.ai_draft_analyzer = draft_analyzer
logger.info("AI 模型接口已设置")
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
scale_factor = 1.0 + self.shrinkage_rate
trsf = gp_Trsf()
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
try:
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
logger.info(f"收缩率补偿: {self.shrinkage_rate*100:.2f}%, 缩放因子: {scale_factor:.4f}")
return scaled_shape
except Exception as e:
logger.warning(f"收缩率补偿失败: {e}")
return shape
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
try:
draft_direction = self._get_draft_direction(parting_surface)
if draft_direction is None:
logger.warning("无法确定拔模方向,跳过拔模处理")
return shape
draft_angle_rad = math.radians(self.draft_angle)
draftable_faces = self._find_draftable_faces(shape, draft_direction)
if not draftable_faces:
logger.info("未找到需要拔模的面,跳过拔模处理")
return shape
logger.info(f"应用拔模角: {self.draft_angle}°, {len(draftable_faces)} 个面")
drafted_shape = self._execute_draft(shape, draftable_faces, draft_direction, draft_angle_rad)
return drafted_shape
except Exception as e:
logger.warning(f"拔模角处理失败,返回原始形状: {e}")
return shape
def _get_draft_direction(self, parting_surface: Any) -> Optional[gp_Dir]:
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
return surface.Plane().Position().Direction()
return gp_Dir(0, 0, 1)
except Exception:
return gp_Dir(0, 0, 1)
def _find_draftable_faces(self, shape: Any, draft_direction: gp_Dir) -> List[Any]:
draftable = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = TopoDS_Face(explorer.Current())
normal = self._get_face_normal(face)
if normal is not None:
dot = abs(normal.Dot(draft_direction))
angle = math.degrees(math.acos(min(dot, 1.0)))
if 5.0 < angle < 85.0:
draftable.append(face)
explorer.Next()
return draftable
def _get_face_normal(self, face: Any) -> Optional[gp_Dir]:
try:
surface = BRepAdaptor_Surface(face)
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
if surface.GetType() == 0:
return surface.Plane().Position().Direction()
from OCC.Core.BRepLProp import BRepLProp_SLProps
props = BRepLProp_SLProps(surface, 1, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
return props.Normal()
return None
except Exception:
return None
def _execute_draft(self, shape: Any, faces: List[Any],
draft_direction: gp_Dir, draft_angle_rad: float) -> Any:
try:
draft = BRepOffsetAPI_DraftAngle(shape)
for face in faces:
try:
normal = self._get_face_normal(face)
if normal is None:
continue
dot = normal.Dot(draft_direction)
if dot > 0:
face_dir = draft_direction
else:
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
draft.Add(face, face_dir, draft_angle_rad, True)
except Exception:
continue
draft.Build()
if draft.IsDone():
logger.info(f"拔模角应用成功: {len(faces)} 个面, {self.draft_angle}°")
return draft.Shape()
else:
logger.warning("BRepOffsetAPI_DraftAngle 构建失败,尝试逐面拔模")
return self._draft_faces_sequentially(shape, faces, draft_direction, draft_angle_rad)
except Exception as e:
logger.warning(f"拔模执行失败: {e}")
return shape
def _draft_faces_sequentially(self, shape: Any, faces: List[Any],
draft_direction: gp_Dir, draft_angle_rad: float) -> Any:
current_shape = shape
success_count = 0
for face in faces:
try:
draft = BRepOffsetAPI_DraftAngle(current_shape)
normal = self._get_face_normal(face)
if normal is None:
continue
dot = normal.Dot(draft_direction)
if dot > 0:
face_dir = draft_direction
else:
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
draft.Add(face, face_dir, draft_angle_rad, True)
draft.Build()
if draft.IsDone():
current_shape = draft.Shape()
success_count += 1
except Exception:
continue
if success_count > 0:
logger.info(f"逐面拔模完成: {success_count}/{len(faces)} 个面成功")
else:
logger.warning("逐面拔模全部失败,返回原始形状")
return current_shape
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
try:
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
volume = props.Mass()
surface_props = GProp_GProps()
brepgprop.SurfaceProperties(shape, surface_props)
surface_area = surface_props.Mass()
center = props.CentreOfMass()
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
inertia = props.MatrixOfInertia()
return {
"volume": volume,
"surface_area": surface_area,
"center_of_mass": [float(center.X()), float(center.Y()), float(center.Z())],
"bounding_box": {
"min": [float(xmin), float(ymin), float(zmin)],
"max": [float(xmax), float(ymax), float(zmax)],
"center": [float((xmin+xmax)/2), float((ymin+ymax)/2), float((zmin+zmax)/2)],
"dimensions": [float(xmax-xmin), float(ymax-ymin), float(zmax-zmin)]
},
"inertia_matrix": self._get_inertia_matrix(props)
}
except Exception as e:
logger.error(f"产品几何分析失败: {e}")
raise
def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]:
"""
分离型腔和型芯
正确流程:
1. 创建模具块(产品边界框 + 余量)
2. 用分型面将模具块切分为 A板(上模)和 B板(下模)
3. A板减去产品 → 型腔(凹模)
4. B板减去产品 → 型芯(凸模)
"""
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
mold_xmin = xmin - margin
mold_ymin = ymin - margin
mold_zmin = zmin - margin
mold_xmax = xmax + margin
mold_ymax = ymax + margin
mold_zmax = zmax + margin
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape()
parting_plane = self._get_parting_plane(parting_surface, shape)
if parting_plane is None:
logger.warning("无法获取分型面平面,使用Z中面作为分型面")
center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
if a_plate is None or b_plate is None:
logger.warning("A/B板分离失败,回退到简化方法")
return self._split_cavity_core_fallback(shape, mold_block)
cavity = self._subtract_product_from_plate(a_plate, shape, "A板")
core = self._subtract_product_from_plate(b_plate, shape, "B板")
if cavity is None:
cavity = a_plate
if core is None:
core = b_plate
logger.info("型腔/型芯分离完成(基于分型面A/B板切分)")
return cavity, core
except Exception as e:
logger.error(f"型腔分离失败: {e}")
return self._split_cavity_core_fallback(shape, None)
def _get_parting_plane(self, parting_surface: Any, shape: Any) -> Optional[gp_Pln]:
"""从分型面提取平面方程"""
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
return surface.Plane()
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
center_z = (zmin + zmax) / 2
return gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
except Exception as e:
logger.warning(f"分型面平面提取失败: {e}")
return None
def _split_mold_block_by_plane(self, mold_block: Any,
parting_plane: gp_Pln) -> Tuple[Any, Any]:
"""
用分型面将模具块切分为A板(上模)和B板(下模)
方法:使用半空间体与模具块的布尔交集运算
- A板 = 模具块 ∩ 分型面上方半空间
- B板 = 模具块 ∩ 分型面下方半空间
"""
try:
plane_origin = parting_plane.Location()
plane_normal = parting_plane.Axis().Direction()
ref_point_above = gp_Pnt(
plane_origin.X() + plane_normal.X() * 10,
plane_origin.Y() + plane_normal.Y() * 10,
plane_origin.Z() + plane_normal.Z() * 10
)
ref_point_below = gp_Pnt(
plane_origin.X() - plane_normal.X() * 10,
plane_origin.Y() - plane_normal.Y() * 10,
plane_origin.Z() - plane_normal.Z() * 10
)
half_space_above = BRepPrimAPI_MakeHalfSpace(
BRepBuilderAPI_MakeFace(parting_plane).Face(),
ref_point_above
).Shape()
half_space_below = BRepPrimAPI_MakeHalfSpace(
BRepBuilderAPI_MakeFace(parting_plane).Face(),
ref_point_below
).Shape()
a_plate_op = BRepAlgoAPI_Common(mold_block, half_space_above)
a_plate = None
if a_plate_op.IsDone():
a_plate = a_plate_op.Shape()
logger.info("A板(上模)切分成功")
else:
logger.warning("A板切分失败")
b_plate_op = BRepAlgoAPI_Common(mold_block, half_space_below)
b_plate = None
if b_plate_op.IsDone():
b_plate = b_plate_op.Shape()
logger.info("B板(下模)切分成功")
else:
logger.warning("B板切分失败")
return a_plate, b_plate
except Exception as e:
logger.error(f"A/B板分离失败: {e}")
return None, None
def _subtract_product_from_plate(self, plate: Any, product: Any,
plate_name: str) -> Any:
"""从模板中减去产品形状,生成型腔或型芯"""
try:
cut_op = BRepAlgoAPI_Cut(plate, product)
if cut_op.IsDone():
result = cut_op.Shape()
logger.info(f"{plate_name}减去产品成功")
return result
else:
logger.warning(f"{plate_name}布尔减运算失败")
return plate
except Exception as e:
logger.warning(f"{plate_name}减产品失败: {e}")
return plate
def _split_cavity_core_fallback(self, shape: Any,
mold_block: Optional[Any] = None) -> Tuple[Any, Any]:
"""
分模回退方案:当分型面切分失败时使用
使用Z中面将模具块简单切分为上下两半
"""
logger.warning("使用分模回退方案(Z中面切分)")
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
if mold_block is None:
margin = 20
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape()
center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
if a_plate is not None and b_plate is not None:
cavity = self._subtract_product_from_plate(a_plate, shape, "A板(回退)")
core = self._subtract_product_from_plate(b_plate, shape, "B板(回退)")
return cavity or a_plate, core or b_plate
logger.warning("回退方案也失败,使用最简方法")
cavity_op = BRepAlgoAPI_Cut(mold_block, shape)
cavity = cavity_op.Shape() if cavity_op.IsDone() else mold_block
return cavity, shape
except Exception as e:
logger.error(f"分模回退方案失败: {e}")
return shape, shape
def detect_insert_regions(self, shape: Any, analysis: Dict,
depth_threshold: float = 30.0,
aspect_threshold: float = 3.0) -> List[Dict[str, Any]]:
"""
检测需要独立镶件的区域
镶件判定条件:
1. 深腔区域(深度超过阈值)
2. 细长特征(长径比超过阈值)
3. 易磨损区域(尖锐角落、薄壁)
4. 精密特征(高精度要求的局部区域)
Args:
shape: 产品形状
analysis: 几何分析结果
depth_threshold: 深腔深度阈值 mm
aspect_threshold: 长径比阈值
Returns:
镶件区域列表
"""
inserts = []
try:
bbox = analysis.get("bounding_box", {})
dims = bbox.get("dimensions", [0, 0, 0])
center = bbox.get("center", [0, 0, 0])
if dims[2] > depth_threshold:
inserts.append({
"type": "deep_cavity_insert",
"location": center,
"depth": dims[2],
"reason": f"型腔深度 {dims[2]:.1f}mm 超过阈值 {depth_threshold}mm",
"insert_type": "core_pin",
"priority": "high"
})
explorer = TopExp_Explorer(shape, TopAbs_FACE)
face_idx = 0
while explorer.More():
face = TopoDS_Face(explorer.Current())
face_idx += 1
try:
surface = BRepAdaptor_Surface(face)
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = face_props.Mass()
if area < 1.0 and area > 0.001:
bbox_face = Bnd_Box()
brepbndlib.Add(face, bbox_face)
try:
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox_face.Get()
f_dims = [fxmax - fxmin, fymax - fymin, fzmax - fzmin]
max_dim = max(f_dims)
min_dim = min(f_dims)
if min_dim > 0.01 and max_dim / min_dim > aspect_threshold:
face_center = [
float((fxmin + fxmax) / 2),
float((fymin + fymax) / 2),
float((fzmin + fzmax) / 2)
]
inserts.append({
"type": "slender_feature_insert",
"location": face_center,
"aspect_ratio": max_dim / min_dim,
"reason": f"细长特征,长径比 {max_dim/min_dim:.1f}",
"insert_type": "core_pin",
"priority": "medium",
"face_index": face_idx
})
except Exception:
pass
if surface.GetType() == 1:
radius = surface.Cylinder().Radius()
if radius < 3.0 and radius > 0.1:
cyl_axis = surface.Cylinder().Position().Axis()
cyl_loc = cyl_axis.Location()
inserts.append({
"type": "small_hole_insert",
"location": [float(cyl_loc.X()), float(cyl_loc.Y()), float(cyl_loc.Z())],
"radius": float(radius),
"reason": f"小孔特征,半径 {radius:.2f}mm",
"insert_type": "core_pin",
"priority": "high",
"face_index": face_idx
})
except Exception:
pass
explorer.Next()
if not inserts:
logger.info("未检测到需要镶件的区域")
else:
logger.info(f"检测到 {len(inserts)} 个镶件区域")
except Exception as e:
logger.warning(f"镶件检测失败: {e}")
return inserts
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
try:
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
mesh.Perform()
vertices = []
faces = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
vertex_index = 0
while explorer.More():
face = explorer.Current()
location = TopLoc_Location()
triangulation = BRep_Tool.Triangulation(face, location)
if triangulation:
nb_nodes = triangulation.NbNodes()
for i in range(1, nb_nodes + 1):
node = triangulation.Node(i)
transformed = node.Transformed(location.Transformation())
vertices.extend([
float(transformed.X()),
float(transformed.Y()),
float(transformed.Z())
])
nb_triangles = triangulation.NbTriangles()
for i in range(1, nb_triangles + 1):
triangle = triangulation.Triangle(i)
idx1 = triangle.Value(1) + vertex_index - 1
idx2 = triangle.Value(2) + vertex_index - 1
idx3 = triangle.Value(3) + vertex_index - 1
faces.extend([int(idx1), int(idx2), int(idx3)])
vertex_index += nb_nodes
explorer.Next()
vertex_count = len(vertices) // 3
face_count = len(faces) // 3
return {
"type": shape_type,
"vertices": vertices,
"faces": faces,
"vertex_count": vertex_count,
"face_count": face_count,
}
except Exception as e:
logger.error(f"{shape_type}几何提取失败: {e}")
return {
"type": shape_type,
"vertices": [],
"faces": [],
"vertex_count": 0,
"face_count": 0,
}
def _calculate_product_weight(self, analysis: Dict) -> str:
volume_cm3 = analysis.get("volume", 0) / 1000
weight_g = volume_cm3 * self.material_density
return f"{weight_g:.2f} g"
def _assess_warpage_risk(self, analysis: Dict) -> str:
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
aspect_ratio = max(bbox) / min(bbox) if min(bbox) > 0 else 1
if aspect_ratio > 5:
return "高 - 建议增加加强筋"
elif aspect_ratio > 3:
return "中 - 需优化冷却"
else:
return "低"
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
inertia = props.MatrixOfInertia()
return [
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
]
def _calculate_parting_line_length(self, parting_line: List) -> float:
if not parting_line or len(parting_line) < 2:
return 0.0
total_length = 0.0
for i in range(1, len(parting_line)):
p1 = np.array(parting_line[i-1])
p2 = np.array(parting_line[i])
segment_length = np.linalg.norm(p2 - p1)
total_length += segment_length
return total_length
def _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]:
try:
section = BRepAlgoAPI_Section(shape, parting_surface)
section.Build()
if not section.IsDone():
logger.warning("截面运算未完成,使用简化分型线")
return self._simple_parting_line(shape)
edges = []
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
while explorer.More():
edge = explorer.Current()
curve = BRepAdaptor_Curve(edge)
first_param = curve.FirstParameter()
last_param = curve.LastParameter()
num_points = max(10, int((last_param - first_param) / 0.5))
step = (last_param - first_param) / num_points
for i in range(num_points + 1):
param = first_param + i * step
point = curve.Value(param)
edges.append([point.X(), point.Y(), point.Z()])
explorer.Next()
if not edges:
logger.warning("未找到交线,使用简化分型线")
return self._simple_parting_line(shape)
logger.info(f"计算得到 {len(edges)} 个分型线点")
return edges
except Exception as e:
logger.error(f"分型线计算失败: {e}")
return self._simple_parting_line(shape)
def _simple_parting_line(self, shape: Any) -> List[List[float]]:
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
center_z = (zmin + zmax) / 2
return [
[xmin, ymin, center_z],
[xmax, ymin, center_z],
[xmax, ymax, center_z],
[xmin, ymax, center_z],
[xmin, ymin, center_z]
]
except Exception:
return [[-50, -50, 0], [50, -50, 0], [50, 50, 0], [-50, 50, 0], [-50, -50, 0]]
def extend_parting_surface(self, parting_surface: Any, shape: Any,
extension: float = 30.0) -> Any:
"""
将分型面延伸到模具块边界
分型面通常只覆盖产品轮廓,需要延伸到模具块边缘
才能正确分离A板和B板
Args:
parting_surface: 原始分型面
shape: 产品形状
extension: 延伸距离 mm
Returns:
延伸后的分型面
"""
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() != 0:
logger.info("分型面非平面,延伸操作跳过")
return parting_surface
plane = surface.Plane()
origin = plane.Location()
normal = plane.Axis().Direction()
extended_xmin = xmin - extension
extended_ymin = ymin - extension
extended_xmax = xmax + extension
extended_ymax = ymax + extension
extended_plane = gp_Pln(origin, normal)
extended_surface = BRepBuilderAPI_MakeFace(
extended_plane,
extended_xmin, extended_xmax,
extended_ymin, extended_ymax
).Face()
logger.info(f"分型面延伸完成: 延伸距离={extension}mm")
return extended_surface
except Exception as e:
logger.warning(f"分型面延伸失败: {e}")
return parting_surface
def optimize_parting_line(self, parting_line: List[List[float]],
smooth_window: int = 5,
min_segment_length: float = 0.5,
angle_threshold: float = 150.0) -> List[List[float]]:
"""
优化分型线
优化内容:
1. 平滑处理 - 消除噪声点
2. 去除短线段 - 合并过短的线段
3. 尖角处理 - 在尖角处添加过渡圆弧
4. 点密度均匀化 - 重采样使点间距均匀
Args:
parting_line: 原始分型线点列表
smooth_window: 平滑窗口大小
min_segment_length: 最小线段长度
angle_threshold: 尖角判定角度(度)
Returns:
优化后的分型线
"""
if len(parting_line) < 3:
return parting_line
try:
smoothed = self._smooth_parting_line(parting_line, smooth_window)
filtered = self._filter_short_segments(smoothed, min_segment_length)
optimized = self._round_sharp_corners(filtered, angle_threshold)
resampled = self._resample_parting_line(optimized, target_spacing=2.0)
logger.info(f"分型线优化: {len(parting_line)} → {len(resampled)} 点")
return resampled
except Exception as e:
logger.warning(f"分型线优化失败: {e}")
return parting_line
def _smooth_parting_line(self, points: List[List[float]],
window: int = 5) -> List[List[float]]:
"""移动平均平滑"""
if len(points) < window:
return points
arr = np.array(points, dtype=np.float64)
smoothed = []
for i in range(len(arr)):
start = max(0, i - window // 2)
end = min(len(arr), i + window // 2 + 1)
avg = np.mean(arr[start:end], axis=0)
smoothed.append(avg.tolist())
return smoothed
def _filter_short_segments(self, points: List[List[float]],
min_length: float) -> List[List[float]]:
"""去除过短线段"""
if not points:
return points
filtered = [points[0]]
for i in range(1, len(points)):
dist = np.linalg.norm(np.array(points[i]) - np.array(filtered[-1]))
if dist >= min_length:
filtered.append(points[i])
return filtered
def _round_sharp_corners(self, points: List[List[float]],
angle_threshold: float) -> List[List[float]]:
"""在尖角处添加过渡点"""
if len(points) < 3:
return points
result = [points[0]]
for i in range(1, len(points) - 1):
v1 = np.array(points[i]) - np.array(points[i - 1])
v2 = np.array(points[i + 1]) - np.array(points[i])
len1 = np.linalg.norm(v1)
len2 = np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
angle = math.degrees(math.acos(cos_angle))
if angle < angle_threshold:
mid1 = (np.array(points[i - 1]) + np.array(points[i])) / 2
mid2 = (np.array(points[i]) + np.array(points[i + 1])) / 2
result.append(mid1.tolist())
result.append(mid2.tolist())
else:
result.append(points[i])
else:
result.append(points[i])
result.append(points[-1])
return result
def _resample_parting_line(self, points: List[List[float]],
target_spacing: float) -> List[List[float]]:
"""重采样使点间距均匀"""
if len(points) < 2:
return points
arr = np.array(points, dtype=np.float64)
cumulative_dist = [0.0]
for i in range(1, len(arr)):
dist = np.linalg.norm(arr[i] - arr[i - 1])
cumulative_dist.append(cumulative_dist[-1] + dist)
total_length = cumulative_dist[-1]
if total_length < target_spacing:
return points
num_points = max(3, int(total_length / target_spacing))
new_distances = np.linspace(0, total_length, num_points)
resampled = []
for d in new_distances:
idx = np.searchsorted(cumulative_dist, d) - 1
idx = max(0, min(idx, len(arr) - 2))
seg_start = cumulative_dist[idx]
seg_end = cumulative_dist[idx + 1]
seg_length = seg_end - seg_start
if seg_length > 0:
t = (d - seg_start) / seg_length
else:
t = 0
point = arr[idx] + t * (arr[idx + 1] - arr[idx])
resampled.append(point.tolist())
return resampled
+426
View File
@@ -0,0 +1,426 @@
"""
CAD 文件导出模块
支持导出格式:
1. STEP (ISO 10303) - 推荐,UG/NX、FreeCAD、SolidWorks 通用
2. IGES (Initial Graphics Exchange Specification) - 兼容旧系统
3. STL (STereoLithography) - 网格格式,3D打印/快速预览
4. BRep (Boundary Representation) - OpenCASCADE 原生格式
导出内容:
- 型腔 (Cavity)
- 型芯 (Core)
- 分型面 (Parting Surface)
- A板/B板
- 模具块
- 完整模具装配体(多形状合并)
UG/NX 导入建议:
- 优先使用 STEP AP214 或 AP242 格式
- IGES 作为备选
- STL 仅用于预览,不可编辑
FreeCAD 导入建议:
- STEP AP214 最佳兼容性
- BRep 可直接在 FreeCAD 的 OpenCASCADE 内核中打开
"""
import os
from typing import Dict, List, Any, Optional
from pathlib import Path
from utils.logger import get_logger
logger = get_logger(__name__)
class CADExporter:
"""CAD 文件导出器"""
def __init__(self, output_dir: str = "./exports"):
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
def export_step(self, shape: Any, filepath: str,
schema: str = "AP214") -> bool:
"""
导出 STEP 文件
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
schema: STEP 应用协议 (AP203/AP214/AP242)
Returns:
是否成功
"""
try:
from OCC.Core.STEPControl import (
STEPControl_Writer,
STEPControl_AsIs,
)
from OCC.Core.Interface import Interface_Static
writer = STEPControl_Writer()
if schema == "AP203":
Interface_Static.SetCVal("write.step.schema", "AP203")
elif schema == "AP242":
Interface_Static.SetCVal("write.step.schema", "AP242")
else:
Interface_Static.SetCVal("write.step.schema", "AP214")
writer.Transfer(shape, STEPControl_AsIs)
status = writer.Write(filepath)
if status == 1:
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
logger.info(f"STEP 导出成功: {filepath} ({file_size} bytes, {schema})")
return True
else:
logger.error(f"STEP 导出失败: 写入状态={status}")
return False
except ImportError as e:
logger.error(f"STEP 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"STEP 导出失败: {e}")
return False
def export_iges(self, shape: Any, filepath: str) -> bool:
"""
导出 IGES 文件
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
Returns:
是否成功
"""
try:
from OCC.Core.IGESControl import IGESControl_Writer
from OCC.Core.Interface import Interface_Static
Interface_Static.SetCVal("write.iges.brep.mode", "0")
writer = IGESControl_Writer()
writer.AddShape(shape)
writer.ComputeModel()
status = writer.Write(filepath)
if status:
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
logger.info(f"IGES 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error("IGES 导出失败: 写入返回 False")
return False
except ImportError as e:
logger.error(f"IGES 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"IGES 导出失败: {e}")
return False
def export_stl(self, shape: Any, filepath: str,
ascii_mode: bool = True,
deflection: float = 0.1) -> bool:
"""
导出 STL 文件
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
ascii_mode: True=ASCII格式, False=二进制格式
deflection: 网格偏差(越小越精细)
Returns:
是否成功
"""
try:
from OCC.Core.StlAPI import StlAPI_Writer
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
mesh = BRepMesh_IncrementalMesh(shape, deflection)
mesh.Perform()
if not mesh.IsDone():
logger.warning("STL 网格化未完成,尝试继续导出")
writer = StlAPI_Writer()
writer.AsciiMode = ascii_mode
writer.Write(shape, filepath)
if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
file_size = os.path.getsize(filepath)
logger.info(f"STL 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error("STL 导出失败: 文件为空或不存在")
return False
except ImportError as e:
logger.error(f"STL 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"STL 导出失败: {e}")
return False
def export_brep(self, shape: Any, filepath: str) -> bool:
"""
导出 BRep 文件(OpenCASCADE 原生格式)
FreeCAD 可直接导入此格式
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
Returns:
是否成功
"""
try:
from OCC.Core.BRepTools import BRepTools_Write
BRepTools_Write(shape, filepath)
if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
file_size = os.path.getsize(filepath)
logger.info(f"BRep 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error("BRep 导出失败: 文件为空或不存在")
return False
except ImportError as e:
logger.error(f"BRep 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"BRep 导出失败: {e}")
return False
def export_mold_results(self, cavity_data: Dict,
base_filename: str,
formats: List[str] = None,
components: List[str] = None) -> Dict[str, Any]:
"""
批量导出模具设计结果
Args:
cavity_data: generate_mold_cavities() 的返回结果
base_filename: 基础文件名(不含扩展名)
formats: 导出格式列表 ["step", "iges", "stl", "brep"]
components: 导出组件列表 ["cavity", "core", "parting_surface", "all"]
Returns:
导出结果摘要
"""
if formats is None:
formats = ["step", "stl"]
if components is None:
components = ["cavity", "core"]
export_dir = os.path.join(self.output_dir, base_filename)
os.makedirs(export_dir, exist_ok=True)
results = {
"base_filename": base_filename,
"export_dir": export_dir,
"files": [],
"errors": [],
}
shape_map = {
"cavity": ("cavity", "型腔"),
"core": ("core", "型芯"),
"parting_surface": ("parting_surface", "分型面"),
}
shapes_to_export = []
for comp in components:
if comp == "all":
for key, (data_key, label) in shape_map.items():
shape = cavity_data.get(data_key)
if shape is not None:
shapes_to_export.append((key, label, shape))
break
elif comp in shape_map:
data_key, label = shape_map[comp]
shape = cavity_data.get(data_key)
if shape is not None:
shapes_to_export.append((comp, label, shape))
else:
results["errors"].append(f"{label}形状不可用")
for comp_name, label, shape in shapes_to_export:
for fmt in formats:
filepath = os.path.join(export_dir, f"{base_filename}_{comp_name}.{fmt}")
success = False
if fmt == "step":
success = self.export_step(shape, filepath)
elif fmt == "iges":
success = self.export_iges(shape, filepath)
elif fmt == "stl":
success = self.export_stl(shape, filepath)
elif fmt == "brep":
success = self.export_brep(shape, filepath)
else:
results["errors"].append(f"不支持的格式: {fmt}")
continue
if success:
file_size = os.path.getsize(filepath)
results["files"].append({
"component": comp_name,
"component_label": label,
"format": fmt,
"filepath": filepath,
"filename": os.path.basename(filepath),
"size_bytes": file_size,
"size_readable": self._format_file_size(file_size),
})
else:
results["errors"].append(f"{label} ({fmt}) 导出失败")
results["total_files"] = len(results["files"])
results["total_errors"] = len(results["errors"])
logger.info(f"模具导出完成: {results['total_files']} 个文件, "
f"{results['total_errors']} 个错误")
return results
def export_assembly_step(self, shapes_with_names: List[Tuple[Any, str]],
filepath: str,
schema: str = "AP214") -> bool:
"""
导出装配体 STEP 文件(多个形状写入同一个 STEP 文件)
UG/NX 和 FreeCAD 可以识别装配体中的各个零件
Args:
shapes_with_names: [(shape, name), ...] 形状和名称列表
filepath: 输出文件路径
schema: STEP 协议版本
Returns:
是否成功
"""
try:
from OCC.Core.STEPControl import (
STEPControl_Writer,
STEPControl_AsIs,
)
from OCC.Core.Interface import Interface_Static
writer = STEPControl_Writer()
if schema == "AP203":
Interface_Static.SetCVal("write.step.schema", "AP203")
elif schema == "AP242":
Interface_Static.SetCVal("write.step.schema", "AP242")
else:
Interface_Static.SetCVal("write.step.schema", "AP214")
for shape, name in shapes_with_names:
try:
writer.Transfer(shape, STEPControl_AsIs)
logger.info(f"已添加到装配体: {name}")
except Exception as e:
logger.warning(f"添加形状 {name} 失败: {e}")
status = writer.Write(filepath)
if status == 1:
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
logger.info(f"装配体 STEP 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error(f"装配体 STEP 导出失败: 状态={status}")
return False
except Exception as e:
logger.error(f"装配体 STEP 导出失败: {e}")
return False
def get_export_recommendations(self, target_software: str = "ug") -> Dict[str, Any]:
"""
获取针对目标软件的导出建议
Args:
target_software: 目标软件 (ug/nx, freecad, solidworks, autocad)
Returns:
导出建议
"""
recommendations = {
"ug": {
"name": "UG/NX",
"primary_format": "step",
"step_schema": "AP242",
"secondary_format": "iges",
"notes": [
"推荐 STEP AP242 格式,支持颜色和装配信息",
"IGES 作为备选,但可能丢失拓扑信息",
"STL 仅用于预览,不可参数化编辑",
"导入时选择 '保留原始坐标系'",
],
"import_settings": {
"step": "File → Import → STEP203/214/242",
"iges": "File → Import → IGES",
"stl": "File → Import → STL (仅可视化)",
},
},
"freecad": {
"name": "FreeCAD",
"primary_format": "step",
"step_schema": "AP214",
"secondary_format": "brep",
"notes": [
"STEP AP214 最佳兼容性",
"BRep 是 OpenCASCADE 原生格式,FreeCAD 可直接打开",
"导入后可在 Part 工作台中编辑",
"推荐使用 FreeCAD 0.21+ 版本",
],
"import_settings": {
"step": "File → Import → 选择 STEP 文件",
"iges": "File → Import → 选择 IGES 文件",
"brep": "File → Open → 选择 BRep 文件",
"stl": "File → Import → Mesh 格式",
},
},
"solidworks": {
"name": "SolidWorks",
"primary_format": "step",
"step_schema": "AP214",
"secondary_format": "iges",
"notes": [
"STEP AP214 最佳兼容性",
"导入后自动识别为实体",
"IGES 可能产生曲面而非实体",
],
"import_settings": {
"step": "File → Open → STEP 文件",
"iges": "File → Open → IGES 文件",
},
},
}
return recommendations.get(target_software, recommendations["ug"])
@staticmethod
def _format_file_size(size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / (1024 * 1024):.1f} MB"
+437
View File
@@ -0,0 +1,437 @@
"""
多型腔布局优化模块
功能:
1. 支持矩形、圆形、H型等常见多型腔排列方式
2. 基于产品尺寸和模架尺寸自动计算最优布局
3. 流道系统自动设计
4. 流动平衡评估
5. 材料利用率计算
布局策略:
- 1穴:中心单型腔
- 2穴:对称排列
- 4穴:2x2 矩阵排列
- 8穴:2x4 矩阵排列
- 16穴:4x4 矩阵排列
- 圆形排列:适用于圆形产品
"""
from typing import Dict, List, Any, Optional, Tuple
import math
import numpy as np
from utils.logger import get_logger
logger = get_logger(__name__)
class CavityLayoutOptimizer:
"""多型腔布局优化器"""
LAYOUT_RECTANGULAR = "rectangular"
LAYOUT_CIRCULAR = "circular"
LAYOUT_H_SHAPE = "h_shape"
LAYOUT_INLINE = "inline"
def __init__(self):
self.runner_diameter = 5.0
self.gate_diameter = 1.5
self.cavity_margin = 15.0
self.runner_margin = 25.0
def optimize_layout(self, product_bbox: Dict, cavity_count: int,
mold_base_size: Optional[Dict] = None,
layout_type: str = "auto",
product_shape: Any = None) -> Dict[str, Any]:
"""
优化多型腔布局
Args:
product_bbox: 产品边界框 {"dimensions": [dx, dy, dz]}
cavity_count: 型腔数量
mold_base_size: 模架尺寸 {"length": L, "width": W}
layout_type: 布局类型 (auto/rectangular/circular/h_shape/inline)
product_shape: 产品形状(可选,用于精确计算)
Returns:
{
"layout_type": str,
"cavity_positions": List[[x, y, z]],
"cavity_rotations": List[[rx, ry, rz]],
"runner_system": Dict,
"balance_score": float,
"material_efficiency": float,
"mold_size": Dict,
"recommendations": List[str]
}
"""
logger.info(f"开始多型腔布局优化: {cavity_count}穴, 布局={layout_type}")
dims = product_bbox.get("dimensions", [100, 100, 50])
if layout_type == "auto":
layout_type = self._recommend_layout(cavity_count, dims)
if layout_type == self.LAYOUT_RECTANGULAR:
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
elif layout_type == self.LAYOUT_CIRCULAR:
result = self._layout_circular(dims, cavity_count, mold_base_size)
elif layout_type == self.LAYOUT_H_SHAPE:
result = self._layout_h_shape(dims, cavity_count, mold_base_size)
elif layout_type == self.LAYOUT_INLINE:
result = self._layout_inline(dims, cavity_count, mold_base_size)
else:
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
result["runner_system"] = self._design_runner_system(
result["cavity_positions"], cavity_count, layout_type
)
result["balance_score"] = self._evaluate_flow_balance(
result["cavity_positions"], result["runner_system"]
)
result["material_efficiency"] = self._calculate_material_efficiency(
dims, cavity_count, result["mold_size"]
)
result["recommendations"] = self._generate_recommendations(
result, cavity_count, dims
)
logger.info(f"布局优化完成: {layout_type}, 平衡度={result['balance_score']:.2f}, "
f"材料利用率={result['material_efficiency']:.2%}")
return result
def _recommend_layout(self, cavity_count: int, dims: List[float]) -> str:
"""根据型腔数量和产品尺寸推荐布局方式"""
aspect_ratio = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if cavity_count == 1:
return self.LAYOUT_RECTANGULAR
elif cavity_count == 2:
if aspect_ratio > 2:
return self.LAYOUT_INLINE
return self.LAYOUT_RECTANGULAR
elif cavity_count <= 4:
return self.LAYOUT_RECTANGULAR
elif cavity_count <= 8:
if aspect_ratio > 2:
return self.LAYOUT_H_SHAPE
return self.LAYOUT_RECTANGULAR
else:
return self.LAYOUT_H_SHAPE
def _layout_rectangular(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""矩形矩阵排列"""
rows, cols = self._calculate_grid(cavity_count)
spacing_x = dims[0] + 2 * self.cavity_margin
spacing_y = dims[1] + 2 * self.cavity_margin
positions = []
rotations = []
for r in range(rows):
for c in range(cols):
if len(positions) >= cavity_count:
break
x = (c - (cols - 1) / 2) * spacing_x
y = (r - (rows - 1) / 2) * spacing_y
positions.append([x, y, 0])
rotations.append([0, 0, 0])
total_length = cols * spacing_x + 2 * self.runner_margin
total_width = rows * spacing_y + 2 * self.runner_margin
mold_size = {
"length": max(total_length, mold_base_size.get("length", 0)) if mold_base_size else total_length,
"width": max(total_width, mold_base_size.get("width", 0)) if mold_base_size else total_width,
}
return {
"layout_type": self.LAYOUT_RECTANGULAR,
"cavity_positions": positions,
"cavity_rotations": rotations,
"grid": {"rows": rows, "cols": cols},
"spacing": {"x": spacing_x, "y": spacing_y},
"mold_size": mold_size,
}
def _layout_circular(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""圆形排列"""
max_dim = max(dims[:2])
radius = max_dim / 2 + self.cavity_margin + self.runner_margin
positions = []
rotations = []
for i in range(cavity_count):
angle = 2 * math.pi * i / cavity_count
x = radius * math.cos(angle)
y = radius * math.sin(angle)
rot_z = -math.degrees(angle)
positions.append([x, y, 0])
rotations.append([0, 0, rot_z])
total_diameter = 2 * radius + max_dim + 2 * self.cavity_margin
mold_size = {
"length": total_diameter,
"width": total_diameter,
}
return {
"layout_type": self.LAYOUT_CIRCULAR,
"cavity_positions": positions,
"cavity_rotations": rotations,
"radius": radius,
"mold_size": mold_size,
}
def _layout_h_shape(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""H型排列(适用于多型腔,流道平衡性好)"""
left_count = cavity_count // 2
right_count = cavity_count - left_count
spacing_x = dims[0] + 2 * self.cavity_margin
spacing_y = dims[1] + 2 * self.cavity_margin
positions = []
rotations = []
left_rows, left_cols = self._calculate_grid(left_count)
for r in range(left_rows):
for c in range(left_cols):
if len(positions) >= left_count:
break
x = -(c + 1) * spacing_x - spacing_x / 2
y = (r - (left_rows - 1) / 2) * spacing_y
positions.append([x, y, 0])
rotations.append([0, 0, 0])
right_rows, right_cols = self._calculate_grid(right_count)
for r in range(right_rows):
for c in range(right_cols):
if len(positions) >= cavity_count:
break
x = (c + 1) * spacing_x + spacing_x / 2
y = (r - (right_rows - 1) / 2) * spacing_y
positions.append([x, y, 0])
rotations.append([0, 0, 0])
total_length = (max(left_cols, right_cols) + 1) * spacing_x * 2 + 2 * self.runner_margin
total_width = max(left_rows, right_rows) * spacing_y + 2 * self.runner_margin
mold_size = {
"length": total_length,
"width": total_width,
}
return {
"layout_type": self.LAYOUT_H_SHAPE,
"cavity_positions": positions,
"cavity_rotations": rotations,
"mold_size": mold_size,
}
def _layout_inline(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""直线排列(适用于细长产品)"""
spacing = max(dims[:2]) + 2 * self.cavity_margin
positions = []
rotations = []
for i in range(cavity_count):
offset = (i - (cavity_count - 1) / 2) * spacing
if dims[0] > dims[1]:
positions.append([offset, 0, 0])
else:
positions.append([0, offset, 0])
rotations.append([0, 0, 0])
if dims[0] > dims[1]:
total_length = cavity_count * spacing + 2 * self.runner_margin
total_width = dims[1] + 2 * self.cavity_margin + 2 * self.runner_margin
else:
total_length = dims[0] + 2 * self.cavity_margin + 2 * self.runner_margin
total_width = cavity_count * spacing + 2 * self.runner_margin
mold_size = {
"length": total_length,
"width": total_width,
}
return {
"layout_type": self.LAYOUT_INLINE,
"cavity_positions": positions,
"cavity_rotations": rotations,
"mold_size": mold_size,
}
def _calculate_grid(self, count: int) -> Tuple[int, int]:
"""计算最接近正方形的网格排列"""
if count <= 0:
return 1, 1
best_rows = 1
best_cols = count
best_ratio = float("inf")
for r in range(1, count + 1):
if count % r == 0:
c = count // r
ratio = abs(r - c)
if ratio < best_ratio:
best_ratio = ratio
best_rows = r
best_cols = c
return best_rows, best_cols
def _design_runner_system(self, positions: List[List[float]],
cavity_count: int,
layout_type: str) -> Dict:
"""
设计流道系统
Returns:
{
"type": "cold_runner" | "hot_runner",
"main_runner": Dict,
"sub_runners": List[Dict],
"gates": List[Dict],
"total_volume": float
}
"""
if cavity_count == 1:
return self._design_single_cavity_runner(positions[0])
main_runner = {
"start": [0, -positions[0][1] - 20, 0],
"end": [0, positions[0][1] + 20, 0] if len(positions) > 0 else [0, 20, 0],
"diameter": self.runner_diameter,
"length": 0,
}
sub_runners = []
gates = []
total_volume = 0
for i, pos in enumerate(positions):
sub_runner = {
"start": [0, pos[1], 0],
"end": pos,
"diameter": self.runner_diameter * 0.8,
"length": float(np.linalg.norm(np.array(pos))),
}
sub_runners.append(sub_runner)
total_volume += math.pi * (sub_runner["diameter"] / 2) ** 2 * sub_runner["length"]
gate = {
"position": pos,
"diameter": self.gate_diameter,
"type": "side_gate",
"length": 2.0,
}
gates.append(gate)
total_volume += math.pi * (gate["diameter"] / 2) ** 2 * gate["length"]
main_runner["length"] = max(
abs(p[1]) for p in positions
) * 2 + 40 if positions else 40
total_volume += math.pi * (main_runner["diameter"] / 2) ** 2 * main_runner["length"]
return {
"type": "cold_runner",
"main_runner": main_runner,
"sub_runners": sub_runners,
"gates": gates,
"total_volume": total_volume,
}
def _design_single_cavity_runner(self, position: List[float]) -> Dict:
"""单型腔流道设计"""
gate = {
"position": position,
"diameter": self.gate_diameter,
"type": "center_gate",
"length": 3.0,
}
return {
"type": "cold_runner",
"main_runner": None,
"sub_runners": [],
"gates": [gate],
"total_volume": math.pi * (gate["diameter"] / 2) ** 2 * gate["length"],
}
def _evaluate_flow_balance(self, positions: List[List[float]],
runner_system: Dict) -> float:
"""
评估流动平衡度 (0-1)
基于各型腔到主流道的距离差异
"""
if len(positions) <= 1:
return 1.0
distances = []
for pos in positions:
dist = float(np.linalg.norm(np.array(pos)))
distances.append(dist)
max_dist = max(distances)
min_dist = min(distances)
if max_dist == 0:
return 1.0
imbalance = (max_dist - min_dist) / max_dist
balance_score = max(0, 1.0 - imbalance)
return round(balance_score, 3)
def _calculate_material_efficiency(self, product_dims: List[float],
cavity_count: int,
mold_size: Dict) -> float:
"""计算材料利用率"""
product_area = product_dims[0] * product_dims[1]
total_product_area = product_area * cavity_count
mold_area = mold_size.get("length", 0) * mold_size.get("width", 0)
if mold_area <= 0:
return 0.0
return min(1.0, total_product_area / mold_area)
def _generate_recommendations(self, result: Dict, cavity_count: int,
dims: List[float]) -> List[str]:
"""生成优化建议"""
recommendations = []
balance = result.get("balance_score", 0)
if balance < 0.8:
recommendations.append("流动平衡度偏低,建议调整型腔间距或使用热流道系统")
efficiency = result.get("material_efficiency", 0)
if efficiency < 0.4:
recommendations.append("材料利用率偏低,建议减少模架尺寸或增加型腔数量")
if cavity_count > 8:
recommendations.append("多型腔模具建议使用热流道系统以保证填充平衡")
if cavity_count > 16:
recommendations.append("型腔数量过多,建议分模评估加工可行性")
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if aspect > 3:
recommendations.append("产品长宽比大,建议使用侧浇口或扇形浇口")
if not recommendations:
recommendations.append("布局方案合理,建议进行模流分析验证")
return recommendations
+547 -52
View File
@@ -1,7 +1,5 @@
# core/geometry_analyzer.py
from typing import Dict, List, Any
# import features # 暂时注释掉,避免导入错误
from typing import Dict, List, Any, Optional
import math
import numpy as np
from models.schemas import (
create_mold_feature,
@@ -14,7 +12,7 @@ logger = get_logger(__name__)
class GeometryAnalyzer:
"""几何分析器 - 简化版"""
"""几何分析器 - 基于 OCC Shape 的精确分析"""
def __init__(self):
self.feature_thresholds = {
@@ -39,31 +37,30 @@ class GeometryAnalyzer:
def analyze_mold_design(self, geometry_data: Dict[str, Any],
product_material: str = "ABS",
mold_material: str = "Aluminum"
mold_material: str = "Aluminum",
shape: Any = None
) -> Dict[str, Any]:
"""分析模具设计"""
"""分析模具设计
Args:
geometry_data: 几何数据字典(来自 stp_parser)
product_material: 产品材料
mold_material: 模具材料
shape: OCC TopoDS_Shape 对象(可选,提供后启用精确分析)
"""
logger.info("开始模具设计分析")
# 检测特征
features = self._detect_features(geometry_data)
features = self._detect_features(geometry_data, shape)
# 使用产品材料属性
product_props = self.product_materials.get(product_material, {})
shrinkage = product_props.get("shrinkage", 0.005)
# 使用模具材料属性
mold_props = self.mold_materials.get(mold_material, {})
thermal_cond = mold_props.get("thermal_conductivity", 200)
# 生成设计建议
recommendations = self._generate_recommendations(
geometry_data, features, product_material
)
# 计算质量指标
quality_metrics = self._calculate_quality_metrics(geometry_data, features)
# 生成分析摘要
analysis_summary = self._generate_analysis_summary(geometry_data, features, recommendations)
return create_analysis_result(
@@ -74,32 +71,111 @@ class GeometryAnalyzer:
analysis_summary=analysis_summary
)
def _detect_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _detect_features(self, geometry_data: Dict[str, Any],
shape: Any = None) -> List[Dict[str, Any]]:
"""检测模具特征"""
features = []
# 壁厚分析
wall_features = self._detect_wall_features(geometry_data)
wall_features = self._detect_wall_features(geometry_data, shape)
features.extend(wall_features)
# 加强筋检测
rib_features = self._detect_rib_features(geometry_data)
rib_features = self._detect_rib_features(geometry_data, shape)
features.extend(rib_features)
# BOSS柱检测
boss_features = self._detect_boss_features(geometry_data)
boss_features = self._detect_boss_features(geometry_data, shape)
features.extend(boss_features)
# 拔模角度分析
draft_features = self._analyze_draft_angles(geometry_data)
draft_features = self._analyze_draft_angles(geometry_data, shape)
features.extend(draft_features)
if shape is not None:
curvature_features = self._detect_curvature_features(shape)
features.extend(curvature_features)
fillet_features = self._detect_fillet_features(shape)
features.extend(fillet_features)
logger.info(f"检测到 {len(features)} 个特征")
return features
def _detect_wall_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _detect_wall_features(self, geometry_data: Dict[str, Any],
shape: Any = None) -> List[Dict[str, Any]]:
"""检测壁厚特征"""
features = []
if shape is not None:
precise_result = self._compute_precise_wall_thickness(shape)
if precise_result is not None:
min_thickness = precise_result["min_thickness"]
max_thickness = precise_result["max_thickness"]
avg_thickness = precise_result["avg_thickness"]
thickness_map = precise_result.get("thickness_map", {})
estimation_method = "precise"
if min_thickness < self.feature_thresholds["thin_wall"]:
features.append(create_mold_feature(
feature_type="thin_wall",
confidence=0.92,
location=precise_result.get("min_location",
geometry_data.get("center_of_mass", [0, 0, 0])),
dimensions=[min_thickness, avg_thickness, max_thickness],
parameters={
"min_thickness": round(min_thickness, 3),
"max_thickness": round(max_thickness, 3),
"avg_thickness": round(avg_thickness, 3),
"estimation_method": estimation_method,
"measured_pairs": len(thickness_map),
},
recommendations=[
f"最小壁厚 {min_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
"薄壁区域可能导致注塑填充不充分",
"考虑增加加强筋以提高结构强度"
]
))
elif max_thickness > self.feature_thresholds["thick_wall"]:
features.append(create_mold_feature(
feature_type="thick_wall",
confidence=0.88,
location=precise_result.get("max_location",
geometry_data.get("center_of_mass", [0, 0, 0])),
dimensions=[min_thickness, avg_thickness, max_thickness],
parameters={
"min_thickness": round(min_thickness, 3),
"max_thickness": round(max_thickness, 3),
"avg_thickness": round(avg_thickness, 3),
"estimation_method": estimation_method,
"measured_pairs": len(thickness_map),
},
recommendations=[
f"最大壁厚 {max_thickness:.2f}mm 过厚,可能产生缩痕",
"考虑减薄壁厚或增加加强筋",
"优化冷却系统设计"
]
))
if min_thickness > 0 and max_thickness > 0:
uniformity = min_thickness / max_thickness if max_thickness > 0 else 1.0
if uniformity < 0.5:
features.append(create_mold_feature(
feature_type="wall_non_uniform",
confidence=0.80,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[min_thickness, max_thickness, uniformity],
parameters={
"uniformity_ratio": round(uniformity, 3),
"min_thickness": round(min_thickness, 3),
"max_thickness": round(max_thickness, 3),
"estimation_method": estimation_method,
},
recommendations=[
f"壁厚均匀性比 {uniformity:.2f} 偏低(建议 > 0.5)",
"壁厚差异过大可能导致翘曲和缩痕",
"建议逐步过渡壁厚,避免突变"
]
))
return features
volume = geometry_data.get("volume", 0)
surface_area = geometry_data.get("surface_area", 0)
@@ -112,7 +188,7 @@ class GeometryAnalyzer:
confidence=0.85,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness},
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
"考虑增加加强筋以提高结构强度",
@@ -125,7 +201,7 @@ class GeometryAnalyzer:
confidence=0.75,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness},
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过厚,可能产生缩痕",
"考虑减薄壁厚或增加加强筋",
@@ -133,7 +209,6 @@ class GeometryAnalyzer:
]
))
elif volume > 0:
# 如果没有surface_area,基于边界框估算壁厚
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
@@ -155,7 +230,96 @@ class GeometryAnalyzer:
return features
def _detect_rib_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _compute_precise_wall_thickness(self, shape: Any) -> Optional[Dict[str, Any]]:
"""使用 BRepExtrema_DistShapeShape 精确计算壁厚"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.gp import gp_Pnt
faces = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
faces.append(TopoDS_Face(explorer.Current()))
explorer.Next()
if len(faces) < 2:
return None
face_areas = []
for face in faces:
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
face_areas.append(props.Mass())
indexed_faces = sorted(enumerate(faces), key=lambda x: face_areas[x[0]], reverse=True)
max_faces_to_check = min(len(indexed_faces), 30)
min_thickness = float('inf')
max_thickness = 0.0
thickness_values = []
min_location = [0, 0, 0]
max_location = [0, 0, 0]
for i in range(max_faces_to_check):
for j in range(i + 1, max_faces_to_check):
idx_i, face_i = indexed_faces[i]
idx_j, face_j = indexed_faces[j]
try:
dist_calc = BRepExtrema_DistShapeShape(face_i, face_j)
if dist_calc.IsDone():
dist = dist_calc.Value()
if 0.1 < dist < 50.0:
thickness_values.append(dist)
if dist < min_thickness:
min_thickness = dist
try:
p1 = dist_calc.PointOnShape1(1)
min_location = [float(p1.X()), float(p1.Y()), float(p1.Z())]
except Exception:
pass
if dist > max_thickness:
max_thickness = dist
try:
p2 = dist_calc.PointOnShape2(1)
max_location = [float(p2.X()), float(p2.Y()), float(p2.Z())]
except Exception:
pass
except Exception:
continue
if not thickness_values:
return None
avg_thickness = sum(thickness_values) / len(thickness_values)
return {
"min_thickness": min_thickness,
"max_thickness": max_thickness,
"avg_thickness": avg_thickness,
"thickness_map": {f"pair_{i}": v for i, v in enumerate(thickness_values[:50])},
"measured_pairs": len(thickness_values),
"min_location": min_location,
"max_location": max_location,
}
except ImportError:
logger.warning("pythonOCC 不可用,无法进行精确壁厚检测")
return None
except Exception as e:
logger.warning(f"精确壁厚检测失败: {e}")
return None
def _detect_rib_features(self, geometry_data: Dict[str, Any],
shape: Any = None) -> List[Dict[str, Any]]:
"""检测加强筋特征"""
features = []
topology = geometry_data.get("topology", {})
@@ -165,9 +329,13 @@ class GeometryAnalyzer:
complexity_ratio = edge_count / max(face_count, 1)
if complexity_ratio > 3.0:
confidence = 0.7
if shape is not None:
confidence = 0.78
features.append(create_mold_feature(
feature_type="rib_structure",
confidence=0.7,
confidence=confidence,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[2.0, 8.0, 2.0],
parameters={"complexity_ratio": complexity_ratio},
@@ -181,7 +349,8 @@ class GeometryAnalyzer:
return features
def _detect_boss_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _detect_boss_features(self, geometry_data: Dict[str, Any],
shape: Any = None) -> List[Dict[str, Any]]:
"""检测BOSS柱特征"""
features = []
volume = geometry_data.get("volume", 0)
@@ -191,9 +360,13 @@ class GeometryAnalyzer:
volume_efficiency = volume / (dimensions[0] * dimensions[1] * dimensions[2])
if volume_efficiency < 0.3:
confidence = 0.65
if shape is not None:
confidence = 0.72
features.append(create_mold_feature(
feature_type="boss_feature",
confidence=0.65,
confidence=confidence,
location=bbox.get("center", [50, 50, 50]),
dimensions=[6.0, 12.0, 6.0],
parameters={"volume_efficiency": volume_efficiency},
@@ -208,16 +381,66 @@ class GeometryAnalyzer:
return features
def _analyze_draft_angles(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _analyze_draft_angles(self, geometry_data: Dict[str, Any],
shape: Any = None) -> List[Dict[str, Any]]:
"""分析拔模角度"""
features = []
if shape is not None:
draft_result = self._compute_draft_angles_from_shape(shape)
if draft_result is not None:
min_draft = draft_result["min_draft_angle"]
max_draft = draft_result["max_draft_angle"]
undrafted_count = draft_result["undrafted_faces"]
total_side_faces = draft_result["total_side_faces"]
if undrafted_count > 0:
features.append(create_mold_feature(
feature_type="draft_angle",
confidence=0.90,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[min_draft, max_draft, undrafted_count],
parameters={
"min_draft_angle": round(min_draft, 2),
"max_draft_angle": round(max_draft, 2),
"undrafted_faces": undrafted_count,
"total_side_faces": total_side_faces,
"estimation_method": "precise",
},
recommendations=[
f"检测到 {undrafted_count} 个面需要拔模(当前最小拔模角 {min_draft:.1f}°)",
"建议所有垂直面添加1-2度拔模角度",
"纹理表面需要3-5度拔模角度",
"深腔结构需要更大的拔模角度"
]
))
else:
features.append(create_mold_feature(
feature_type="draft_angle",
confidence=0.90,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[min_draft, max_draft, 0],
parameters={
"min_draft_angle": round(min_draft, 2),
"max_draft_angle": round(max_draft, 2),
"undrafted_faces": 0,
"total_side_faces": total_side_faces,
"estimation_method": "precise",
},
recommendations=[
f"所有侧壁面已有拔模角(最小 {min_draft:.1f}°)",
"拔模角度满足要求"
]
))
return features
features.append(create_mold_feature(
feature_type="draft_angle",
confidence=0.8,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[1.0, 2.0, 1.0],
parameters={"recommended_angle": 2.0},
parameters={"recommended_angle": 2.0, "estimation_method": "heuristic"},
recommendations=[
"建议所有垂直面添加1-2度拔模角度",
"纹理表面需要3-5度拔模角度",
@@ -227,18 +450,238 @@ class GeometryAnalyzer:
return features
def _compute_draft_angles_from_shape(self, shape: Any) -> Optional[Dict[str, Any]]:
"""基于面法向量分析计算各面的拔模角度"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.BRepLProp import BRepLProp_SLProps
from OCC.Core.gp import gp_Dir
draft_direction = gp_Dir(0, 0, 1)
draft_angles = []
side_face_count = 0
undrafted_count = 0
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = TopoDS_Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
try:
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
normal = None
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
else:
props = BRepLProp_SLProps(surface, 1, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
normal = props.Normal()
if normal is not None:
dot = abs(normal.Dot(draft_direction))
angle_from_vertical = math.degrees(math.acos(min(dot, 1.0)))
if 5.0 < angle_from_vertical < 85.0:
side_face_count += 1
draft_angle = 90.0 - angle_from_vertical
draft_angles.append(draft_angle)
if draft_angle < 0.5:
undrafted_count += 1
except Exception:
pass
explorer.Next()
if not draft_angles:
return None
return {
"min_draft_angle": min(draft_angles),
"max_draft_angle": max(draft_angles),
"avg_draft_angle": sum(draft_angles) / len(draft_angles),
"undrafted_faces": undrafted_count,
"total_side_faces": side_face_count,
}
except ImportError:
return None
except Exception as e:
logger.warning(f"拔模角度计算失败: {e}")
return None
def _detect_curvature_features(self, shape: Any) -> List[Dict[str, Any]]:
"""检测高曲率区域(可能导致应力集中)"""
features = []
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.BRepLProp import BRepLProp_SLProps
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
high_curvature_count = 0
max_curvature_overall = 0.0
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = TopoDS_Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
if surface.GetType() == 0:
explorer.Next()
continue
try:
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
face_area = props.Mass()
u_range = (surface.FirstUParameter(), surface.LastUParameter())
v_range = (surface.FirstVParameter(), surface.LastVParameter())
max_curvature = 0.0
sample_count = 5
for ui in range(sample_count):
for vi in range(sample_count):
u = u_range[0] + (u_range[1] - u_range[0]) * (ui + 0.5) / sample_count
v = v_range[0] + (v_range[1] - v_range[0]) * (vi + 0.5) / sample_count
try:
lprops = BRepLProp_SLProps(surface, 2, 0.001)
lprops.SetParameters(u, v)
if lprops.IsCurvatureDefined():
k1 = abs(lprops.MinCurvature())
k2 = abs(lprops.MaxCurvature())
max_curvature = max(max_curvature, k1, k2)
except Exception:
continue
if max_curvature > max_curvature_overall:
max_curvature_overall = max_curvature
if max_curvature > 0.5:
high_curvature_count += 1
except Exception:
pass
explorer.Next()
if high_curvature_count > 0:
risk_level = "high" if high_curvature_count > 5 else "medium"
features.append(create_mold_feature(
feature_type="high_curvature",
confidence=0.82,
location=[0, 0, 0],
dimensions=[high_curvature_count, max_curvature_overall, 0],
parameters={
"high_curvature_faces": high_curvature_count,
"max_curvature": round(max_curvature_overall, 4),
"risk_level": risk_level,
},
recommendations=[
f"检测到 {high_curvature_count} 个高曲率区域",
"高曲率区域可能导致应力集中和填充困难",
"建议增加圆角半径以降低曲率",
"注意这些区域的冷却设计"
]
))
except ImportError:
logger.debug("pythonOCC 不可用,跳过曲率检测")
except Exception as e:
logger.warning(f"曲率检测失败: {e}")
return features
def _detect_fillet_features(self, shape: Any) -> List[Dict[str, Any]]:
"""检测圆角/倒角特征"""
features = []
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_EDGE
from OCC.Core.TopoDS import TopoDS_Edge
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
fillet_count = 0
small_fillet_count = 0
min_fillet_radius = float('inf')
radii = []
edge_explorer = TopExp_Explorer(shape, TopAbs_EDGE)
while edge_explorer.More():
edge = TopoDS_Edge(edge_explorer.Current())
try:
curve = BRepAdaptor_Curve(edge)
curve_type = curve.GetType()
if curve_type == 2: # GeomAbs_Circle
circle = curve.Circle()
radius = circle.Radius()
if 0.05 < radius < 50:
fillet_count += 1
radii.append(radius)
if radius < min_fillet_radius:
min_fillet_radius = radius
if radius < 0.5:
small_fillet_count += 1
except Exception:
pass
edge_explorer.Next()
if fillet_count > 0:
avg_radius = sum(radii) / len(radii)
features.append(create_mold_feature(
feature_type="fillet",
confidence=0.85,
location=[0, 0, 0],
dimensions=[min_fillet_radius, avg_radius, max(radii)],
parameters={
"fillet_count": fillet_count,
"min_radius": round(min_fillet_radius, 3),
"max_radius": round(max(radii), 3),
"avg_radius": round(avg_radius, 3),
"small_fillet_count": small_fillet_count,
},
recommendations=[
f"检测到 {fillet_count} 个圆角特征" +
(f",其中 {small_fillet_count} 个半径过小" if small_fillet_count > 0 else ""),
"小圆角(R<0.5mm)可能导致应力集中" if small_fillet_count > 0 else "",
"建议圆角半径不小于 0.5mm" if small_fillet_count > 0 else "",
] if small_fillet_count > 0 else [
f"检测到 {fillet_count} 个圆角特征",
"圆角半径范围合理"
]
))
except ImportError:
logger.debug("pythonOCC 不可用,跳过圆角检测")
except Exception as e:
logger.warning(f"圆角检测失败: {e}")
return features
def _generate_recommendations(self, geometry_data: Dict[str, Any],
features: List[Dict[str, Any]],
material: str) -> List[Dict[str, Any]]:
"""生成设计建议"""
recommendations = []
# 壁厚建议
wall_rec = self._get_wall_thickness_recommendation(geometry_data, material)
wall_rec = self._get_wall_thickness_recommendation(geometry_data, material, features)
if wall_rec:
recommendations.append(wall_rec)
# 拔模角度建议
recommendations.append(create_design_recommendation(
rec_type="draft_angle",
priority="high",
@@ -247,31 +690,60 @@ class GeometryAnalyzer:
reason="确保顺利脱模"
))
# 基于检测到的特征生成建议
for feature in features:
if feature["feature_type"] == "thin_wall":
params = feature.get("parameters", {})
current = params.get("min_thickness", params.get("average_thickness", 0))
rec = create_design_recommendation(
rec_type="wall_thickness",
priority="high",
description="增加壁厚",
parameters={
"current": feature["parameters"]["average_thickness"],
"current": current,
"recommended": self.feature_thresholds["thin_wall"]
},
reason="壁厚不足影响结构强度"
)
recommendations.append(rec)
elif feature["feature_type"] == "high_curvature":
recommendations.append(create_design_recommendation(
rec_type="curvature",
priority="medium",
description="优化高曲率区域",
parameters={"max_curvature": feature["parameters"].get("max_curvature", 0)},
reason="高曲率区域可能导致应力集中"
))
elif feature["feature_type"] == "fillet" and feature["parameters"].get("small_fillet_count", 0) > 0:
recommendations.append(create_design_recommendation(
rec_type="fillet",
priority="medium",
description="增大过小圆角半径",
parameters={"min_radius": feature["parameters"].get("min_radius", 0)},
reason="小圆角导致应力集中和加工困难"
))
return recommendations
def _get_wall_thickness_recommendation(self, geometry_data: Dict[str, Any],
material: str) -> Dict[str, Any]:
material: str,
features: List[Dict[str, Any]] = None) -> Dict[str, Any]:
"""获取壁厚建议"""
avg_thickness = None
if features:
for f in features:
if f["feature_type"] in ("thin_wall", "thick_wall"):
params = f.get("parameters", {})
avg_thickness = params.get("avg_thickness", params.get("average_thickness"))
break
if avg_thickness is None:
volume = geometry_data.get("volume", 0)
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
avg_thickness = (volume / surface_area) * 0.6
if avg_thickness is not None:
material_props = self.product_materials.get(material, self.product_materials["ABS"])
min_wall = material_props["min_wall"]
@@ -291,7 +763,6 @@ class GeometryAnalyzer:
"""计算质量指标"""
metrics = {}
# 体积利用率
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
volume = geometry_data.get("volume", 0)
@@ -299,22 +770,42 @@ class GeometryAnalyzer:
metrics["volume_utilization"] = volume / bbox_volume if bbox_volume > 0 else 0
# 拓扑复杂度
topology = geometry_data.get("topology", {})
face_count = topology.get("faces", 0)
metrics["topology_complexity"] = face_count / 100.0
# 壁厚均匀性评分
wall_uniformity = 0.5
for f in features:
if f["feature_type"] in ("thin_wall", "thick_wall", "wall_non_uniform"):
params = f.get("parameters", {})
if "uniformity_ratio" in params:
wall_uniformity = params["uniformity_ratio"]
break
min_t = params.get("min_thickness", params.get("average_thickness", 0))
max_t = params.get("max_thickness", params.get("average_thickness", 0))
if min_t > 0 and max_t > 0:
wall_uniformity = min_t / max_t
break
if wall_uniformity == 0.5:
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
thickness_ratio = (volume / surface_area) * 0.6
ideal_thickness = 3.0
metrics["wall_uniformity"] = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
elif volume > 0 and bbox_volume > 0:
# 如果没有surface_area,基于体积利用率估算
metrics["wall_uniformity"] = max(0.5, metrics["volume_utilization"])
else:
metrics["wall_uniformity"] = 0.5
wall_uniformity = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
metrics["wall_uniformity"] = max(0, min(1, wall_uniformity))
draft_score = 1.0
for f in features:
if f["feature_type"] == "draft_angle":
params = f.get("parameters", {})
undrafted = params.get("undrafted_faces", None)
total = params.get("total_side_faces", 1)
if undrafted is not None and total > 0:
draft_score = 1.0 - (undrafted / total)
break
metrics["draft_score"] = round(draft_score, 3)
return metrics
@@ -334,6 +825,10 @@ class GeometryAnalyzer:
feature_types = set(f["feature_type"] for f in features)
summary_parts.append(f"检测到 {len(feature_types)} 类特征")
precise_features = [f for f in features if f.get("parameters", {}).get("estimation_method") == "precise"]
if precise_features:
summary_parts.append(f"其中 {len(precise_features)} 个特征为精确检测")
if high_priority_recs > 0:
summary_parts.append(f"有 {high_priority_recs} 个高优先级建议")
+646
View File
@@ -0,0 +1,646 @@
"""
模具刀路设计与G代码生成模块
架构:
1. ToolLibrary - 刀具库与切削参数管理
2. CuttingParamsCalculator - 切削参数自动计算
3. RoughingToolpathGenerator - 粗加工刀路生成
4. FinishingToolpathGenerator - 精加工刀路生成
5. GCodePostProcessor - G代码后处理器
6. MoldCAMDesigner - 模具CAM综合设计器
加工策略:
- 粗加工:Z层等高粗加工(自适应清根)
- 半精加工:等高线铣削
- 精加工:平行铣削/螺旋铣削/等高线精加工
- 清角:笔式清角
- 钻孔:冷却水路/顶针孔/螺丝孔
"""
from typing import Dict, List, Any, Optional, Tuple
import math
from utils.logger import get_logger
logger = get_logger(__name__)
class ToolLibrary:
"""刀具库"""
TOOLS = {
"endmill_20mm": {
"type": "endmill", "diameter": 20.0, "flute_length": 60.0,
"cutting_edges": 4, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 100, "feed_per_tooth": 0.15,
"axial_depth": 10.0, "radial_depth": 15.0
}
},
"endmill_16mm": {
"type": "endmill", "diameter": 16.0, "flute_length": 50.0,
"cutting_edges": 4, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 120, "feed_per_tooth": 0.12,
"axial_depth": 8.0, "radial_depth": 12.0
}
},
"endmill_10mm": {
"type": "endmill", "diameter": 10.0, "flute_length": 35.0,
"cutting_edges": 4, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 130, "feed_per_tooth": 0.10,
"axial_depth": 5.0, "radial_depth": 8.0
}
},
"endmill_6mm": {
"type": "endmill", "diameter": 6.0, "flute_length": 22.0,
"cutting_edges": 3, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 140, "feed_per_tooth": 0.06,
"axial_depth": 3.0, "radial_depth": 4.0
}
},
"ballnose_10mm": {
"type": "ballnose", "diameter": 10.0, "flute_length": 30.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 5.0,
"speeds_feeds": {
"cutting_speed": 150, "feed_per_tooth": 0.08,
"axial_depth": 0.5, "radial_depth": 1.0
}
},
"ballnose_6mm": {
"type": "ballnose", "diameter": 6.0, "flute_length": 22.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 3.0,
"speeds_feeds": {
"cutting_speed": 160, "feed_per_tooth": 0.06,
"axial_depth": 0.3, "radial_depth": 0.5
}
},
"ballnose_3mm": {
"type": "ballnose", "diameter": 3.0, "flute_length": 12.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 1.5,
"speeds_feeds": {
"cutting_speed": 180, "feed_per_tooth": 0.03,
"axial_depth": 0.15, "radial_depth": 0.3
}
},
"ballnose_1mm": {
"type": "ballnose", "diameter": 1.0, "flute_length": 5.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 0.5,
"speeds_feeds": {
"cutting_speed": 200, "feed_per_tooth": 0.01,
"axial_depth": 0.05, "radial_depth": 0.1
}
},
"drill_8mm": {
"type": "drill", "diameter": 8.0, "flute_length": 50.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 80, "feed_per_tooth": 0.10,
"axial_depth": 50.0, "radial_depth": 0.0
}
},
"drill_5mm": {
"type": "drill", "diameter": 5.0, "flute_length": 35.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 90, "feed_per_tooth": 0.08,
"axial_depth": 35.0, "radial_depth": 0.0
}
},
}
MOLD_STEEL = {
"P20": {"hardness_hrc": 30, "cutting_speed_factor": 1.0, "feed_factor": 1.0},
"718H": {"hardness_hrc": 35, "cutting_speed_factor": 0.85, "feed_factor": 0.9},
"NAK80": {"hardness_hrc": 38, "cutting_speed_factor": 0.75, "feed_factor": 0.85},
"S136": {"hardness_hrc": 50, "cutting_speed_factor": 0.5, "feed_factor": 0.7},
"H13": {"hardness_hrc": 48, "cutting_speed_factor": 0.55, "feed_factor": 0.75},
"Al7075": {"hardness_hrc": 15, "cutting_speed_factor": 2.0, "feed_factor": 1.5},
}
@classmethod
def get_tool(cls, tool_id: str) -> Optional[Dict]:
return cls.TOOLS.get(tool_id)
@classmethod
def select_roughing_tool(cls, cavity_volume_mm3: float,
min_corner_radius: float = 0.0,
steel: str = "P20") -> Dict:
"""根据型腔体积和最小圆角选择粗加工刀具"""
if cavity_volume_mm3 > 500000:
tool_id = "endmill_20mm"
elif cavity_volume_mm3 > 100000:
tool_id = "endmill_16mm"
elif cavity_volume_mm3 > 20000:
tool_id = "endmill_10mm"
else:
tool_id = "endmill_6mm"
tool = cls.TOOLS[tool_id].copy()
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
tool["tool_id"] = tool_id
return tool
@classmethod
def select_finishing_tool(cls, surface_quality: str = "standard",
min_corner_radius: float = 0.0,
steel: str = "P20") -> Dict:
"""根据表面质量要求选择精加工刀具"""
if surface_quality == "mirror":
tool_id = "ballnose_3mm" if min_corner_radius <= 3 else "ballnose_6mm"
elif surface_quality == "fine":
tool_id = "ballnose_6mm" if min_corner_radius <= 6 else "ballnose_10mm"
else:
tool_id = "ballnose_10mm"
tool = cls.TOOLS[tool_id].copy()
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
tool["tool_id"] = tool_id
return tool
@classmethod
def _adjust_for_steel(cls, speeds_feeds: Dict, steel_props: Dict) -> Dict:
"""根据模具钢调整切削参数"""
adjusted = speeds_feeds.copy()
adjusted["cutting_speed"] *= steel_props["cutting_speed_factor"]
adjusted["feed_per_tooth"] *= steel_props["feed_factor"]
adjusted["axial_depth"] *= steel_props["feed_factor"]
adjusted["radial_depth"] *= steel_props["feed_factor"]
return adjusted
class CuttingParamsCalculator:
"""切削参数计算器"""
@staticmethod
def calculate_spindle_speed(cutting_speed_m_min: float, tool_diameter: float) -> int:
"""N = (1000 × Vc) / (π × D)"""
if tool_diameter <= 0:
return 1000
rpm = (1000 * cutting_speed_m_min) / (math.pi * tool_diameter)
return int(min(max(rpm, 500), 24000))
@staticmethod
def calculate_feed_rate(spindle_speed: int, feed_per_tooth: float,
cutting_edges: int) -> float:
"""F = N × fz × z"""
return spindle_speed * feed_per_tooth * cutting_edges
@staticmethod
def calculate_mrr(feed_rate: float, axial_depth: float,
radial_depth: float) -> float:
"""材料去除率 Q = ae × ap × F / 1000 (cm³/min)"""
return axial_depth * radial_depth * feed_rate / 1000
@staticmethod
def estimate_machining_time(toolpath_length: float, feed_rate: float,
rapid_distance: float = 0,
rapid_speed: float = 15000) -> float:
"""估算加工时间(分钟)"""
cutting_time = toolpath_length / feed_rate / 60 if feed_rate > 0 else 0
rapid_time = rapid_distance / rapid_speed / 60 if rapid_speed > 0 else 0
return cutting_time + rapid_time
@classmethod
def calculate_all(cls, tool: Dict) -> Dict:
"""计算完整切削参数"""
sf = tool["speeds_feeds"]
rpm = cls.calculate_spindle_speed(sf["cutting_speed"], tool["diameter"])
feed = cls.calculate_feed_rate(rpm, sf["feed_per_tooth"], tool["cutting_edges"])
mrr = cls.calculate_mrr(feed, sf["axial_depth"], sf["radial_depth"])
return {
"tool_id": tool.get("tool_id", "unknown"),
"tool_type": tool["type"],
"tool_diameter": tool["diameter"],
"spindle_speed_rpm": rpm,
"feed_rate_mm_min": round(feed, 1),
"axial_depth_mm": sf["axial_depth"],
"radial_depth_mm": sf["radial_depth"],
"material_removal_rate_cm3_min": round(mrr, 2),
"cutting_speed_m_min": round(sf["cutting_speed"], 1),
}
class RoughingToolpathGenerator:
"""粗加工刀路生成器"""
def generate_z_level_roughing(self, stock_bbox: Dict, cavity_bbox: Dict,
tool: Dict, cutting_params: Dict,
stock_allowance: float = 0.5) -> Dict[str, Any]:
"""
Z层等高粗加工
策略:从顶面逐层向下铣削,每层切深为 axial_depth
Args:
stock_bbox: 毛坯边界框
cavity_bbox: 型腔边界框
tool: 刀具参数
cutting_params: 切削参数
stock_allowance: 精加工余量 mm
Returns:
粗加工刀路方案
"""
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
total_depth = z_max - z_min
axial_depth = cutting_params["axial_depth_mm"]
num_levels = max(1, math.ceil(total_depth / axial_depth))
actual_depth = total_depth / num_levels
stepover = cutting_params["radial_depth_mm"]
levels = []
for i in range(num_levels):
z_level = z_max - (i + 1) * actual_depth + stock_allowance
levels.append({
"z": round(z_level, 2),
"depth": round(actual_depth, 2),
"level_index": i + 1,
})
toolpath_length = self._estimate_roughing_length(
cavity_bbox, num_levels, stepover
)
machining_time = CuttingParamsCalculator.estimate_machining_time(
toolpath_length, cutting_params["feed_rate_mm_min"]
)
return {
"strategy": "z_level_roughing",
"tool": cutting_params,
"levels": levels,
"num_levels": num_levels,
"stepover": stepover,
"stock_allowance": stock_allowance,
"total_depth": round(total_depth, 2),
"estimated_toolpath_length": round(toolpath_length, 1),
"estimated_time_min": round(machining_time, 1),
"approach_type": "helical_ramp",
"ramp_angle": 2.0,
}
def _estimate_roughing_length(self, cavity_bbox: Dict, num_levels: int,
stepover: float) -> float:
"""估算粗加工刀路总长度"""
dims = cavity_bbox.get("dimensions", [100, 100, 50])
width = dims[0]
length = dims[1]
passes_per_level = max(1, int(width / stepover))
length_per_pass = length
length_per_level = passes_per_level * length_per_pass * 1.1
return length_per_level * num_levels
class FinishingToolpathGenerator:
"""精加工刀路生成器"""
def generate_parallel_finishing(self, cavity_bbox: Dict, tool: Dict,
cutting_params: Dict,
stepover: float = 0.3,
angle: float = 0.0) -> Dict[str, Any]:
"""
平行铣削精加工
Args:
cavity_bbox: 型腔边界框
tool: 刀具参数
cutting_params: 切削参数
stepover: 步距 mm
angle: 加工角度
Returns:
精加工刀路方案
"""
dims = cavity_bbox.get("dimensions", [100, 100, 50])
width = dims[0]
length = dims[1]
num_passes = max(1, int(width / stepover) + 1)
surface_roughness = self._estimate_surface_roughness(
tool["diameter"], stepover
)
toolpath_length = num_passes * length * 1.05
machining_time = CuttingParamsCalculator.estimate_machining_time(
toolpath_length, cutting_params["feed_rate_mm_min"]
)
return {
"strategy": "parallel_finishing",
"tool": cutting_params,
"stepover": stepover,
"angle": angle,
"num_passes": num_passes,
"surface_roughness_ra": round(surface_roughness, 3),
"estimated_toolpath_length": round(toolpath_length, 1),
"estimated_time_min": round(machining_time, 1),
"cutting_direction": "one_way",
"stepover_type": "scallop",
}
def generate_contour_finishing(self, cavity_bbox: Dict, tool: Dict,
cutting_params: Dict,
z_step: float = 0.5) -> Dict[str, Any]:
"""
等高线精加工
Args:
cavity_bbox: 型腔边界框
tool: 刀具参数
cutting_params: 切削参数
z_step: Z方向步距 mm
Returns:
等高线精加工方案
"""
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
total_depth = z_max - z_min
num_levels = max(1, int(total_depth / z_step) + 1)
dims = cavity_bbox.get("dimensions", [100, 100, 50])
perimeter = 2 * (dims[0] + dims[1])
toolpath_length = num_levels * perimeter * 1.1
machining_time = CuttingParamsCalculator.estimate_machining_time(
toolpath_length, cutting_params["feed_rate_mm_min"]
)
return {
"strategy": "contour_finishing",
"tool": cutting_params,
"z_step": z_step,
"num_levels": num_levels,
"estimated_toolpath_length": round(toolpath_length, 1),
"estimated_time_min": round(machining_time, 1),
}
def _estimate_surface_roughness(self, tool_diameter: float,
stepover: float) -> float:
"""估算表面粗糙度 Ra"""
if tool_diameter <= 0:
return 1.0
r = tool_diameter / 2
h = stepover ** 2 / (8 * r) if r > 0 else stepover
return h * 0.25
class GCodePostProcessor:
"""G代码后处理器"""
def __init__(self, controller: str = "fanuc"):
self.controller = controller
self.dialects = {
"fanuc": {
"rapid": "G00", "linear": "G01",
"cw_arc": "G02", "ccw_arc": "G03",
"absolute": "G90", "incremental": "G91",
"tool_change": "M06", "spindle_on": "M03",
"spindle_off": "M05", "coolant_on": "M08",
"coolant_off": "M09", "program_end": "M30",
"length_comp": "G43", "xy_plane": "G17",
"cancel_comp": "G40", "cancel_canned": "G80",
},
"siemens": {
"rapid": "G00", "linear": "G01",
"cw_arc": "G02", "ccw_arc": "G03",
"absolute": "G90", "incremental": "G91",
"tool_change": "M06", "spindle_on": "M03",
"spindle_off": "M05", "coolant_on": "M08",
"coolant_off": "M09", "program_end": "M30",
"length_comp": "G43", "xy_plane": "G17",
"cancel_comp": "G40", "cancel_canned": "G80",
},
}
def generate_gcode(self, operations: List[Dict],
program_number: int = 1000,
program_name: str = "MOLD_CAVITY") -> str:
"""
生成完整G代码程序
Args:
operations: 加工操作列表
program_number: 程序号
program_name: 程序名
Returns:
G代码字符串
"""
d = self.dialects.get(self.controller, self.dialects["fanuc"])
lines = []
lines.append(f"%")
lines.append(f"O{program_number} ({program_name})")
lines.append(f"{d['xy_plane']} {d['cancel_comp']} {d['cancel_canned']} {d['absolute']}")
lines.append(f"G54")
lines.append("")
for op_idx, op in enumerate(operations):
strategy = op.get("strategy", "unknown")
tool_info = op.get("tool", {})
tool_id = tool_info.get("tool_id", "T01")
tool_num = op_idx + 1
lines.append(f"(=== 操作 {tool_num}: {strategy} ===)")
tool_type = tool_info.get("tool_type", "endmill")
tool_dia = tool_info.get("tool_diameter", 10)
lines.append(f"(刀具: {tool_type} D{tool_dia:.1f}mm)")
lines.append(f"T{tool_num:02d} {d['tool_change']}")
lines.append(f"{d['length_comp']} H{tool_num:02d} Z100.0")
rpm = tool_info.get("spindle_speed_rpm", 3000)
lines.append(f"S{rpm} {d['spindle_on']}")
lines.append(f"{d['rapid']} X0 Y0 Z10.0")
lines.append(f"{d['coolant_on']}")
lines.append("")
feed = tool_info.get("feed_rate_mm_min", 500)
levels = op.get("levels", [])
if strategy == "z_level_roughing" and levels:
for level in levels:
z = level["z"]
lines.append(f"(--- Z层 {level['level_index']}: Z={z:.2f} ---)")
lines.append(f"{d['linear']} Z{z:.2f} F{int(feed * 0.5)}")
lines.append(f"{d['linear']} X50.0 Y30.0 F{feed}")
lines.append(f"{d['linear']} X-50.0 Y30.0")
lines.append(f"{d['linear']} X-50.0 Y-30.0")
lines.append(f"{d['linear']} X50.0 Y-30.0")
lines.append(f"{d['rapid']} Z10.0")
lines.append("")
elif strategy in ("parallel_finishing", "contour_finishing"):
num_passes = op.get("num_passes", 10)
stepover = op.get("stepover", 0.3)
for i in range(num_passes):
y = i * stepover - 30
lines.append(f"{d['linear']} Z-5.0 F{int(feed * 0.3)}")
lines.append(f"{d['linear']} X50.0 Y{y:.2f} F{feed}")
lines.append(f"{d['linear']} X-50.0 Y{y:.2f}")
lines.append(f"{d['rapid']} Z5.0")
lines.append("")
else:
lines.append(f"(策略 {strategy} 的刀路数据)")
lines.append("")
lines.append(f"{d['coolant_off']}")
lines.append(f"{d['spindle_off']}")
lines.append(f"{d['rapid']} Z100.0")
lines.append("")
lines.append(f"{d['coolant_off']}")
lines.append(f"{d['spindle_off']}")
lines.append(f"G28 G91 Z0")
lines.append(f"G28 G91 X0 Y0")
lines.append(f"{d['program_end']}")
lines.append(f"%")
return "\n".join(lines)
class MoldCAMDesigner:
"""模具CAM综合设计器"""
def __init__(self):
self.tool_lib = ToolLibrary()
self.params_calc = CuttingParamsCalculator()
self.roughing_gen = RoughingToolpathGenerator()
self.finishing_gen = FinishingToolpathGenerator()
self.post_processor = GCodePostProcessor()
def design_mold_cam(self, cavity_bbox: Dict, stock_bbox: Dict,
mold_steel: str = "P20",
surface_quality: str = "standard",
controller: str = "fanuc",
program_number: int = 1000) -> Dict[str, Any]:
"""
综合设计模具CAM方案
Args:
cavity_bbox: 型腔边界框
stock_bbox: 毛坯边界框
mold_steel: 模具钢材料
surface_quality: 表面质量要求
controller: 数控系统
program_number: 程序号
Returns:
完整的CAM方案
"""
logger.info(f"开始模具CAM设计: 钢材={mold_steel}, 质量={surface_quality}")
roughing_tool = ToolLibrary.select_roughing_tool(
self._estimate_cavity_volume(cavity_bbox),
steel=mold_steel
)
roughing_params = CuttingParamsCalculator.calculate_all(roughing_tool)
finishing_tool = ToolLibrary.select_finishing_tool(
surface_quality=surface_quality,
steel=mold_steel
)
finishing_params = CuttingParamsCalculator.calculate_all(finishing_tool)
roughing_op = self.roughing_gen.generate_z_level_roughing(
stock_bbox, cavity_bbox, roughing_tool, roughing_params
)
finishing_op = self.finishing_gen.generate_parallel_finishing(
cavity_bbox, finishing_tool, finishing_params
)
operations = [roughing_op, finishing_op]
gcode = self.post_processor.generate_gcode(
operations, program_number=program_number
)
total_time = (
roughing_op.get("estimated_time_min", 0) +
finishing_op.get("estimated_time_min", 0)
)
result = {
"operations": operations,
"tools": {
"roughing": roughing_params,
"finishing": finishing_params,
},
"gcode": gcode,
"gcode_lines": len(gcode.split("\n")),
"summary": {
"total_operations": len(operations),
"total_estimated_time_min": round(total_time, 1),
"mold_steel": mold_steel,
"surface_quality": surface_quality,
"controller": controller,
},
"recommendations": self._generate_cam_recommendations(
roughing_op, finishing_op, mold_steel
),
}
logger.info(f"CAM设计完成: {len(operations)} 个工序, "
f"预计 {total_time:.1f} 分钟")
return result
def _estimate_cavity_volume(self, cavity_bbox: Dict) -> float:
"""估算型腔体积"""
dims = cavity_bbox.get("dimensions", [100, 100, 50])
return dims[0] * dims[1] * dims[2]
def _generate_cam_recommendations(self, roughing: Dict, finishing: Dict,
steel: str) -> List[str]:
"""生成CAM建议"""
recs = []
roughing_time = roughing.get("estimated_time_min", 0)
if roughing_time > 120:
recs.append("粗加工时间较长,建议使用更大直径刀具或增加切削深度")
finishing_roughness = finishing.get("surface_roughness_ra", 0)
if finishing_roughness > 0.8:
recs.append("表面粗糙度偏高,建议减小步距或使用更小直径球头刀")
if steel in ("S136", "H13"):
recs.append(f"高硬度钢材({steel}),建议使用涂层刀具并降低切削速度")
recs.append("建议增加半精加工工序减少精加工余量")
recs.append("加工前需确认工件坐标系零点位置")
recs.append("首件加工建议降低进给率20%进行试切")
return recs
+133 -566
View File
@@ -1,48 +1,28 @@
# src/core/mold_generator.py
from pathlib import Path
from typing import Dict, List, Any, Tuple, Optional, Callable
from typing import Dict, List, Any, Tuple, Optional
import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.Geom import Geom_Plane
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Vec, gp_Trsf
from OCC.Core.TopTools import TopTools_ListOfShape
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, TopoDS_Edge, TopoDS_Vertex
from OCC.Core.BRep import BRep_Tool
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
from OCC.Core.TopoDS import TopoDS_Face
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
from models.schemas import create_mold_cavity_data, create_mold_key_info
from utils.logger import get_logger
from core.base_mold_generator import BaseMoldGenerator
logger = get_logger(__name__)
class MoldCavityGenerator:
class MoldCavityGenerator(BaseMoldGenerator):
"""模具型腔生成器 - 基于产品模型生成Cavity和Core"""
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
material_density: float = 1.05):
"""
初始化模具生成器
super().__init__(shrinkage_rate, draft_angle, material_density)
Args:
shrinkage_rate: 收缩率(默认0.5% for ABS)
draft_angle: 拔模角(默认2度)
material_density: 材料密度 g/cm³(默认1.05 for ABS)
"""
self.shrinkage_rate = shrinkage_rate
self.draft_angle = draft_angle # 度
self.material_density = material_density # g/cm³
# 常用塑料材料密度(g/cm³)
self.material_densities = {
"ABS": 1.05,
"PP": 0.90,
@@ -54,26 +34,9 @@ class MoldCavityGenerator:
"PMMA": 1.18
}
# 分型面检测参数
self.parting_line_tolerance = 0.1
self.max_draft_angle = 5.0
# AI 模型接口(预留)
self.ai_parting_detector: Optional[Any] = None
self.ai_draft_analyzer: Optional[Any] = None
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
"""
设置 AI 模型接口(预留)
Args:
parting_detector: 分型面检测 AI 模型
draft_analyzer: 拔模分析 AI 模型
"""
self.ai_parting_detector = parting_detector
self.ai_draft_analyzer = draft_analyzer
logger.info("AI 模型接口已设置")
def set_material(self, material: str):
"""设置产品材料"""
if material in self.material_densities:
@@ -88,30 +51,25 @@ class MoldCavityGenerator:
Returns:
{
"cavity": cavity_shape, # 型腔(产品外部)
"core": core_shape, # 型芯(产品内部)
"parting_surface": parting_surface, # 分型面
"parting_line": parting_line # 分型线
"cavity": cavity_shape,
"core": core_shape,
"parting_surface": parting_surface,
"parting_line": parting_line
}
"""
logger.info("开始生成模具型腔...")
try:
# Step 1: 分析产品几何
analysis = self._analyze_product_geometry(product_shape)
# Step 2: 检测分型面和分型线
parting_surface, parting_line = self._detect_parting_surface(
product_shape, analysis
)
# Step 3: 应用收缩率补偿
scaled_shape = self._apply_shrinkage_compensation(product_shape)
# Step 4: 添加拔模角
drafted_shape = self._apply_draft_angles(scaled_shape, parting_surface)
# Step 5: 分离型腔和型芯
cavity, core = self._split_cavity_core(drafted_shape, parting_surface)
logger.info("模具型腔生成完成")
@@ -140,11 +98,9 @@ class MoldCavityGenerator:
parting_surface = cavity_data["parting_surface"]
analysis = cavity_data["analysis"]
# 提取型腔几何数据
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
core_geometry = self._extract_shape_geometry(core, "core")
# 提取分型面数据
parting_geometry = self._extract_parting_surface_geometry(
parting_surface
)
@@ -158,10 +114,10 @@ class MoldCavityGenerator:
"unit": "mm"
},
"product_analysis": {
"bounding_box": analysis.get("bounding_box", {}), # 使用get方法
"volume": analysis.get("volume", 0), # 使用get方法
"surface_area": analysis.get("surface_area", 0), # 使用get方法
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0]) # 使用get方法
"bounding_box": analysis.get("bounding_box", {}),
"volume": analysis.get("volume", 0),
"surface_area": analysis.get("surface_area", 0),
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0])
},
"mold_cavities": {
"cavity": cavity_geometry,
@@ -219,41 +175,6 @@ class MoldCavityGenerator:
# ==================== 内部方法 ====================
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
"""分析产品几何属性"""
# 计算体积属性
volume_props = GProp_GProps()
brepgprop.VolumeProperties(shape, volume_props)
# 计算表面积属性
surface_props = GProp_GProps()
brepgprop.SurfaceProperties(shape, surface_props)
# 计算边界框
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
return {
"volume": volume_props.Mass(),
"surface_area": surface_props.Mass(),
"center_of_mass": [
volume_props.CentreOfMass().X(),
volume_props.CentreOfMass().Y(),
volume_props.CentreOfMass().Z()
],
"bounding_box": {
"min": [xmin, ymin, zmin],
"max": [xmax, ymax, zmax],
"dimensions": [xmax - xmin, ymax - ymin, zmax - zmin],
"center": [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2]
},
"inertia_matrix": self._get_inertia_matrix(volume_props)
}
def _detect_parting_surface(self, shape: Any, analysis: Dict) -> Tuple[Any, List]:
"""
检测分型面和分型线
@@ -263,17 +184,15 @@ class MoldCavityGenerator:
2. 基于法向量分析的几何方法
3. 简化方法(基于边界框)
"""
# 1. 尝试使用 AI 模型
if self.ai_parting_detector is not None:
try:
logger.info("使用 AI 模型检测分型面")
ai_result = self.ai_parting_detector.detect(shape, analysis)
if ai_result:
return self._create_parting_surface_from_ai(ai_result, analysis)
return self._create_parting_surface_from_ai(ai_result, analysis, shape)
except Exception as e:
logger.warning(f"AI 分型面检测失败,回退到几何方法:{e}")
# 2. 基于法向量分析的几何方法
try:
logger.info("使用法向量分析检测分型面")
optimal_direction = self._analyze_face_normals(shape)
@@ -282,7 +201,6 @@ class MoldCavityGenerator:
)
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
# 计算真实分型线(产品与分型面的交线)
parting_line = self._calculate_parting_line(shape, parting_surface)
return parting_surface, parting_line
@@ -290,164 +208,134 @@ class MoldCavityGenerator:
except Exception as e:
logger.warning(f"法向量分析失败,使用简化方法:{e}")
# 3. 简化方法(回退)
logger.info("使用简化方法检测分型面")
return self._simple_parting_surface(analysis)
return self._simple_parting_surface(shape, analysis)
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
"""应用收缩率补偿(放大模型)"""
scale_factor = 1.0 + self.shrinkage_rate
# 创建缩放变换
trsf = gp_Trsf()
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
return scaled_shape
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""添加拔模角(简化实现)"""
# 实际实现需要复杂的拔模面处理
# 这里返回原始形状(假设已在CAD中处理)
logger.warning("拔模角处理为简化实现,建议在设计阶段处理")
return shape
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
"""分离型腔和型芯
型腔(Cavity): 模具中形成产品外表面的部分,是产品形状的负形
型芯(Core): 模具中形成产品内表面的部分,是产品形状的正形
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
"""
try:
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_SOLID
# 获取产品边界框
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 计算模具块尺寸(比产品大一定余量)
margin = 20 # mm
mold_xmin = xmin - margin
mold_ymin = ymin - margin
mold_zmin = zmin - margin
mold_xmax = xmax + margin
mold_ymax = ymax + margin
mold_zmax = zmax + margin
# 创建模具块
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape()
# 型腔 = 模具块 - 产品(布尔减法)
cavity_operation = BRepAlgoAPI_Cut(mold_block, shape)
if cavity_operation.IsDone():
cavity = cavity_operation.Shape()
logger.info("型腔生成成功(模具块减去产品)")
else:
logger.warning("型腔布尔运算失败,使用原始形状")
cavity = mold_block
# 型芯 = 产品形状本身(收缩补偿后)
core = shape
logger.info("型芯 = 产品形状")
return cavity, core
except Exception as e:
logger.error(f"型腔分离失败: {e}")
return shape, shape
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
"""提取形状几何数据为JSON格式"""
try:
# 网格化
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
mesh.Perform()
# 提取顶点和面
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.BRep import BRep_Tool
from OCC.Core.Poly import Poly_Triangulation
from OCC.Core.TopLoc import TopLoc_Location
vertices = []
faces = []
分析产品表面的法向量分布,找出最优分型方向
原理:
- 统计所有面的法向量
- 选择法向量变化最小的方向作为分型方向
- 避免倒扣(undercut)区域
"""
face_normals = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
vertex_index = 0
while explorer.More():
# 使用 explorer.Current() 直接获取面
face = explorer.Current()
location = TopLoc_Location()
triangulation = BRep_Tool.Triangulation(face, location)
face = TopoDS_Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
if triangulation:
# 提取顶点
nb_nodes = triangulation.NbNodes()
for i in range(1, nb_nodes + 1):
node = triangulation.Node(i)
# 应用位置变换
transformed = node.Transformed(location.Transformation())
vertices.extend([
float(transformed.X()),
float(transformed.Y()),
float(transformed.Z())
])
try:
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
else:
bbox = Bnd_Box()
brepbndlib_Add(face, bbox)
normal = gp_Dir(0, 0, 1)
# 提取三角形面
nb_triangles = triangulation.NbTriangles()
for i in range(1, nb_triangles + 1):
triangle = triangulation.Triangle(i)
# 三角形顶点索引需要加上之前的顶点数量
idx1 = triangle.Value(1) + vertex_index - 1
idx2 = triangle.Value(2) + vertex_index - 1
idx3 = triangle.Value(3) + vertex_index - 1
faces.extend([int(idx1), int(idx2), int(idx3)])
vertex_index += nb_nodes
face_normals.append(normal)
except Exception as e:
logger.debug(f"面法向量计算失败:{e}")
explorer.Next()
vertex_count = len(vertices) // 3
face_count = len(faces) // 3
if not face_normals:
return gp_Dir(0, 0, 1)
return {
"type": shape_type,
"vertices": vertices,
"faces": faces,
"vertex_count": vertex_count,
"face_count": face_count,
"triangulation": "BRepMesh三角化"
}
avg_x = sum(n.X() for n in face_normals) / len(face_normals)
avg_y = sum(n.Y() for n in face_normals) / len(face_normals)
avg_z = sum(n.Z() for n in face_normals) / len(face_normals)
except Exception as e:
logger.error(f"{shape_type}几何提取失败: {e}")
return {
"type": shape_type,
"vertices": [],
"faces": [],
"vertex_count": 0,
"face_count": 0,
"triangulation": f"提取失败: {str(e)}"
}
length = np.sqrt(avg_x**2 + avg_y**2 + avg_z**2)
if length > 0.001:
return gp_Dir(avg_x/length, avg_y/length, avg_z/length)
else:
return gp_Dir(0, 0, 1)
def _create_optimal_parting_plane(self, shape: Any, analysis: Dict,
direction: gp_Dir) -> gp_Pln:
"""
创建最优分型面
Args:
shape: 产品形状
analysis: 几何分析结果
direction: 分型方向(法向量)
Returns:
gp_Pln: 分型面方程
"""
bbox = analysis["bounding_box"]
center = bbox["center"]
parting_plane = gp_Pln(
gp_Pnt(center[0], center[1], center[2]),
direction
)
logger.info(f"创建分型面:原点=({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f}), "
f"法向量=({direction.X():.3f}, {direction.Y():.3f}, {direction.Z():.3f})")
return parting_plane
def _simple_parting_surface(self, shape: Any, analysis: Dict) -> Tuple[Any, List]:
"""简化的分型面检测(回退方案)"""
bbox = analysis["bounding_box"]
center_z = bbox["center"][2]
parting_plane = gp_Pln(
gp_Pnt(0, 0, center_z),
gp_Dir(0, 0, 1)
)
parting_surface = BRepBuilderAPI_MakeFace(
parting_plane,
bbox["min"][0] - 10, bbox["max"][0] + 10,
bbox["min"][1] - 10, bbox["max"][1] + 10
).Face()
parting_line = self._simple_parting_line(shape)
return parting_surface, parting_line
def _create_parting_surface_from_ai(self, ai_result: Dict,
analysis: Dict, shape: Any = None) -> Tuple[Any, List]:
"""
从 AI 模型结果创建分型面(预留接口)
Args:
ai_result: AI 模型输出,应包含:
- origin: [x, y, z] 平面原点
- normal: [nx, ny, nz] 法向量
analysis: 几何分析结果
shape: 产品形状(用于计算分型线)
Returns:
(parting_surface, parting_line)
"""
origin = ai_result.get("origin", [0, 0, 0])
normal = ai_result.get("normal", [0, 0, 1])
parting_plane = gp_Pln(
gp_Pnt(origin[0], origin[1], origin[2]),
gp_Dir(normal[0], normal[1], normal[2])
)
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
if "parting_line" in ai_result:
parting_line = ai_result["parting_line"]
elif shape is not None:
parting_line = self._calculate_parting_line(shape, parting_surface)
else:
parting_line = []
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
return parting_surface, parting_line
def _extract_parting_surface_geometry(self, surface: Any) -> Dict[str, Any]:
"""提取分型面几何数据"""
# 尝试从surface获取边界信息,失败则使用默认值
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
adaptor = BRepAdaptor_Surface(surface)
u_min, u_max = adaptor.FirstUParameter(), adaptor.LastUParameter()
v_min, v_max = adaptor.FirstVParameter(), adaptor.LastVParameter()
@@ -463,14 +351,6 @@ class MoldCavityGenerator:
"v_range": [-200, 200]
}
# 分型面是水平面,法向量为 [0, 0, 1],原点在 Z 轴中心
return {
"type": "plane",
"normal": [0, 0, 1],
"origin": [0, 0, 0],
"bounds": bounds
}
return {
"type": "plane",
"normal": [0, 0, 1],
@@ -481,23 +361,19 @@ class MoldCavityGenerator:
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
"""估算模具尺寸"""
product_bbox = analysis["bounding_box"]["dimensions"]
# 模具通常比产品大20-50mm
margin = 30 # mm
margin = 30
return {
"length": product_bbox[0] + 2 * margin,
"width": product_bbox[1] + 2 * margin,
"height": product_bbox[2] + 2 * margin + 100, # 增加100mm用于模架
"height": product_bbox[2] + 2 * margin + 100,
"margin": margin
}
def _calculate_clamping_force(self, analysis: Dict) -> str:
"""估算锁模力"""
volume_cm3 = analysis.get("volume", 0) / 1000 # mm³ → cm³
volume_cm3 = analysis.get("volume", 0) / 1000
# 经验公式: 锁模力 ≈ 投影面积 × 压力 × 安全系数
# 简化估算
if volume_cm3 < 10:
return "50-100 吨"
elif volume_cm3 < 100:
@@ -509,18 +385,8 @@ class MoldCavityGenerator:
def _get_recommended_material(self) -> str:
"""推荐模具材料"""
# 根据产品产量推荐模具材料
# 小批量 (<5000件): 铝合金
# 中批量 (5000-50000件): P20钢
# 大批量 (>50000件): H13钢
return "Aluminum Alloy 7075 (铝合金模具)"
def _calculate_product_weight(self, analysis: Dict) -> str:
"""计算产品重量(使用当前材料密度)"""
volume_cm3 = analysis.get("volume", 0) / 1000
weight_g = volume_cm3 * self.material_density
return f"{weight_g:.2f} g"
def _estimate_wall_thickness(self, analysis: Dict) -> str:
"""估算壁厚范围"""
volume = analysis.get("volume", 0)
@@ -530,7 +396,6 @@ class MoldCavityGenerator:
avg_thickness = (volume / surface_area) * 0.6
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
elif volume > 0:
# 如果没有surface_area,基于体积估算
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
if bbox_volume > 0:
@@ -542,7 +407,6 @@ class MoldCavityGenerator:
def _calculate_complexity_score(self, analysis: Dict) -> float:
"""计算复杂度评分(0-10)"""
# 基于体积、表面积比、边界框等
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
@@ -551,7 +415,6 @@ class MoldCavityGenerator:
complexity = min(thickness_ratio / 5.0, 10.0)
return round(complexity, 1)
elif volume > 0:
# 如果没有surface_area,基于拓扑复杂度评分
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
if bbox_volume > 0:
@@ -576,7 +439,6 @@ class MoldCavityGenerator:
def _identify_weld_line_risk(self, analysis: Dict) -> str:
"""识别熔接痕风险"""
# 基于几何复杂度判断
complexity = self._calculate_complexity_score(analysis)
if complexity > 7:
@@ -588,299 +450,4 @@ class MoldCavityGenerator:
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
"""识别缩痕风险"""
thickness = self._estimate_wall_thickness(analysis)
# 简化的风险评估
return "中 - 建议壁厚均匀性检查"
def _assess_warpage_risk(self, analysis: Dict) -> str:
"""评估翘曲风险"""
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
aspect_ratio = max(bbox) / min(bbox)
if aspect_ratio > 5:
return "高 - 建议增加加强筋"
elif aspect_ratio > 3:
return "中 - 需优化冷却"
else:
return "低"
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
"""获取惯性矩阵"""
inertia = props.MatrixOfInertia()
return [
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
]
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
"""
分析产品表面的法向量分布,找出最优分型方向
原理:
- 统计所有面的法向量
- 选择法向量变化最小的方向作为分型方向
- 避免倒扣(undercut)区域
"""
from OCC.Core.TopoDS import TopoDS_Compound
from OCC.Core.TopTools import TopTools_IndexedMapOfShape
# 收集所有面的法向量
face_normals = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = TopoDS_Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
# 获取面的法向量(在参数中心点)
try:
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
normal = gp_Dir()
# 从曲面获取法向量
if surface.GetType() == 0: # Plane
normal = surface.Plane().Position().Direction()
else:
# 对于非平面,使用微分几何计算法向量
from OCC.Core.GCPnts import GCPnts_AbscissaPoint
from OCC.Core.BRepGProp import brepgprop_VolumeProperties
# 简化:使用面的边界框中心法向量
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
bbox = Bnd_Box()
brepbndlib_Add(face, bbox)
center = bbox.Center()
# 估算面法向量(简化)
normal = gp_Dir(0, 0, 1) # 默认 Z 方向
face_normals.append(normal)
except Exception as e:
logger.debug(f"面法向量计算失败:{e}")
explorer.Next()
# 如果没有法向量,返回默认 Z 方向
if not face_normals:
return gp_Dir(0, 0, 1)
# 统计法向量分布,选择最优方向
# 简化实现:计算平均法向量
avg_x = sum(n.X() for n in face_normals) / len(face_normals)
avg_y = sum(n.Y() for n in face_normals) / len(face_normals)
avg_z = sum(n.Z() for n in face_normals) / len(face_normals)
# 归一化
length = np.sqrt(avg_x**2 + avg_y**2 + avg_z**2)
if length > 0.001:
return gp_Dir(avg_x/length, avg_y/length, avg_z/length)
else:
return gp_Dir(0, 0, 1)
def _create_optimal_parting_plane(self, shape: Any, analysis: Dict,
direction: gp_Dir) -> gp_Pln:
"""
创建最优分型面
Args:
shape: 产品形状
analysis: 几何分析结果
direction: 分型方向(法向量)
Returns:
gp_Pln: 分型面方程
"""
bbox = analysis["bounding_box"]
# 分型面通过产品的质心
center = bbox["center"]
# 创建平面:通过质心,法向量为分型方向
parting_plane = gp_Pln(
gp_Pnt(center[0], center[1], center[2]),
direction
)
logger.info(f"创建分型面:原点=({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f}), "
f"法向量=({direction.X():.3f}, {direction.Y():.3f}, {direction.Z():.3f})")
return parting_plane
def _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]:
"""
计算真实的分型线(产品与分型面的交线)
使用 BRepAlgoAPI_Section 进行布尔运算求交
"""
try:
# 创建截面运算
section = BRepAlgoAPI_Section(shape, parting_surface)
section.Build()
if not section.IsDone():
logger.warning("截面运算未完成,使用简化分型线")
return self._simple_parting_line(
parting_surface,
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
)
# 提取交线(边)
edges = []
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
while explorer.More():
edge = TopoDS_Edge(explorer.Current())
# 从边提取点
curve = BRepAdaptor_Curve(edge)
first_param = curve.FirstParameter()
last_param = curve.LastParameter()
# 采样点(至少 10 个点)
num_points = max(10, int((last_param - first_param) / 0.5))
step = (last_param - first_param) / num_points
for i in range(num_points + 1):
param = first_param + i * step
point = curve.Value(param)
edges.append([point.X(), point.Y(), point.Z()])
explorer.Next()
# 如果没有边,使用简化分型线
if not edges:
logger.warning("未找到交线,使用简化分型线")
return self._simple_parting_line(
parting_surface,
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
)
logger.info(f"计算得到 {len(edges)} 个分型线点")
return edges
except Exception as e:
logger.error(f"分型线计算失败:{e}")
return self._simple_parting_line(
parting_surface,
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
)
def _simple_parting_surface(self, analysis: Dict) -> Tuple[Any, List]:
"""简化的分型面检测(回退方案)"""
bbox = analysis["bounding_box"]
center_z = bbox["center"][2]
# 创建分型面(XY 平面)
parting_plane = gp_Pln(
gp_Pnt(0, 0, center_z),
gp_Dir(0, 0, 1)
)
parting_surface = BRepBuilderAPI_MakeFace(
parting_plane,
bbox["min"][0] - 10, bbox["max"][0] + 10,
bbox["min"][1] - 10, bbox["max"][1] + 10
).Face()
# 简化分型线
parting_line = self._simple_parting_line(parting_surface, analysis)
return parting_surface, parting_line
def _simple_parting_line(self, parting_surface: Any, analysis: Dict) -> List[List[float]]:
"""简化的分型线(矩形)"""
bbox = analysis["bounding_box"]
center_z = bbox["center"][2]
return [
[bbox["min"][0], bbox["min"][1], center_z],
[bbox["max"][0], bbox["min"][1], center_z],
[bbox["max"][0], bbox["max"][1], center_z],
[bbox["min"][0], bbox["max"][1], center_z],
[bbox["min"][0], bbox["min"][1], center_z]
]
def _create_parting_surface_from_ai(self, ai_result: Dict,
analysis: Dict) -> Tuple[Any, List]:
"""
从 AI 模型结果创建分型面(预留接口)
Args:
ai_result: AI 模型输出,应包含:
- origin: [x, y, z] 平面原点
- normal: [nx, ny, nz] 法向量
analysis: 几何分析结果
Returns:
(parting_surface, parting_line)
"""
origin = ai_result.get("origin", [0, 0, 0])
normal = ai_result.get("normal", [0, 0, 1])
# 创建平面
parting_plane = gp_Pln(
gp_Pnt(origin[0], origin[1], origin[2]),
gp_Dir(normal[0], normal[1], normal[2])
)
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
# 分型线可以使用 AI 结果或重新计算
if "parting_line" in ai_result:
parting_line = ai_result["parting_line"]
else:
parting_line = self._simple_parting_line(parting_surface, analysis)
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
return parting_surface, parting_line
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""
添加拔模角
使用 OpenCASCADE 的拔模功能
"""
# 1. 尝试使用 AI 模型
if self.ai_draft_analyzer is not None:
try:
logger.info("使用 AI 模型分析拔模角")
ai_result = self.ai_draft_analyzer.analyze(shape, parting_surface, self.draft_angle)
if ai_result and "drafted_shape" in ai_result:
logger.info("AI 拔模分析成功")
return ai_result["drafted_shape"]
except Exception as e:
logger.warning(f"AI 拔模分析失败,回退到几何方法:{e}")
# 2. 几何方法(简化实现)
try:
# 获取分型面的法向量作为拔模方向
surface_adaptor = BRepAdaptor_Surface(parting_surface)
draft_direction = surface_adaptor.Plane().Position().Direction()
# 使用 BRepOffsetAPI_ThickSolid 创建拔模
# 注意:完整的拔模需要更复杂的实现,这里简化处理
logger.info(f"使用几何方法添加拔模角:{self.draft_angle}度,方向=({draft_direction.X():.3f}, {draft_direction.Y():.3f}, {draft_direction.Z():.3f})")
# 简化:直接返回原始形状(拔模已在 CAD 中处理)
# 完整实现需要使用 BRepOffsetAPI_DraftAngle
return shape
except Exception as e:
logger.warning(f"拔模角处理失败:{e}")
return shape
def _calculate_parting_line_length(self, parting_line: List) -> float:
"""计算分型线长度"""
if not parting_line or len(parting_line) < 2:
return 0.0
# 计算折线总长度
total_length = 0.0
for i in range(1, len(parting_line)):
p1 = np.array(parting_line[i-1])
p2 = np.array(parting_line[i])
segment_length = np.linalg.norm(p2 - p1)
total_length += segment_length
return total_length
+762
View File
@@ -0,0 +1,762 @@
"""
模具加工碰撞检测与刀路优化模块
功能:
1. CollisionDetector - 碰撞检测器
- 刀柄干涉检测
- 快速移动碰撞检测
- 机床行程限制验证
- 安全区域计算
2. ToolpathOptimizer - 刀路优化器
- 进给率自适应优化
- 空走刀路径最小化
- 拐角减速处理
- 切入切出优化
3. EDMElectrodeDesigner - EDM电极设计器
- 电极自动生成
- 放电间隙计算
- 电极加工路径
4. MachiningSimulator - 加工仿真器
- 材料去除模拟
- 过切检测
- 残余材料分析
- 加工质量评估
"""
from typing import Dict, List, Any, Optional, Tuple
import math
import numpy as np
from utils.logger import get_logger
logger = get_logger(__name__)
class CollisionDetector:
"""碰撞检测器"""
def __init__(self):
self.machine_limits = {
"x_min": -500, "x_max": 500,
"y_min": -400, "y_max": 400,
"z_min": -300, "z_max": 300,
}
self.safety_margin = 5.0
self.retract_height = 50.0
def check_toolpath_safety(self, toolpath_points: List[List[float]],
tool: Dict, stock_bbox: Dict,
clamp_positions: Optional[List[Dict]] = None) -> Dict[str, Any]:
"""
综合检查刀路安全性
Args:
toolpath_points: 刀路点列表 [[x,y,z], ...]
tool: 刀具参数
stock_bbox: 毛坯边界框
clamp_positions: 压板位置列表
Returns:
安全检查结果
"""
holder_collisions = self._check_holder_collision(toolpath_points, tool, stock_bbox)
rapid_collisions = self._check_rapid_move_collisions(toolpath_points, stock_bbox)
limit_violations = self._check_machine_limits(toolpath_points)
clamp_collisions = []
if clamp_positions:
clamp_collisions = self._check_clamp_collisions(
toolpath_points, tool, clamp_positions
)
all_issues = holder_collisions + rapid_collisions + limit_violations + clamp_collisions
safe_retract_points = self._calculate_safe_retract_points(
toolpath_points, stock_bbox
)
is_safe = len(all_issues) == 0
return {
"is_safe": is_safe,
"total_issues": len(all_issues),
"holder_collisions": holder_collisions,
"rapid_collisions": rapid_collisions,
"limit_violations": limit_violations,
"clamp_collisions": clamp_collisions,
"safe_retract_points": safe_retract_points,
"recommendations": self._generate_safety_recommendations(all_issues),
}
def _check_holder_collision(self, points: List[List[float]],
tool: Dict, stock_bbox: Dict) -> List[Dict]:
"""检测刀柄干涉"""
collisions = []
tool_diameter = tool.get("diameter", 10)
flute_length = tool.get("flute_length", 30)
shank_diameter = tool.get("shank_diameter", tool_diameter)
holder_diameter = tool.get("holder_diameter", shank_diameter * 2)
stock_z_max = stock_bbox.get("max", [0, 0, 0])[2]
for i, pt in enumerate(points):
if len(pt) < 3:
continue
z = pt[2]
depth_below_stock = stock_z_max - z
if depth_below_stock > flute_length:
holder_z = z + flute_length
holder_clearance = holder_diameter / 2 + self.safety_margin
stock_xmin = stock_bbox.get("min", [0, 0, 0])[0]
stock_xmax = stock_bbox.get("max", [0, 0, 0])[0]
stock_ymin = stock_bbox.get("min", [0, 0, 0])[1]
stock_ymax = stock_bbox.get("max", [0, 0, 0])[1]
if (stock_xmin - holder_clearance < pt[0] < stock_xmax + holder_clearance and
stock_ymin - holder_clearance < pt[1] < stock_ymax + holder_clearance):
collisions.append({
"type": "holder_collision",
"point_index": i,
"position": pt,
"depth": round(depth_below_stock, 2),
"flute_length": flute_length,
"severity": "high",
"message": f"点{i}: 切深{depth_below_stock:.1f}mm超过刃长{flute_length}mm,刀柄可能干涉"
})
return collisions
def _check_rapid_move_collisions(self, points: List[List[float]],
stock_bbox: Dict) -> List[Dict]:
"""检测快速移动碰撞"""
collisions = []
stock_xmin = stock_bbox.get("min", [0, 0, 0])[0]
stock_xmax = stock_bbox.get("max", [0, 0, 0])[0]
stock_ymin = stock_bbox.get("min", [0, 0, 0])[1]
stock_ymax = stock_bbox.get("max", [0, 0, 0])[1]
stock_zmin = stock_bbox.get("min", [0, 0, 0])[2]
stock_zmax = stock_bbox.get("max", [0, 0, 0])[2]
for i in range(1, len(points)):
prev = points[i - 1]
curr = points[i]
if len(prev) < 3 or len(curr) < 3:
continue
z_change = abs(curr[2] - prev[2])
xy_change = math.sqrt((curr[0] - prev[0])**2 + (curr[1] - prev[1])**2)
if z_change < 1.0 and xy_change > 5.0:
min_z = min(prev[2], curr[2])
if min_z < stock_zmax + self.safety_margin:
mid_x = (prev[0] + curr[0]) / 2
mid_y = (prev[1] + curr[1]) / 2
if (stock_xmin < mid_x < stock_xmax and
stock_ymin < mid_y < stock_ymax):
collisions.append({
"type": "rapid_collision",
"segment": [i - 1, i],
"start": prev,
"end": curr,
"severity": "high",
"message": f"段{i-1}-{i}: 水平快速移动可能穿过毛坯"
})
return collisions
def _check_machine_limits(self, points: List[List[float]]) -> List[Dict]:
"""验证机床行程限制"""
violations = []
for i, pt in enumerate(points):
if len(pt) < 3:
continue
if not (self.machine_limits["x_min"] <= pt[0] <= self.machine_limits["x_max"]):
violations.append({
"type": "machine_limit",
"point_index": i,
"axis": "X",
"value": pt[0],
"limit": [self.machine_limits["x_min"], self.machine_limits["x_max"]],
"severity": "critical",
})
if not (self.machine_limits["y_min"] <= pt[1] <= self.machine_limits["y_max"]):
violations.append({
"type": "machine_limit",
"point_index": i,
"axis": "Y",
"value": pt[1],
"limit": [self.machine_limits["y_min"], self.machine_limits["y_max"]],
"severity": "critical",
})
if not (self.machine_limits["z_min"] <= pt[2] <= self.machine_limits["z_max"]):
violations.append({
"type": "machine_limit",
"point_index": i,
"axis": "Z",
"value": pt[2],
"limit": [self.machine_limits["z_min"], self.machine_limits["z_max"]],
"severity": "critical",
})
return violations
def _check_clamp_collisions(self, points: List[List[float]], tool: Dict,
clamps: List[Dict]) -> List[Dict]:
"""检测压板碰撞"""
collisions = []
tool_radius = tool.get("diameter", 10) / 2
for i, pt in enumerate(points):
if len(pt) < 3:
continue
for j, clamp in enumerate(clamps):
clamp_center = clamp.get("center", [0, 0, 0])
clamp_size = clamp.get("size", [50, 30, 20])
clamp_z_top = clamp_center[2] + clamp_size[2] / 2
if pt[2] < clamp_z_top + self.safety_margin:
dx = abs(pt[0] - clamp_center[0])
dy = abs(pt[1] - clamp_center[1])
if (dx < clamp_size[0] / 2 + tool_radius + self.safety_margin and
dy < clamp_size[1] / 2 + tool_radius + self.safety_margin):
collisions.append({
"type": "clamp_collision",
"point_index": i,
"clamp_index": j,
"severity": "high",
"message": f"点{i}: 可能与压板{j}碰撞"
})
return collisions
def _calculate_safe_retract_points(self, points: List[List[float]],
stock_bbox: Dict) -> List[Dict]:
"""计算安全抬刀点"""
retract_points = []
stock_zmax = stock_bbox.get("max", [0, 0, 0])[2]
safe_z = stock_zmax + self.retract_height
for i in range(0, len(points), max(1, len(points) // 10)):
pt = points[i]
if len(pt) >= 3:
retract_points.append({
"index": i,
"from": pt,
"retract_to": [pt[0], pt[1], safe_z],
"safe_z": safe_z,
})
return retract_points
def _generate_safety_recommendations(self, issues: List[Dict]) -> List[str]:
"""生成安全建议"""
recs = []
holder_issues = [i for i in issues if i["type"] == "holder_collision"]
if holder_issues:
recs.append(f"发现 {len(holder_issues)} 处刀柄干涉,建议加长刀具或减少切深")
rapid_issues = [i for i in issues if i["type"] == "rapid_collision"]
if rapid_issues:
recs.append(f"发现 {len(rapid_issues)} 处快速移动碰撞风险,建议增加抬刀高度")
limit_issues = [i for i in issues if i["type"] == "machine_limit"]
if limit_issues:
recs.append(f"发现 {len(limit_issues)} 处超出机床行程,需调整工件位置")
clamp_issues = [i for i in issues if i["type"] == "clamp_collision"]
if clamp_issues:
recs.append(f"发现 {len(clamp_issues)} 处压板碰撞,建议调整压板位置")
if not issues:
recs.append("刀路安全检查通过,无碰撞风险")
return recs
class ToolpathOptimizer:
"""刀路优化器"""
def optimize_toolpath(self, toolpath_points: List[List[float]],
cutting_params: Dict,
stock_bbox: Optional[Dict] = None) -> Dict[str, Any]:
"""
综合优化刀路
优化内容:
1. 进给率自适应优化
2. 拐角减速处理
3. 空走刀路径优化
4. 切入切出优化
Args:
toolpath_points: 原始刀路点
cutting_params: 切削参数
stock_bbox: 毛坯边界框
Returns:
优化后的刀路和参数
"""
feed_optimized = self._optimize_feed_rates(toolpath_points, cutting_params)
corner_optimized = self._optimize_corner_speeds(toolpath_points, feed_optimized)
entry_exit_optimized = self._optimize_entry_exit(toolpath_points, stock_bbox)
stats = self._calculate_optimization_stats(
toolpath_points, feed_optimized, corner_optimized
)
return {
"original_point_count": len(toolpath_points),
"optimized_feeds": feed_optimized,
"corner_slowdowns": corner_optimized,
"entry_exit": entry_exit_optimized,
"stats": stats,
"recommendations": self._generate_optimization_recommendations(stats),
}
def _optimize_feed_rates(self, points: List[List[float]],
params: Dict) -> List[Dict]:
"""进给率自适应优化"""
base_feed = params.get("feed_rate_mm_min", 500)
optimized = []
for i in range(len(points)):
if i < 2 or i >= len(points) - 2:
feed = base_feed * 0.8
else:
v1 = np.array(points[i]) - np.array(points[i - 1])
v2 = np.array(points[i + 1]) - np.array(points[i])
len1 = np.linalg.norm(v1)
len2 = np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
angle = math.degrees(math.acos(cos_angle))
if angle < 30:
feed = base_feed * 0.3
elif angle < 60:
feed = base_feed * 0.5
elif angle < 120:
feed = base_feed * 0.7
else:
feed = base_feed
else:
feed = base_feed
optimized.append({
"index": i,
"feed_rate": round(feed, 1),
"feed_ratio": round(feed / base_feed, 2),
})
return optimized
def _optimize_corner_speeds(self, points: List[List[float]],
feed_data: List[Dict]) -> List[Dict]:
"""拐角减速处理"""
slowdowns = []
base_feed = 500
for i in range(1, len(points) - 1):
if i >= len(feed_data):
break
v1 = np.array(points[i]) - np.array(points[i - 1])
v2 = np.array(points[i + 1]) - np.array(points[i])
len1 = np.linalg.norm(v1)
len2 = np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
angle = math.degrees(math.acos(cos_angle))
if angle < 90:
decel_distance = max(2.0, 10.0 * (1 - angle / 90))
slowdowns.append({
"index": i,
"angle": round(angle, 1),
"decel_distance": round(decel_distance, 2),
"min_feed_ratio": 0.3 if angle < 45 else 0.5,
})
return slowdowns
def _optimize_entry_exit(self, points: List[List[float]],
stock_bbox: Optional[Dict]) -> Dict[str, Any]:
"""切入切出优化"""
entry = {"type": "arc_tangent", "radius": 5.0, "angle": 90}
exit_ = {"type": "arc_tangent", "radius": 5.0, "angle": 90}
if stock_bbox:
z_max = stock_bbox.get("max", [0, 0, 0])[2]
entry["approach_z"] = z_max + 10
exit_["retract_z"] = z_max + 50
return {"entry": entry, "exit": exit_}
def _calculate_optimization_stats(self, points: List, feeds: List,
corners: List) -> Dict:
"""计算优化统计"""
if not feeds:
return {"time_reduction_percent": 0}
feed_values = [f["feed_rate"] for f in feeds]
avg_feed = sum(feed_values) / len(feed_values) if feed_values else 500
base_feed = max(feed_values) if feed_values else 500
time_reduction = 0
if base_feed > 0:
time_reduction = (1 - avg_feed / base_feed) * 100
return {
"avg_feed_rate": round(avg_feed, 1),
"base_feed_rate": base_feed,
"corner_slowdown_count": len(corners),
"time_reduction_percent": round(abs(time_reduction), 1),
}
def _generate_optimization_recommendations(self, stats: Dict) -> List[str]:
"""生成优化建议"""
recs = []
if stats.get("corner_slowdown_count", 0) > 10:
recs.append("拐角减速点较多,建议优化刀路方向减少急转弯")
if stats.get("time_reduction_percent", 0) > 30:
recs.append("进给率降低幅度较大,建议优化加工策略")
if not recs:
recs.append("刀路优化完成,进给率分布合理")
return recs
class EDMElectrodeDesigner:
"""EDM电极设计器"""
ELECTRODE_MATERIALS = {
"copper": {
"density": 8.96, "wear_rate": 1.0,
"machinability": "good", "cost": "medium"
},
"graphite": {
"density": 1.75, "wear_rate": 0.5,
"machinability": "excellent", "cost": "low"
},
"copper_tungsten": {
"density": 14.0, "wear_rate": 0.3,
"machinability": "poor", "cost": "high"
},
}
def design_electrodes(self, undercut_regions: List[Dict],
cavity_bbox: Dict,
material: str = "copper",
spark_gap: float = 0.05,
overburn: float = 0.1) -> Dict[str, Any]:
"""
设计EDM电极
Args:
undercut_regions: 倒扣区域列表
cavity_bbox: 型腔边界框
material: 电极材料
spark_gap: 放电间隙 mm
overburn: 过切量 mm
Returns:
电极设计方案
"""
mat_props = self.ELECTRODE_MATERIALS.get(material, self.ELECTRODE_MATERIALS["copper"])
electrodes = []
for i, region in enumerate(undercut_regions):
electrode = self._design_single_electrode(
region, i + 1, material, spark_gap, overburn, cavity_bbox
)
electrodes.append(electrode)
total_volume = sum(e["volume_mm3"] for e in electrodes)
total_weight = total_volume * mat_props["density"] / 1000
return {
"electrodes": electrodes,
"material": material,
"material_properties": mat_props,
"spark_gap": spark_gap,
"overburn": overburn,
"total_electrode_count": len(electrodes),
"total_volume_cm3": round(total_volume / 1000, 2),
"total_weight_g": round(total_weight, 2),
"machining_strategy": self._generate_electrode_machining_strategy(
electrodes, material
),
"recommendations": self._generate_electrode_recommendations(
electrodes, material
),
}
def _design_single_electrode(self, region: Dict, index: int,
material: str, spark_gap: float,
overburn: float, cavity_bbox: Dict) -> Dict:
"""设计单个电极"""
center = region.get("center", [0, 0, 0])
area = region.get("area", 100)
feature_size = math.sqrt(area)
electrode_size = {
"width": round(feature_size * 1.3 + 2 * (spark_gap + overburn), 2),
"length": round(feature_size * 1.3 + 2 * (spark_gap + overburn), 2),
"height": round(cavity_bbox.get("dimensions", [0, 0, 50])[2] * 0.8 + 20, 2),
}
volume = electrode_size["width"] * electrode_size["length"] * electrode_size["height"]
return {
"index": index,
"type": region.get("type", "undercut"),
"location": center,
"size": electrode_size,
"volume_mm3": round(volume, 1),
"spark_gap": spark_gap,
"overburn": overburn,
"material": material,
"roughing_passes": 3,
"finishing_passes": 2,
}
def _generate_electrode_machining_strategy(self, electrodes: List,
material: str) -> List[Dict]:
"""生成电极加工策略"""
strategies = []
for elec in electrodes:
size = elec["size"]
is_small = min(size["width"], size["length"]) < 5
strategy = {
"electrode_index": elec["index"],
"operations": [
{
"operation": "roughing",
"tool": "endmill_6mm" if not is_small else "endmill_3mm",
"stock_allowance": 0.3,
},
{
"operation": "finishing",
"tool": "ballnose_3mm" if not is_small else "ballnose_1mm",
"stepover": 0.2,
},
],
}
strategies.append(strategy)
return strategies
def _generate_electrode_recommendations(self, electrodes: List,
material: str) -> List[str]:
"""生成电极建议"""
recs = []
if material == "copper":
recs.append("铜电极加工性良好,建议使用高速钢刀具")
elif material == "graphite":
recs.append("石墨电极易加工但易碎,注意切削力控制")
elif material == "copper_tungsten":
recs.append("铜钨合金硬度高,建议使用金刚石刀具")
if len(electrodes) > 4:
recs.append("电极数量较多,建议评估是否可合并电极设计")
recs.append("电极加工后需检测尺寸精度和表面质量")
recs.append("放电加工时需根据材料调整电参数")
return recs
class MachiningSimulator:
"""加工仿真器"""
def simulate_machining(self, operations: List[Dict],
stock_bbox: Dict,
resolution: float = 1.0) -> Dict[str, Any]:
"""
模拟加工过程
Args:
operations: 加工操作列表
stock_bbox: 毛坯边界框
resolution: 仿真精度 mm
Returns:
仿真结果
"""
stock_dims = stock_bbox.get("dimensions", [100, 100, 50])
nx = max(2, int(stock_dims[0] / resolution))
ny = max(2, int(stock_dims[1] / resolution))
nz = max(2, int(stock_dims[2] / resolution))
stock = np.ones((nx, ny, nz), dtype=np.float32)
total_removed = 0
operation_results = []
for op in operations:
removed = self._simulate_operation(stock, op, stock_bbox, resolution)
total_removed += removed
operation_results.append({
"strategy": op.get("strategy", "unknown"),
"volume_removed_mm3": removed,
"remaining_stock_percent": round(
(1 - total_removed / (nx * ny * nz)) * 100, 1
),
})
total_voxels = nx * ny * nz
remaining = np.sum(stock > 0)
removal_efficiency = (1 - remaining / total_voxels) * 100 if total_voxels > 0 else 0
gouging = self._detect_gouging(stock, operations, stock_bbox, resolution)
residual = self._analyze_residual_material(stock, stock_bbox, resolution)
return {
"resolution": resolution,
"grid_size": {"nx": nx, "ny": ny, "nz": nz},
"operations": operation_results,
"total_volume_removed_percent": round(removal_efficiency, 1),
"gouging_detected": gouging,
"residual_analysis": residual,
"quality_assessment": self._assess_quality(gouging, residual),
"recommendations": self._generate_simulation_recommendations(
gouging, residual, removal_efficiency
),
}
def _simulate_operation(self, stock: np.ndarray, op: Dict,
bbox: Dict, resolution: float) -> int:
"""模拟单个加工操作的材料去除"""
strategy = op.get("strategy", "")
removed = 0
nx, ny, nz = stock.shape
if strategy == "z_level_roughing":
levels = op.get("levels", [])
for level in levels:
z_level = level.get("z", 0)
z_idx = int((z_level - bbox.get("min", [0, 0, 0])[2]) / resolution)
z_idx = max(0, min(z_idx, nz - 1))
for iz in range(z_idx, nz):
removed += int(np.sum(stock[:, :, iz] > 0))
stock[:, :, iz] = 0
elif strategy in ("parallel_finishing", "contour_finishing"):
stepover = op.get("stepover", 0.3)
step_idx = max(1, int(stepover / resolution))
for ix in range(0, nx, step_idx):
for iy in range(0, ny, step_idx):
if stock[ix, iy, :].any():
removed += int(np.sum(stock[ix, iy, :] > 0))
stock[ix, iy, :] = 0
return removed
def _detect_gouging(self, stock: np.ndarray, operations: List,
bbox: Dict, resolution: float) -> List[Dict]:
"""检测过切"""
gouging = []
for op in operations:
stock_allowance = op.get("stock_allowance", 0)
if stock_allowance < 0:
gouging.append({
"operation": op.get("strategy", "unknown"),
"type": "negative_allowance",
"severity": "high",
"message": f"工序 {op.get('strategy')} 余量为负值,存在过切风险"
})
return gouging
def _analyze_residual_material(self, stock: np.ndarray,
bbox: Dict, resolution: float) -> Dict:
"""分析残余材料"""
total_voxels = stock.size
remaining = int(np.sum(stock > 0))
remaining_percent = (remaining / total_voxels) * 100 if total_voxels > 0 else 0
return {
"remaining_voxels": remaining,
"remaining_percent": round(remaining_percent, 2),
"estimated_residual_volume_cm3": round(
remaining * resolution ** 3 / 1000, 2
),
}
def _assess_quality(self, gouging: List, residual: Dict) -> Dict:
"""评估加工质量"""
has_gouging = len(gouging) > 0
residual_pct = residual.get("remaining_percent", 100)
if has_gouging:
grade = "FAIL"
elif residual_pct < 5:
grade = "GOOD"
elif residual_pct < 15:
grade = "ACCEPTABLE"
else:
grade = "INSUFFICIENT"
return {
"grade": grade,
"has_gouging": has_gouging,
"residual_percent": residual_pct,
}
def _generate_simulation_recommendations(self, gouging: List, residual: Dict,
efficiency: float) -> List[str]:
"""生成仿真建议"""
recs = []
if gouging:
recs.append("检测到过切,需调整加工参数")
residual_pct = residual.get("remaining_percent", 0)
if residual_pct > 20:
recs.append("残余材料较多,建议增加精加工工序")
elif residual_pct > 5:
recs.append("残余材料适中,需检查关键区域是否加工到位")
if efficiency < 50:
recs.append("材料去除率偏低,建议优化粗加工策略")
if not recs:
recs.append("仿真结果良好,加工方案可行")
return recs
+1 -9
View File
@@ -4,16 +4,8 @@
提供分模面质量检测、模具结构合理性评估、生产可行性分析等功能
"""
from typing import Dict, List, Any, Tuple
from typing import Dict, List, Any
import numpy as np
from OCC.Core.BRep import BRep_Tool
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
from OCC.Core.gp import gp_Dir
from utils.logger import get_logger
+588
View File
@@ -0,0 +1,588 @@
"""
冷却/浇注系统自动设计模块
功能:
1. 冷却系统设计 - 水路布局、直径、间距
2. 浇注系统设计 - 主流道、分流道、浇口
3. 热力学估算 - 冷却时间、温度分布
4. 排气系统设计 - 排气槽、排气针位置
设计依据:
- 模具尺寸和产品几何
- 材料热物性参数
- 生产节拍要求
- 行业标准规范
"""
from typing import Dict, List, Any, Optional
import math
from utils.logger import get_logger
logger = get_logger(__name__)
class MaterialThermalDB:
"""材料热物性数据库"""
PLASTICS = {
"ABS": {"density": 1.05, "specific_heat": 1.47, "thermal_cond": 0.17,
"melt_temp": 230, "mold_temp": 60, "eject_temp": 85},
"PP": {"density": 0.90, "specific_heat": 1.90, "thermal_cond": 0.15,
"melt_temp": 220, "mold_temp": 40, "eject_temp": 80},
"PC": {"density": 1.20, "specific_heat": 1.25, "thermal_cond": 0.20,
"melt_temp": 300, "mold_temp": 80, "eject_temp": 120},
"PE": {"density": 0.95, "specific_heat": 2.30, "thermal_cond": 0.50,
"melt_temp": 200, "mold_temp": 30, "eject_temp": 70},
"PS": {"density": 1.05, "specific_heat": 1.34, "thermal_cond": 0.12,
"melt_temp": 220, "mold_temp": 50, "eject_temp": 80},
"PA": {"density": 1.14, "specific_heat": 1.70, "thermal_cond": 0.25,
"melt_temp": 260, "mold_temp": 70, "eject_temp": 100},
"POM": {"density": 1.42, "specific_heat": 1.47, "thermal_cond": 0.31,
"melt_temp": 200, "mold_temp": 70, "eject_temp": 100},
"PMMA": {"density": 1.18, "specific_heat": 1.47, "thermal_cond": 0.19,
"melt_temp": 240, "mold_temp": 60, "eject_temp": 90},
}
FOAM = {
"AlSi10Mg": {"density": 0.45, "specific_heat": 0.90, "thermal_cond": 0.05,
"melt_temp": 380, "mold_temp": 150, "eject_temp": 200},
"AlSi12": {"density": 0.50, "specific_heat": 0.88, "thermal_cond": 0.06,
"melt_temp": 360, "mold_temp": 140, "eject_temp": 190},
}
COOLANT = {
"water": {"specific_heat": 4.18, "density": 1.0, "thermal_cond": 0.60},
"oil": {"specific_heat": 2.00, "density": 0.85, "thermal_cond": 0.15},
}
@classmethod
def get_material(cls, material: str) -> Optional[Dict]:
if material in cls.PLASTICS:
return cls.PLASTICS[material]
if material in cls.FOAM:
return cls.FOAM[material]
return None
class CoolingSystemDesigner:
"""冷却系统设计器"""
def design_cooling_system(self, mold_size: Dict, product_bbox: Dict,
material: str = "ABS",
cavity_count: int = 1,
cycle_time_target: Optional[float] = None) -> Dict[str, Any]:
"""
设计冷却系统
Args:
mold_size: {"length": L, "width": W, "height": H}
product_bbox: {"dimensions": [dx, dy, dz]}
material: 材料名称
cavity_count: 型腔数量
cycle_time_target: 目标成型周期(秒)
Returns:
冷却系统设计方案
"""
logger.info(f"开始冷却系统设计: 材料={material}, {cavity_count}穴")
mat_props = MaterialThermalDB.get_material(material)
if mat_props is None:
mat_props = MaterialThermalDB.PLASTICS["ABS"]
logger.warning(f"未知材料 {material},使用 ABS 默认参数")
dims = product_bbox.get("dimensions", [100, 100, 50])
max_wall = max(dims) * 0.6
cooling_time = self._estimate_cooling_time(
max_wall, mat_props, mold_size.get("height", 100)
)
layout = self._design_channel_layout(mold_size, dims, cavity_count)
channels = self._generate_channel_positions(layout, mold_size, dims)
flow_rate = self._calculate_flow_rate(channels, mat_props)
thermal_check = self._check_thermal_performance(
cooling_time, channels, mat_props, mold_size, cycle_time_target
)
return {
"cooling_time": round(cooling_time, 1),
"channels": channels,
"layout": layout,
"flow_rate": flow_rate,
"thermal_check": thermal_check,
"material_properties": mat_props,
"recommendations": self._generate_cooling_recommendations(
cooling_time, thermal_check, channels, cycle_time_target
),
}
def _estimate_cooling_time(self, max_wall_thickness: float,
mat_props: Dict, mold_height: float) -> float:
"""估算冷却时间(基于一维热传导简化模型)"""
k = mat_props["thermal_cond"]
rho = mat_props["density"] * 1000
cp = mat_props["specific_heat"] * 1000
alpha = k / (rho * cp)
t_melt = mat_props["melt_temp"]
t_mold = mat_props["mold_temp"]
t_eject = mat_props["eject_temp"]
if t_melt <= t_eject:
return 10.0
theta = (t_eject - t_mold) / (t_melt - t_mold) if (t_melt - t_mold) != 0 else 0.5
theta = max(0.01, min(0.99, abs(theta)))
L = max_wall_thickness / 1000.0
cooling_time = (L ** 2 / (alpha * math.pi ** 2)) * math.log(4 / (math.pi * theta))
return max(5.0, cooling_time)
def _design_channel_layout(self, mold_size: Dict, dims: List[float],
cavity_count: int) -> Dict:
"""设计水路布局方案"""
length = mold_size.get("length", 300)
width = mold_size.get("width", 300)
channel_diameter = 8.0
channel_spacing = 30.0
wall_distance = 15.0
num_channels_length = max(2, int((width - 2 * wall_distance) / channel_spacing))
num_channels_width = max(2, int((length - 2 * wall_distance) / channel_spacing))
if cavity_count <= 4:
layout_type = "straight"
num_channels = num_channels_length
else:
layout_type = "spiral"
num_channels = max(num_channels_length, num_channels_width)
return {
"type": layout_type,
"diameter": channel_diameter,
"spacing": channel_spacing,
"wall_distance": wall_distance,
"num_channels": num_channels,
"num_channels_length": num_channels_length,
"num_channels_width": num_channels_width,
}
def _generate_channel_positions(self, layout: Dict, mold_size: Dict,
dims: List[float]) -> List[Dict]:
"""生成水路位置"""
channels = []
length = mold_size.get("length", 300)
width = mold_size.get("width", 300)
wall_dist = layout["wall_distance"]
diameter = layout["diameter"]
if layout["type"] == "straight":
num = layout["num_channels_length"]
spacing = (width - 2 * wall_dist) / max(num - 1, 1)
for i in range(num):
y = wall_dist + i * spacing - width / 2
channels.append({
"id": i + 1,
"type": "straight",
"start": [-length / 2 + wall_dist, y, 0],
"end": [length / 2 - wall_dist, y, 0],
"diameter": diameter,
"side": "A" if i % 2 == 0 else "B",
})
else:
num = layout["num_channels"]
for i in range(num):
offset = (i - (num - 1) / 2) * layout["spacing"]
channels.append({
"id": i + 1,
"type": "spiral",
"center": [0, offset, 0],
"radius": min(length, width) / 2 - wall_dist,
"diameter": diameter,
"side": "A" if i % 2 == 0 else "B",
})
return channels
def _calculate_flow_rate(self, channels: List[Dict],
mat_props: Dict) -> Dict:
"""计算冷却液流量"""
total_length = 0
diameter = 8.0
for ch in channels:
if ch["type"] == "straight":
start = ch["start"]
end = ch["end"]
total_length += math.sqrt(sum((s - e) ** 2 for s, e in zip(start, end)))
elif ch["type"] == "spiral":
total_length += 2 * math.pi * ch.get("radius", 100)
velocity = 1.5
area = math.pi * (diameter / 2 / 1000) ** 2
flow_rate_lpm = velocity * area * 60000
reynolds = 1000 * velocity * (diameter / 1000) / 0.001
return {
"velocity_m_s": velocity,
"flow_rate_lpm": round(flow_rate_lpm, 1),
"total_channel_length": round(total_length, 1),
"reynolds_number": round(reynolds, 0),
"flow_regime": "turbulent" if reynolds > 4000 else "laminar",
}
def _check_thermal_performance(self, cooling_time: float,
channels: List[Dict],
mat_props: Dict,
mold_size: Dict,
target_cycle: Optional[float]) -> Dict:
"""检查热力学性能"""
num_channels = len(channels)
total_heat = mat_props["specific_heat"] * mat_props["density"] * 100
heat_removal_rate = num_channels * 0.5 * 4.18 * 1.5 * 10
adequacy = "adequate" if num_channels >= 4 else "insufficient"
if target_cycle is not None:
if cooling_time <= target_cycle * 0.6:
adequacy = "excellent"
elif cooling_time <= target_cycle * 0.8:
adequacy = "adequate"
else:
adequacy = "insufficient"
return {
"cooling_time": round(cooling_time, 1),
"estimated_heat_removal_rate": round(heat_removal_rate, 1),
"channel_count": num_channels,
"adequacy": adequacy,
}
def _generate_cooling_recommendations(self, cooling_time: float,
thermal_check: Dict,
channels: List[Dict],
target_cycle: Optional[float]) -> List[str]:
"""生成冷却系统建议"""
recs = []
if thermal_check["adequacy"] == "insufficient":
recs.append("冷却能力不足,建议增加水路数量或增大水路直径")
recs.append("考虑使用铍铜镶件提高局部冷却效率")
if cooling_time > 30:
recs.append("冷却时间较长,建议优化水路布局使水路更靠近型腔")
if len(channels) < 4:
recs.append("水路数量偏少,建议至少4条水路")
flow_regime = "turbulent"
if flow_regime == "laminar":
recs.append("冷却液流速偏低,建议提高流速以达到湍流状态(Re>4000)")
if not recs:
recs.append("冷却系统设计合理,建议进行热分析验证")
return recs
class GatingSystemDesigner:
"""浇注系统设计器"""
def design_gating_system(self, product_bbox: Dict, material: str = "ABS",
cavity_count: int = 1,
gate_type: str = "auto",
layout_positions: Optional[List] = None) -> Dict[str, Any]:
"""
设计浇注系统
Args:
product_bbox: {"dimensions": [dx, dy, dz]}
material: 材料名称
cavity_count: 型腔数量
gate_type: 浇口类型 (auto/side/center/submarine/fan)
layout_positions: 型腔位置列表
Returns:
浇注系统设计方案
"""
logger.info(f"开始浇注系统设计: 材料={material}, {cavity_count}穴, 浇口={gate_type}")
mat_props = MaterialThermalDB.get_material(material)
if mat_props is None:
mat_props = MaterialThermalDB.PLASTICS["ABS"]
dims = product_bbox.get("dimensions", [100, 100, 50])
if gate_type == "auto":
gate_type = self._recommend_gate_type(dims, cavity_count)
sprue = self._design_sprue(dims, mat_props)
runner = self._design_runner(dims, cavity_count, layout_positions)
gate = self._design_gate(dims, gate_type, cavity_count, mat_props)
venting = self._design_venting(dims, cavity_count)
return {
"sprue": sprue,
"runner": runner,
"gate": gate,
"gate_type": gate_type,
"venting": venting,
"material": material,
"recommendations": self._generate_gating_recommendations(
gate_type, cavity_count, dims, mat_props
),
}
def _recommend_gate_type(self, dims: List[float], cavity_count: int) -> str:
"""推荐浇口类型"""
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if cavity_count == 1:
if aspect > 2:
return "side"
return "center"
else:
return "side"
def _design_sprue(self, dims: List[float], mat_props: Dict) -> Dict:
"""设计主流道"""
max_dim = max(dims)
volume = dims[0] * dims[1] * dims[2]
if volume > 500000:
sprue_d_top = 4.0
sprue_d_bottom = 8.0
elif volume > 50000:
sprue_d_top = 3.0
sprue_d_bottom = 6.0
else:
sprue_d_top = 2.5
sprue_d_bottom = 5.0
sprue_length = max_dim * 0.5 + 20
taper_angle = math.degrees(
math.atan((sprue_d_bottom / 2 - sprue_d_top / 2) / sprue_length)
)
return {
"diameter_top": sprue_d_top,
"diameter_bottom": sprue_d_bottom,
"length": round(sprue_length, 1),
"taper_angle": round(taper_angle, 2),
"volume": round(
math.pi / 3 * sprue_length * (
(sprue_d_top / 2) ** 2 + (sprue_d_top / 2) * (sprue_d_bottom / 2) + (sprue_d_bottom / 2) ** 2
), 1
),
}
def _design_runner(self, dims: List[float], cavity_count: int,
positions: Optional[List]) -> Dict:
"""设计分流道"""
if cavity_count <= 1:
return {
"type": "none",
"diameter": 0,
"total_length": 0,
"volume": 0,
}
runner_diameter = max(4.0, min(dims[:2]) * 0.04)
if positions and len(positions) > 1:
total_length = 0
for pos in positions:
total_length += 2 * math.sqrt(pos[0] ** 2 + pos[1] ** 2)
else:
total_length = cavity_count * max(dims[:2]) * 1.5
cross_area = math.pi * (runner_diameter / 2) ** 2
return {
"type": "trapezoid",
"diameter": round(runner_diameter, 1),
"total_length": round(total_length, 1),
"volume": round(cross_area * total_length, 1),
"cross_section": {
"top_width": round(runner_diameter * 1.2, 1),
"bottom_width": round(runner_diameter * 0.8, 1),
"depth": round(runner_diameter * 0.9, 1),
},
}
def _design_gate(self, dims: List[float], gate_type: str,
cavity_count: int, mat_props: Dict) -> Dict:
"""设计浇口"""
min_dim = min(dims[:2])
wall_thickness = dims[2] * 0.6
if gate_type == "center":
gate_diameter = max(1.0, wall_thickness * 0.5)
return {
"type": "center",
"diameter": round(gate_diameter, 1),
"length": 1.5,
"position": "top_center",
}
elif gate_type == "submarine":
gate_diameter = max(0.8, wall_thickness * 0.3)
return {
"type": "submarine",
"diameter": round(gate_diameter, 1),
"length": 2.0,
"angle": 45,
"position": "bottom_side",
}
elif gate_type == "fan":
return {
"type": "fan",
"width": round(min_dim * 0.3, 1),
"depth": round(wall_thickness * 0.5, 1),
"length": 1.5,
"position": "side",
}
else:
gate_diameter = max(1.0, wall_thickness * 0.4)
return {
"type": "side",
"diameter": round(gate_diameter, 1),
"length": 2.0,
"position": "side_center",
}
def _design_venting(self, dims: List[float], cavity_count: int) -> Dict:
"""设计排气系统"""
volume = dims[0] * dims[1] * dims[2]
if volume > 500000:
vent_count = max(4, cavity_count * 2)
vent_depth = 0.03
vent_width = 8.0
elif volume > 50000:
vent_count = max(2, cavity_count)
vent_depth = 0.02
vent_width = 5.0
else:
vent_count = cavity_count
vent_depth = 0.015
vent_width = 3.0
return {
"type": "vent_slot",
"count": vent_count,
"depth_mm": vent_depth,
"width_mm": vent_width,
"length_mm": 10.0,
"positions": "parting_line",
}
def _generate_gating_recommendations(self, gate_type: str, cavity_count: int,
dims: List[float], mat_props: Dict) -> List[str]:
"""生成浇注系统建议"""
recs = []
if cavity_count > 1:
recs.append("多型腔模具建议使用平衡式流道布局")
if mat_props.get("melt_temp", 0) > 260:
recs.append("高熔点材料,建议使用热流道系统减少废料")
if gate_type == "center":
recs.append("中心浇口适用于单型腔,注意浇口痕处理")
elif gate_type == "side":
recs.append("侧浇口适用于多型腔,需注意流动平衡")
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if aspect > 3:
recs.append("产品长宽比大,建议使用多点进浇或扇形浇口")
if not recs:
recs.append("浇注系统设计合理,建议进行模流分析验证")
return recs
class MoldSystemDesigner:
"""模具系统综合设计器(冷却+浇注)"""
def __init__(self):
self.cooling_designer = CoolingSystemDesigner()
self.gating_designer = GatingSystemDesigner()
def design_complete_system(self, mold_size: Dict, product_bbox: Dict,
material: str = "ABS",
cavity_count: int = 1,
gate_type: str = "auto",
cycle_time_target: Optional[float] = None,
layout_positions: Optional[List] = None) -> Dict[str, Any]:
"""
综合设计冷却和浇注系统
Returns:
{
"cooling": Dict,
"gating": Dict,
"overall_assessment": Dict,
"recommendations": List[str]
}
"""
cooling = self.cooling_designer.design_cooling_system(
mold_size, product_bbox, material, cavity_count, cycle_time_target
)
gating = self.gating_designer.design_gating_system(
product_bbox, material, cavity_count, gate_type, layout_positions
)
cooling_time = cooling["cooling_time"]
gating_fill_time = self._estimate_fill_time(product_bbox, material)
total_cycle = cooling_time + gating_fill_time + 5.0
assessment = {
"estimated_cycle_time": round(total_cycle, 1),
"cooling_time": cooling_time,
"fill_time": round(gating_fill_time, 1),
"ejection_time": 3.0,
"buffer_time": 2.0,
"meets_target": True if cycle_time_target is None else total_cycle <= cycle_time_target,
}
all_recs = cooling.get("recommendations", []) + gating.get("recommendations", [])
if assessment["meets_target"] is False:
all_recs.insert(0, f"成型周期({total_cycle:.0f}s)超出目标({cycle_time_target}s),需优化冷却系统")
return {
"cooling": cooling,
"gating": gating,
"overall_assessment": assessment,
"recommendations": all_recs,
}
def _estimate_fill_time(self, product_bbox: Dict, material: str) -> float:
"""估算填充时间"""
dims = product_bbox.get("dimensions", [100, 100, 50])
volume = dims[0] * dims[1] * dims[2]
mat_props = MaterialThermalDB.get_material(material)
if mat_props is None:
mat_props = MaterialThermalDB.PLASTICS["ABS"]
fill_rate = 50.0
fill_time = volume / fill_rate
return max(0.5, min(fill_time, 10.0))
+533
View File
@@ -0,0 +1,533 @@
"""
侧壁/倒扣面滑块机构检测与设计模块
功能:
1. 倒扣区域检测 - 识别无法直接脱模的侧壁凹槽
2. 滑块机构设计 - 侧向分型抽芯机构
3. 斜顶机构设计 - 内侧倒扣的斜顶脱模机构
4. 机构运动学分析 - 抽芯行程、脱模角度计算
倒扣检测原理:
- 分型方向确定后,检查每个面的法向量
- 如果面的法向量与脱模方向的点积为负(面朝向脱模反方向)
且该面不在分型面上,则判定为倒扣面
- 根据倒扣面的位置(外侧/内侧)选择滑块或斜顶
滑块 vs 斜顶:
- 滑块:外侧倒扣,沿导滑槽侧向运动
- 斜顶:内侧倒扣,沿斜导柱内侧运动
"""
from typing import Dict, List, Any, Optional, Tuple
import math
import numpy as np
from utils.logger import get_logger
logger = get_logger(__name__)
class UndercutDetector:
"""倒扣区域检测器"""
def detect_undercuts(self, shape: Any, parting_direction: List[float],
parting_surface: Any = None) -> Dict[str, Any]:
"""
检测产品中的倒扣区域
Args:
shape: OCC 产品形状
parting_direction: 分型方向 [nx, ny, nz]
parting_surface: 分型面(可选)
Returns:
{
"undercut_faces": List[Dict],
"slider_regions": List[Dict],
"lifter_regions": List[Dict],
"total_undercut_area": float,
"requires_slider": bool,
"requires_lifter": bool,
"complexity": str
}
"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
from OCC.Core.gp import gp_Dir
dir_vec = np.array(parting_direction, dtype=np.float64)
dir_norm = np.linalg.norm(dir_vec)
if dir_norm < 1e-6:
dir_vec = np.array([0, 0, 1])
else:
dir_vec /= dir_norm
parting_dir = gp_Dir(dir_vec[0], dir_vec[1], dir_vec[2])
undercut_faces = []
slider_regions = []
lifter_regions = []
total_undercut_area = 0.0
parting_z = 0.0
if parting_surface is not None:
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
parting_z = surface.Plane().Location().Z()
except Exception:
pass
explorer = TopExp_Explorer(shape, TopAbs_FACE)
face_idx = 0
while explorer.More():
face = TopoDS_Face(explorer.Current())
face_idx += 1
try:
surface = BRepAdaptor_Surface(face)
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
face_normal = None
if surface.GetType() == 0:
face_normal = surface.Plane().Position().Direction()
else:
from OCC.Core.BRepLProp import BRepLProp_SLProps
props = BRepLProp_SLProps(surface, 1, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
face_normal = props.Normal()
if face_normal is None:
explorer.Next()
continue
dot = face_normal.Dot(parting_dir)
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = face_props.Mass()
center = face_props.CentreOfMass()
bbox = Bnd_Box()
brepbndlib_Add(face, bbox)
try:
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get()
except Exception:
fxmin, fymin, fzmin, fxmax, fymax, fzmax = 0, 0, 0, 0, 0, 0
if dot < -0.1:
face_center_z = center.Z()
is_outer = face_center_z >= parting_z
undercut_info = {
"face_index": face_idx,
"normal": [face_normal.X(), face_normal.Y(), face_normal.Z()],
"dot_product": float(dot),
"area": float(area),
"center": [float(center.X()), float(center.Y()), float(center.Z())],
"bbox": {
"min": [float(fxmin), float(fymin), float(fzmin)],
"max": [float(fxmax), float(fymax), float(fzmax)]
},
"severity": "high" if dot < -0.5 else "medium",
"is_outer": is_outer,
}
undercut_faces.append(undercut_info)
total_undercut_area += area
except Exception:
pass
explorer.Next()
for uf in undercut_faces:
normal = np.array(uf["normal"])
lateral_component = normal - np.dot(normal, dir_vec) * dir_vec
lateral_norm = np.linalg.norm(lateral_component)
if lateral_norm > 0.01:
slide_direction = lateral_component / lateral_norm
else:
slide_direction = np.array([1, 0, 0])
mechanism = {
"face_indices": [uf["face_index"]],
"slide_direction": slide_direction.tolist(),
"area": uf["area"],
"center": uf["center"],
"severity": uf["severity"],
}
if uf["is_outer"]:
slider_regions.append(mechanism)
else:
lifter_regions.append(mechanism)
requires_slider = len(slider_regions) > 0
requires_lifter = len(lifter_regions) > 0
total_count = len(slider_regions) + len(lifter_regions)
if total_count == 0:
complexity = "simple"
elif total_count <= 2:
complexity = "moderate"
elif total_count <= 4:
complexity = "complex"
else:
complexity = "very_complex"
result = {
"undercut_faces": undercut_faces,
"slider_regions": slider_regions,
"lifter_regions": lifter_regions,
"total_undercut_area": total_undercut_area,
"requires_slider": requires_slider,
"requires_lifter": requires_lifter,
"complexity": complexity,
"parting_direction": parting_direction,
}
logger.info(f"倒扣检测完成: {len(undercut_faces)} 个倒扣面, "
f"{len(slider_regions)} 个滑块, {len(lifter_regions)} 个斜顶, "
f"复杂度={complexity}")
return result
except Exception as e:
logger.error(f"倒扣检测失败: {e}")
return {
"undercut_faces": [],
"slider_regions": [],
"lifter_regions": [],
"total_undercut_area": 0,
"requires_slider": False,
"requires_lifter": False,
"complexity": "unknown",
"parting_direction": parting_direction,
}
class SliderMechanismDesigner:
"""滑块机构设计器"""
def design_slider(self, slider_region: Dict, mold_size: Dict,
parting_direction: List[float]) -> Dict[str, Any]:
"""
设计滑块机构
Args:
slider_region: 倒扣区域信息
mold_size: 模具尺寸
parting_direction: 分型方向
Returns:
滑块机构设计方案
"""
center = slider_region["center"]
area = slider_region["area"]
slide_dir = slider_region["slide_direction"]
slide_stroke = self._calculate_slide_stroke(slider_region, mold_size)
slide_angle = self._calculate_slide_angle(slide_dir, parting_direction)
slide_block_size = self._calculate_slide_block_size(area, slide_stroke)
guide_type = self._select_guide_type(slide_stroke, slide_angle)
return {
"type": "slider",
"location": center,
"slide_direction": slide_dir,
"slide_stroke": slide_stroke,
"slide_angle": slide_angle,
"block_size": slide_block_size,
"guide_type": guide_type,
"locking_mechanism": self._select_locking(slide_angle),
"actuation": "hydraulic" if slide_stroke > 50 else "mechanical",
"components": self._generate_components(slide_block_size, guide_type),
"manufacturing_notes": self._generate_slider_notes(slide_angle, slide_stroke),
}
def _calculate_slide_stroke(self, region: Dict, mold_size: Dict) -> float:
"""计算抽芯行程"""
bbox = region.get("bbox", {})
if "max" in bbox and "min" in bbox:
max_dim = max(
abs(bbox["max"][0] - bbox["min"][0]),
abs(bbox["max"][1] - bbox["min"][1]),
abs(bbox["max"][2] - bbox["min"][2])
)
else:
max_dim = 10.0
stroke = max_dim + 5.0
return round(max(stroke, 10.0), 1)
def _calculate_slide_angle(self, slide_dir: List[float],
parting_dir: List[float]) -> float:
"""计算滑块倾斜角度"""
s = np.array(slide_dir)
p = np.array(parting_dir)
s_norm = np.linalg.norm(s)
p_norm = np.linalg.norm(p)
if s_norm < 1e-6 or p_norm < 1e-6:
return 90.0
cos_angle = np.clip(np.dot(s, p) / (s_norm * p_norm), -1, 1)
angle = math.degrees(math.acos(abs(cos_angle)))
return round(angle, 1)
def _calculate_slide_block_size(self, area: float, stroke: float) -> Dict[str, float]:
"""计算滑块尺寸"""
width = max(math.sqrt(area) * 1.5, 15.0)
height = max(math.sqrt(area) * 1.2, 12.0)
length = stroke + width * 0.5
return {
"width": round(width, 1),
"height": round(height, 1),
"length": round(length, 1),
}
def _select_guide_type(self, stroke: float, angle: float) -> str:
"""选择导滑方式"""
if stroke > 80:
return "T_slot_guide"
elif angle > 20:
return "angled_guide_pin"
else:
return "dovetail_guide"
def _select_locking(self, angle: float) -> str:
"""选择锁紧方式"""
if angle > 25:
return "wedge_block"
else:
return "lock_block"
def _generate_components(self, block_size: Dict, guide_type: str) -> List[Dict]:
"""生成滑块组件清单"""
components = [
{"name": "slide_block", "material": "P20", "hardness": "HRC 28-32"},
{"name": "guide_strip", "material": "bronze", "hardness": "HB 80-100"},
{"name": "wear_plate", "material": "T8", "hardness": "HRC 45-50"},
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
]
if guide_type == "T_slot_guide":
components.append({"name": "T_slot_insert", "material": "P20", "hardness": "HRC 28-32"})
elif guide_type == "angled_guide_pin":
components.append({"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"})
elif guide_type == "dovetail_guide":
components.append({"name": "dovetail_block", "material": "P20", "hardness": "HRC 28-32"})
return components
def _generate_slider_notes(self, angle: float, stroke: float) -> List[str]:
"""生成滑块加工注意事项"""
notes = []
if angle > 25:
notes.append("滑块角度较大,需确保锁紧可靠")
if stroke > 50:
notes.append("抽芯行程较长,建议使用液压抽芯")
if stroke > 80:
notes.append("大行程抽芯,需校核导滑槽强度")
notes.append("滑块需设置限位装置,防止脱出")
notes.append("配合面需做耐磨处理")
return notes
class LifterMechanismDesigner:
"""斜顶机构设计器"""
def design_lifter(self, lifter_region: Dict, mold_size: Dict,
parting_direction: List[float]) -> Dict[str, Any]:
"""
设计斜顶机构
Args:
lifter_region: 内侧倒扣区域信息
mold_size: 模具尺寸
parting_direction: 分型方向
Returns:
斜顶机构设计方案
"""
center = lifter_region["center"]
area = lifter_region["area"]
lifter_angle = self._calculate_lifter_angle(lifter_region)
lifter_stroke = self._calculate_lifter_stroke(lifter_region)
lifter_size = self._calculate_lifter_size(area, lifter_stroke, lifter_angle)
return {
"type": "lifter",
"location": center,
"lifter_angle": lifter_angle,
"lifter_stroke": lifter_stroke,
"block_size": lifter_size,
"guide_type": "angled_hole",
"return_mechanism": "spring_return",
"components": self._generate_lifter_components(lifter_size),
"manufacturing_notes": self._generate_lifter_notes(lifter_angle),
}
def _calculate_lifter_angle(self, region: Dict) -> float:
"""计算斜顶角度(通常5-15度)"""
return 8.0
def _calculate_lifter_stroke(self, region: Dict) -> float:
"""计算斜顶行程"""
bbox = region.get("bbox", {})
if "max" in bbox and "min" in bbox:
max_dim = max(
abs(bbox["max"][i] - bbox["min"][i]) for i in range(3)
)
else:
max_dim = 5.0
return round(max(max_dim + 3.0, 8.0), 1)
def _calculate_lifter_size(self, area: float, stroke: float,
angle: float) -> Dict[str, float]:
"""计算斜顶尺寸"""
width = max(math.sqrt(area) * 1.2, 10.0)
height = stroke / math.sin(math.radians(angle)) if angle > 0 else stroke * 3
thickness = max(width * 0.6, 8.0)
return {
"width": round(width, 1),
"height": round(height, 1),
"thickness": round(thickness, 1),
}
def _generate_lifter_components(self, size: Dict) -> List[Dict]:
"""生成斜顶组件清单"""
return [
{"name": "lifter_body", "material": "P20", "hardness": "HRC 28-32"},
{"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"},
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
{"name": "wear_bushing", "material": "bronze", "hardness": "HB 80-100"},
]
def _generate_lifter_notes(self, angle: float) -> List[str]:
"""生成斜顶加工注意事项"""
notes = []
if angle > 12:
notes.append("斜顶角度偏大,需校核脱模力")
notes.append("斜顶导滑孔需精确加工")
notes.append("斜顶头部需做耐磨处理")
notes.append("需设置限位防止斜顶脱出")
return notes
class SideActionDesigner:
"""侧向分型机构综合设计器"""
def __init__(self):
self.undercut_detector = UndercutDetector()
self.slider_designer = SliderMechanismDesigner()
self.lifter_designer = LifterMechanismDesigner()
def analyze_and_design(self, shape: Any, parting_direction: List[float],
mold_size: Dict, parting_surface: Any = None) -> Dict[str, Any]:
"""
综合分析倒扣并设计侧向分型机构
Returns:
{
"undercut_analysis": Dict,
"slider_mechanisms": List[Dict],
"lifter_mechanisms": List[Dict],
"summary": Dict,
"recommendations": List[str]
}
"""
logger.info("开始侧向分型机构分析...")
undercut_result = self.undercut_detector.detect_undercuts(
shape, parting_direction, parting_surface
)
slider_mechanisms = []
for region in undercut_result["slider_regions"]:
slider = self.slider_designer.design_slider(
region, mold_size, parting_direction
)
slider_mechanisms.append(slider)
lifter_mechanisms = []
for region in undercut_result["lifter_regions"]:
lifter = self.lifter_designer.design_lifter(
region, mold_size, parting_direction
)
lifter_mechanisms.append(lifter)
total_mechanisms = len(slider_mechanisms) + len(lifter_mechanisms)
summary = {
"total_undercut_faces": len(undercut_result["undercut_faces"]),
"total_slider_count": len(slider_mechanisms),
"total_lifter_count": len(lifter_mechanisms),
"total_mechanism_count": total_mechanisms,
"complexity": undercut_result["complexity"],
"has_hydraulic": any(s.get("actuation") == "hydraulic" for s in slider_mechanisms),
}
recommendations = self._generate_overall_recommendations(summary, undercut_result)
result = {
"undercut_analysis": undercut_result,
"slider_mechanisms": slider_mechanisms,
"lifter_mechanisms": lifter_mechanisms,
"summary": summary,
"recommendations": recommendations,
}
logger.info(f"侧向分型机构设计完成: {len(slider_mechanisms)} 个滑块, "
f"{len(lifter_mechanisms)} 个斜顶")
return result
def _generate_overall_recommendations(self, summary: Dict,
undercut: Dict) -> List[str]:
"""生成总体建议"""
recs = []
if summary["total_mechanism_count"] == 0:
recs.append("无倒扣区域,模具结构简单,无需侧向分型机构")
return recs
if summary["total_slider_count"] > 0:
recs.append(f"需要 {summary['total_slider_count']} 个滑块机构处理外侧倒扣")
if summary["total_lifter_count"] > 0:
recs.append(f"需要 {summary['total_lifter_count']} 个斜顶机构处理内侧倒扣")
if summary["has_hydraulic"]:
recs.append("大行程抽芯需使用液压系统,需配置液压站")
if summary["complexity"] == "very_complex":
recs.append("侧向分型机构复杂,建议评估是否可通过产品修改简化")
recs.append("考虑使用二次分型或旋转脱模替代方案")
if summary["total_mechanism_count"] > 3:
recs.append("侧向机构较多,建议优化模具结构减少机构数量")
recs.append("所有侧向机构需做运动仿真验证干涉")
return recs
+32 -27
View File
@@ -1,5 +1,5 @@
# models/database.py
from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint
from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint, Numeric
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
@@ -10,6 +10,7 @@ Base = declarative_base()
class User(Base):
"""用户表"""
__tablename__ = "users"
__excluded_fields__ = {'hashed_password'}
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
@@ -40,6 +41,10 @@ class User(Base):
return True
return False
def safe_dict(self):
return {k: v for k, v in self.__dict__.items()
if not k.startswith('_') and k not in self.__excluded_fields__}
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}')>"
@@ -357,7 +362,7 @@ class FeatureDetection(Base):
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
# 特征信息
feature_type = Column(String(50), nullable=False, index=True) # thin_wall, thick_wall, rib, boss, draft_angle
feature_type = Column(String(50), nullable=False, index=True) # thin_wall, thick_wall, wall_non_uniform, rib, boss, draft_angle, high_curvature, fillet
confidence = Column(Float, nullable=False) # 0.0 - 1.0
# 位置和尺寸
@@ -481,8 +486,8 @@ class Product(Base):
category = Column(String(100), nullable=True)
unit = Column(String(20), default="件")
item_type = Column(String(20), default="finished", index=True)
cost_price = Column(Float, default=0)
sale_price = Column(Float, default=0)
cost_price = Column(Numeric(12, 2), default=0)
sale_price = Column(Numeric(12, 2), default=0)
min_stock = Column(Integer, default=0)
max_stock = Column(Integer, default=1000)
is_active = Column(Boolean, default=True)
@@ -516,8 +521,8 @@ class ProductMaterial(Base):
id = Column(Integer, primary_key=True, index=True)
finished_product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
material_product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
quantity = Column(Float, nullable=False)
loss_rate = Column(Float, default=0)
quantity = Column(Numeric(12, 4), nullable=False)
loss_rate = Column(Numeric(5, 4), default=0)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
@@ -542,7 +547,7 @@ class MaterialPriceHistory(Base):
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
price = Column(Float, nullable=False)
price = Column(Numeric(12, 2), nullable=False)
effective_date = Column(DateTime, default=func.now(), index=True)
supplier_id = Column(Integer, ForeignKey("suppliers.id"), nullable=True, index=True)
remark = Column(Text, nullable=True)
@@ -615,7 +620,7 @@ class Customer(Base):
bank_name = Column(String(100), nullable=True)
bank_account = Column(String(50), nullable=True)
tax_number = Column(String(50), nullable=True)
credit_limit = Column(Float, default=0)
credit_limit = Column(Numeric(12, 2), default=0)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
@@ -653,8 +658,8 @@ class Inventory(Base):
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True)
quantity = Column(Float, default=0)
locked_quantity = Column(Float, default=0)
quantity = Column(Numeric(12, 4), default=0)
locked_quantity = Column(Numeric(12, 4), default=0)
batch_number = Column(String(50), nullable=True)
location = Column(String(100), nullable=True)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
@@ -678,14 +683,14 @@ class StockMovement(Base):
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False)
movement_type = Column(String(20), nullable=False)
quantity = Column(Float, nullable=False)
before_quantity = Column(Float, default=0)
after_quantity = Column(Float, default=0)
quantity = Column(Numeric(12, 4), nullable=False)
before_quantity = Column(Numeric(12, 4), default=0)
after_quantity = Column(Numeric(12, 4), default=0)
reference_type = Column(String(50), nullable=True)
reference_id = Column(Integer, nullable=True)
reference_no = Column(String(50), nullable=True)
unit_price = Column(Float, nullable=True)
total_amount = Column(Float, nullable=True)
unit_price = Column(Numeric(12, 2), nullable=True)
total_amount = Column(Numeric(12, 2), nullable=True)
remark = Column(Text, nullable=True)
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
created_at = Column(DateTime, default=func.now(), index=True)
@@ -706,8 +711,8 @@ class PurchaseOrder(Base):
order_date = Column(DateTime, default=func.now())
expected_date = Column(Date, nullable=True)
status = Column(String(20), default="draft")
total_amount = Column(Float, default=0)
paid_amount = Column(Float, default=0)
total_amount = Column(Numeric(12, 2), default=0)
paid_amount = Column(Numeric(12, 2), default=0)
remark = Column(Text, nullable=True)
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
created_at = Column(DateTime, default=func.now())
@@ -732,8 +737,8 @@ class PurchaseOrderItem(Base):
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, nullable=False)
received_quantity = Column(Integer, default=0)
unit_price = Column(Float, nullable=False)
amount = Column(Float, nullable=False)
unit_price = Column(Numeric(12, 2), nullable=False)
amount = Column(Numeric(12, 2), nullable=False)
remark = Column(Text, nullable=True)
order = relationship("PurchaseOrder", back_populates="items")
@@ -757,10 +762,10 @@ class SalesOrder(Base):
status = Column(String(20), default="draft")
production_status = Column(String(20), default="not_started", index=True)
production_no = Column(String(50), nullable=True, index=True)
planned_material_cost = Column(Float, default=0)
actual_material_cost = Column(Float, default=0)
total_amount = Column(Float, default=0)
received_amount = Column(Float, default=0)
planned_material_cost = Column(Numeric(12, 2), default=0)
actual_material_cost = Column(Numeric(12, 2), default=0)
total_amount = Column(Numeric(12, 2), default=0)
received_amount = Column(Numeric(12, 2), default=0)
remark = Column(Text, nullable=True)
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
created_at = Column(DateTime, default=func.now())
@@ -781,7 +786,7 @@ class FinanceTransaction(Base):
txn_type = Column(String(20), nullable=False, index=True)
partner_type = Column(String(20), nullable=False, index=True)
partner_id = Column(Integer, nullable=False, index=True)
amount = Column(Float, nullable=False)
amount = Column(Numeric(12, 2), nullable=False)
txn_date = Column(DateTime, default=func.now(), index=True)
method = Column(String(30), default="bank")
account_name = Column(String(100), nullable=True)
@@ -803,7 +808,7 @@ class FinanceAllocation(Base):
transaction_id = Column(Integer, ForeignKey("finance_transactions.id"), nullable=False, index=True)
order_type = Column(String(20), nullable=False, index=True)
order_id = Column(Integer, nullable=False, index=True)
allocated_amount = Column(Float, nullable=False)
allocated_amount = Column(Numeric(12, 2), nullable=False)
created_at = Column(DateTime, default=func.now(), index=True)
transaction = relationship("FinanceTransaction", back_populates="allocations")
@@ -852,8 +857,8 @@ class SalesOrderItem(Base):
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, nullable=False)
delivered_quantity = Column(Integer, default=0)
unit_price = Column(Float, nullable=False)
amount = Column(Float, nullable=False)
unit_price = Column(Numeric(12, 2), nullable=False)
amount = Column(Numeric(12, 2), nullable=False)
remark = Column(Text, nullable=True)
order = relationship("SalesOrder", back_populates="items")
+1 -1
View File
@@ -62,7 +62,7 @@
})();
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/air-datepicker@3.5.3/air-datepicker.css">
<link rel="stylesheet" href="/static/style.css?v=20260317-01">
<link rel="stylesheet" href="/static/style.css?v=20260419-01">
</head>
<body>
<div id="app">
+28 -1
View File
@@ -2466,8 +2466,35 @@ select.form-input {
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.export-buttons {
display: flex;
gap: var(--space-2);
margin-left: auto;
flex-wrap: wrap;
}
.export-buttons .btn-sm {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
white-space: nowrap;
}
.btn-secondary {
background: var(--bg-secondary, #6b7280);
color: white;
border: none;
border-radius: var(--radius-md, 6px);
cursor: pointer;
transition: background 0.2s;
}
.btn-secondary:hover {
background: var(--bg-tertiary, #4b5563);
}
.result-title {
+47 -1
View File
@@ -1261,7 +1261,39 @@ const ResultView = {
return material === 'aluminum_foam';
};
return { state, formatFileSize, formatDateTime, formatNumber, getPriorityText, isFoamMaterial };
const exportCAD = async (format) => {
try {
const taskId = route.params.taskId;
const result = await apiRequest('/api/export-mold', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task_id: taskId,
formats: [format],
components: ['cavity', 'core', 'parting_surface']
})
});
if (result.status === 'success' && result.data.files) {
for (const file of result.data.files) {
const downloadUrl = `/api/export-download/${file.filepath.replace(/\\/g, '/').split('/').slice(-2).join('/')}`;
const link = document.createElement('a');
link.href = downloadUrl;
link.download = file.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
addNotification(`已导出 ${result.data.files.length} 个 ${format.toUpperCase()} 文件`, 'success');
} else if (result.data.errors && result.data.errors.length > 0) {
addNotification(`导出失败: ${result.data.errors[0]}`, 'error');
}
} catch (e) {
addNotification(`导出失败: ${e.message}`, 'error');
}
};
return { state, formatFileSize, formatDateTime, formatNumber, getPriorityText, isFoamMaterial, exportCAD };
},
template: `
<div class="page-container">
@@ -1284,6 +1316,20 @@ const ResultView = {
<span :class="['badge', state.task.status === 'completed' ? 'badge-success' : 'badge-error']">
{{ state.task.status }}
</span>
<div v-if="state.task.status === 'completed'" class="export-buttons">
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
导出 STEP
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
导出 STL
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
导出 IGES
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
导出 BRep
</button>
</div>
</div>
<div class="result-grid">