diff --git a/.env.example b/.env.example index c0779c1..fef42d5 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 2a62d5a..0a2c590 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ __pycache__/ .DS_Store *.log .env +1panel.env +*.env.local logs/ uploads/ html_output/ diff --git a/config/settings.py b/config/settings.py index cf62343..ffc6181 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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', '系统管理员') diff --git a/requirements.txt b/requirements.txt index 2af6bfc..b88df5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ # 几何处理核心 # ============================================ # PythonOCC - CAD几何处理 -# pythonocc-core>=7.7.0 +pythonocc-core>=7.7.0 # 网格处理 trimesh>=3.21.0 # 科学计算 diff --git a/src/api/inventory/finance_routes.py b/src/api/inventory/finance_routes.py index 3743f26..93cf5df 100644 --- a/src/api/inventory/finance_routes.py +++ b/src/api/inventory/finance_routes.py @@ -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, ) diff --git a/src/api/inventory/product_routes.py b/src/api/inventory/product_routes.py index 1069201..32c2e45 100644 --- a/src/api/inventory/product_routes.py +++ b/src/api/inventory/product_routes.py @@ -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, ) diff --git a/src/api/inventory/purchase_order_routes.py b/src/api/inventory/purchase_order_routes.py index cfac12c..0e5169c 100644 --- a/src/api/inventory/purchase_order_routes.py +++ b/src/api/inventory/purchase_order_routes.py @@ -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 ) diff --git a/src/api/inventory/sales_order_routes.py b/src/api/inventory/sales_order_routes.py index 8ff5d45..8b2d5b5 100644 --- a/src/api/inventory/sales_order_routes.py +++ b/src/api/inventory/sales_order_routes.py @@ -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, diff --git a/src/api/inventory/schemas/finance_schemas.py b/src/api/inventory/schemas/finance_schemas.py index 054185a..d5c5ce7 100644 --- a/src/api/inventory/schemas/finance_schemas.py +++ b/src/api/inventory/schemas/finance_schemas.py @@ -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,13 +37,11 @@ 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 @@ -50,7 +49,7 @@ class FinanceTransactionResponse(BaseModel): 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] = [] diff --git a/src/api/inventory/schemas/inventory_schemas.py b/src/api/inventory/schemas/inventory_schemas.py index 85078e5..033984d 100644 --- a/src/api/inventory/schemas/inventory_schemas.py +++ b/src/api/inventory/schemas/inventory_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/product_schemas.py b/src/api/inventory/schemas/product_schemas.py index f0dd524..56e1c72 100644 --- a/src/api/inventory/schemas/product_schemas.py +++ b/src/api/inventory/schemas/product_schemas.py @@ -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] diff --git a/src/api/inventory/schemas/purchase_order_schemas.py b/src/api/inventory/schemas/purchase_order_schemas.py index f3bcd43..53ae7c4 100644 --- a/src/api/inventory/schemas/purchase_order_schemas.py +++ b/src/api/inventory/schemas/purchase_order_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/sales_order_schemas.py b/src/api/inventory/schemas/sales_order_schemas.py index a11f7ff..4ae237f 100644 --- a/src/api/inventory/schemas/sales_order_schemas.py +++ b/src/api/inventory/schemas/sales_order_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/stock_movement_schemas.py b/src/api/inventory/schemas/stock_movement_schemas.py index 4f92639..c438510 100644 --- a/src/api/inventory/schemas/stock_movement_schemas.py +++ b/src/api/inventory/schemas/stock_movement_schemas.py @@ -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 diff --git a/src/api/routes.py b/src/api/routes.py index 882725f..be73709 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -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 # 添加验证结果 diff --git a/src/core/ai_parting_detector.py b/src/core/ai_parting_detector.py new file mode 100644 index 0000000..71d0eae --- /dev/null +++ b/src/core/ai_parting_detector.py @@ -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, + ) diff --git a/src/core/aluminum_foam_mold.py b/src/core/aluminum_foam_mold.py index 296cb6f..18c2e22 100644 --- a/src/core/aluminum_foam_mold.py +++ b/src/core/aluminum_foam_mold.py @@ -5,65 +5,54 @@ 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, + + def __init__(self, shrinkage_rate: float = 0.015, draft_angle: float = 3.0, material_density: float = 0.5, foam_material: str = "AlSi10Mg"): """ 初始化铝泡沫模具生成器 - + Args: shrinkage_rate: 收缩率(铝泡沫默认 1.5%) draft_angle: 拔模角(铝泡沫建议 3-5°) 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, @@ -94,8 +83,7 @@ class AluminumFoamMoldGenerator: "description": "轻质铝镁泡沫" } } - - # 塑料材料数据库(保留原有) + self.plastic_materials = { "ABS": {"density": 1.05, "shrinkage": 0.005}, "PP": {"density": 0.90, "shrinkage": 0.016}, @@ -106,21 +94,15 @@ class AluminumFoamMoldGenerator: "POM": {"density": 1.42, "shrinkage": 0.020}, "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): """设置铝泡沫材料""" if material in self.foam_materials: @@ -131,7 +113,7 @@ class AluminumFoamMoldGenerator: logger.info(f"铝泡沫材料设置为 {material}, 密度: {props['density']} g/cm³") else: logger.warning(f"未知材料 {material}, 使用当前设置") - + def set_material(self, material: str): """设置材料(自动识别类型)""" if material in self.foam_materials: @@ -143,11 +125,11 @@ class AluminumFoamMoldGenerator: logger.info(f"塑料材料设置为 {material}, 密度: {props['density']} g/cm³") else: logger.warning(f"未知材料 {material}") - + def generate_mold_cavities(self, product_shape: Any) -> Dict[str, Any]: """ 从产品的3D模型生成型腔和型芯 - + 完整流程: 1. 分析产品几何 2. 检测分型面(支持多分型面) @@ -160,34 +142,26 @@ 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("铝泡沫模具型腔生成完成") - + return { "cavity": cavity, "core": core, @@ -205,24 +179,21 @@ class AluminumFoamMoldGenerator: except Exception as e: logger.error(f"模具型腔生成失败: {e}") raise - + def generate_detailed_cavity_json(self, cavity_data: Dict) -> Dict[str, Any]: """生成详细的型腔三维JSON数据""" cavity = cavity_data["cavity"] core = cavity_data["core"] 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 = { "metadata": { "version": "3.0", @@ -258,14 +229,14 @@ class AluminumFoamMoldGenerator: ) } } - + return detailed_json - + def generate_cavity_key_info(self, cavity_data: Dict) -> Dict[str, Any]: """生成模具型腔的关键信息""" analysis = cavity_data["analysis"] material_info = self.foam_materials.get(self.foam_material, {}) - + key_info = { "mold_parameters": { "shrinkage_rate": f"{self.shrinkage_rate * 100:.2f}%", @@ -299,47 +270,21 @@ class AluminumFoamMoldGenerator: "venting_requirement": self._assess_venting_requirement(analysis) } } - + return key_info - + # ==================== 核心算法实现 ==================== - + 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) - - # 边界框 - 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) - } - + """分析产品几何属性(扩展基类版本,增加法向量统计)""" + result = super()._analyze_product_geometry(shape) + result["normal_statistics"] = self._analyze_face_normals(shape) + return result + + 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]: """ 改进的法向量分析 - 使用高斯权重和多点采样 @@ -347,139 +292,121 @@ class AluminumFoamMoldGenerator: face_normals = [] face_centers = [] face_areas = [] - + explorer = TopExp_Explorer(shape, TopAbs_FACE) - + while explorer.More(): face = TopoDS_Face(explorer.Current()) 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]) - + for i in range(sample_points): for j in range(sample_points): 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 - + face_normals.append(normal_sum) face_centers.append([center.X(), center.Y(), center.Z()]) face_areas.append(area) - + except Exception as e: logger.debug(f"面分析失败: {e}") - + explorer.Next() - + if not face_normals: return { "primary_direction": [0, 0, 1], "confidence": 0.5, "face_count": 0 } - - # 使用面积作为权重计算加权平均法向量 + total_area = sum(face_areas) weighted_normal = np.array([0.0, 0.0, 0.0]) - + for i, normal in enumerate(face_normals): 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) dot_products.append(abs(dot)) - + confidence = np.mean(dot_products) if dot_products else 0.5 - + return { "primary_direction": weighted_normal.tolist(), "confidence": float(confidence), "face_count": len(face_normals), "normal_distribution": face_normals } - + def _detect_parting_surfaces(self, shape: Any, analysis: Dict) -> Dict[str, Any]: """ 检测分型面(支持多分型面) """ - # 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,9 +415,9 @@ class AluminumFoamMoldGenerator: "direction": [1, 0, 0], "reason": "产品扁平,需要垂直分型" }) - except: + except Exception: pass - + return { "primary_surface": parting_surface, "primary_line": parting_line, @@ -499,386 +426,181 @@ class AluminumFoamMoldGenerator: "additional_surfaces": additional_surfaces, "surface_count": 1 + len(additional_surfaces) } - + def _detect_undercut_regions(self, shape: Any, parting_surface: Any) -> List[Dict]: """ 检测倒扣区域 """ undercut_regions = [] - + try: - # 获取分型面法向量 surface = BRepAdaptor_Surface(parting_surface) parting_normal = surface.Plane().Position().Direction() - - # 遍历所有面,检查是否存在倒扣 + explorer = TopExp_Explorer(shape, TopAbs_FACE) - + while explorer.More(): face = TopoDS_Face(explorer.Current()) - + 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() + + + 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()], "severity": abs(dot) }) - + except Exception as e: logger.debug(f"倒扣检测失败: {e}") - + explorer.Next() - + logger.info(f"检测到 {len(undercut_regions)} 个倒扣区域") - + except Exception as e: logger.warning(f"倒扣区域检测异常: {e}") - + 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样条拟合 """ if len(parting_line) < 4: return parting_line - + try: points = np.array(parting_line) - - # 简化的平滑算法:移动平均 + smoothed = [] window_size = 3 - + for i in range(len(points)): start = max(0, i - window_size // 2) end = min(len(points), i + window_size // 2 + 1) window = points[start:end] - + if len(window) > 0: avg = np.mean(window, axis=0) smoothed.append(avg.tolist()) - + return smoothed - + except Exception as e: logger.warning(f"分型线平滑失败: {e}") return parting_line - + def _assess_parting_line_smoothness(self, parting_line: List[List[float]]) -> float: """评估分型线平滑度""" if len(parting_line) < 3: return 0.0 - + try: points = np.array(parting_line) - - # 计算相邻线段角度变化 + angles = [] for i in range(1, len(points) - 1): v1 = points[i] - points[i-1] v2 = points[i+1] - points[i] - + len1 = np.linalg.norm(v1) len2 = np.linalg.norm(v2) - + if len1 > 0.001 and len2 > 0.001: cos_angle = np.dot(v1, v2) / (len1 * len2) cos_angle = max(-1, min(1, cos_angle)) angle = np.arccos(cos_angle) angles.append(np.degrees(angle)) - + 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) ).Shape() - + logger.info(f"模具块生成: {length}x{width}x{height} mm") return mold_block - + except Exception as e: 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: adaptor = BRepAdaptor_Surface(surface) normal = adaptor.Plane().Position().Direction() - + return { "type": "plane", "normal": [normal.X(), normal.Y(), normal.Z()], "origin": [0, 0, 0], "bounds": {"u_range": [-200, 200], "v_range": [-200, 200]} } - except: + except Exception: return { "type": "plane", "normal": [0, 0, 1], "origin": [0, 0, 0], "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]) - + parting_plane = gp_Pln( gp_Pnt(origin[0], origin[1], origin[2]), gp_Dir(normal[0], normal[1], normal[2]) ) - + 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, "primary_line": parting_line, @@ -887,25 +609,25 @@ class AluminumFoamMoldGenerator: "additional_surfaces": [], "surface_count": 1 } - + # ==================== 辅助方法 ==================== - + def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]: """估算模具尺寸""" dims = analysis["bounding_box"]["dimensions"] margin = 30 - + return { "length": dims[0] + 2 * margin, "width": dims[1] + 2 * margin, "height": dims[2] + margin + 80, "margin": margin } - + def _calculate_clamping_force(self, analysis: Dict) -> str: """估算锁模力""" volume_cm3 = analysis.get("volume", 0) / 1000 - + if volume_cm3 < 10: return "30-50 吨" elif volume_cm3 < 50: @@ -914,40 +636,34 @@ class AluminumFoamMoldGenerator: return "100-200 吨" 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) surface_area = analysis.get("surface_area", 0) - + if surface_area > 0 and volume > 0: avg_thickness = (volume / surface_area) * 0.6 return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm" - + return "10.0 - 30.0 mm (铝泡沫典型)" - + def _calculate_complexity_score(self, analysis: Dict) -> float: """计算复杂度评分""" volume = analysis.get("volume", 0) surface_area = analysis.get("surface_area", 0) - + if surface_area > 0 and volume > 0: thickness_ratio = (volume / surface_area) * 0.6 complexity = min(thickness_ratio / 5.0, 10.0) return round(complexity, 1) - + return 5.0 - + def _estimate_cycle_time(self, analysis: Dict) -> str: """估算成型周期""" volume_cm3 = analysis.get("volume", 0) / 1000 - + if volume_cm3 < 10: return "60-90 秒" elif volume_cm3 < 50: @@ -956,58 +672,18 @@ class AluminumFoamMoldGenerator: return "120-180 秒" else: return "180-300 秒" - + def _identify_sink_mark_risk(self, analysis: Dict) -> str: """识别缩痕风险""" 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 模型接口已设置") diff --git a/src/core/base_mold_generator.py b/src/core/base_mold_generator.py new file mode 100644 index 0000000..98feff9 --- /dev/null +++ b/src/core/base_mold_generator.py @@ -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 diff --git a/src/core/cad_exporter.py b/src/core/cad_exporter.py new file mode 100644 index 0000000..3430c0e --- /dev/null +++ b/src/core/cad_exporter.py @@ -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" diff --git a/src/core/cavity_layout_optimizer.py b/src/core/cavity_layout_optimizer.py new file mode 100644 index 0000000..4cd81a1 --- /dev/null +++ b/src/core/cavity_layout_optimizer.py @@ -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 diff --git a/src/core/geometry_analyzer.py b/src/core/geometry_analyzer.py index a4ecef5..938e12d 100644 --- a/src/core/geometry_analyzer.py +++ b/src/core/geometry_analyzer.py @@ -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]: """获取壁厚建议""" - volume = geometry_data.get("volume", 0) - surface_area = geometry_data.get("surface_area", 0) + avg_thickness = None - if volume > 0 and surface_area > 0: - avg_thickness = (volume / surface_area) * 0.6 + 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 - # 壁厚均匀性评分 - 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 = 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 + 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,7 +825,11 @@ 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} 个高优先级建议") - return " | ".join(summary_parts) if summary_parts else "分析完成" \ No newline at end of file + return " | ".join(summary_parts) if summary_parts else "分析完成" diff --git a/src/core/mold_cam.py b/src/core/mold_cam.py new file mode 100644 index 0000000..83f3252 --- /dev/null +++ b/src/core/mold_cam.py @@ -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 diff --git a/src/core/mold_generator.py b/src/core/mold_generator.py index dcd00a1..1a8d572 100644 --- a/src/core/mold_generator.py +++ b/src/core/mold_generator.py @@ -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,25 +34,8 @@ 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): """设置产品材料""" @@ -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,61 +175,24 @@ 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]: """ 检测分型面和分型线 - + 优先级: 1. AI 模型检测(如果已设置) 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) @@ -281,173 +200,142 @@ class MoldCavityGenerator: shape, analysis, optimal_direction ) parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face() - - # 计算真实分型线(产品与分型面的交线) + parting_line = self._calculate_parting_line(shape, parting_surface) - + return parting_surface, parting_line - + 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 + 原理: + - 统计所有面的法向量 + - 选择法向量变化最小的方向作为分型方向 + - 避免倒扣(undercut)区域 + """ + face_normals = [] + explorer = TopExp_Explorer(shape, TopAbs_FACE) - def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]: - """提取形状几何数据为JSON格式""" - try: - # 网格化 - mesh = BRepMesh_IncrementalMesh(shape, 0.1) - mesh.Perform() + while explorer.More(): + face = TopoDS_Face(explorer.Current()) + surface = BRepAdaptor_Surface(face) - # 提取顶点和面 - 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 + try: + if surface.GetType() == 0: + normal = surface.Plane().Position().Direction() + else: + bbox = Bnd_Box() + brepbndlib_Add(face, bbox) + normal = gp_Dir(0, 0, 1) - vertices = [] - faces = [] + face_normals.append(normal) + except Exception as e: + logger.debug(f"面法向量计算失败:{e}") - explorer = TopExp_Explorer(shape, TopAbs_FACE) - vertex_index = 0 + explorer.Next() - while explorer.More(): - # 使用 explorer.Current() 直接获取面 - face = explorer.Current() - location = TopLoc_Location() - triangulation = BRep_Tool.Triangulation(face, location) + if not face_normals: + return gp_Dir(0, 0, 1) - 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()) - ]) + 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) - # 提取三角形面 - 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)]) + 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) - vertex_index += nb_nodes + def _create_optimal_parting_plane(self, shape: Any, analysis: Dict, + direction: gp_Dir) -> gp_Pln: + """ + 创建最优分型面 - explorer.Next() + Args: + shape: 产品形状 + analysis: 几何分析结果 + direction: 分型方向(法向量) - vertex_count = len(vertices) // 3 - face_count = len(faces) // 3 + Returns: + gp_Pln: 分型面方程 + """ + bbox = analysis["bounding_box"] + center = bbox["center"] - return { - "type": shape_type, - "vertices": vertices, - "faces": faces, - "vertex_count": vertex_count, - "face_count": face_count, - "triangulation": "BRepMesh三角化" - } + parting_plane = gp_Pln( + gp_Pnt(center[0], center[1], center[2]), + direction + ) - 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)}" - } + 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 diff --git a/src/core/mold_machining.py b/src/core/mold_machining.py new file mode 100644 index 0000000..648a66b --- /dev/null +++ b/src/core/mold_machining.py @@ -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 diff --git a/src/core/mold_quality_inspector.py b/src/core/mold_quality_inspector.py index 6b14636..2b9efda 100644 --- a/src/core/mold_quality_inspector.py +++ b/src/core/mold_quality_inspector.py @@ -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 diff --git a/src/core/mold_system_designer.py b/src/core/mold_system_designer.py new file mode 100644 index 0000000..8288bf2 --- /dev/null +++ b/src/core/mold_system_designer.py @@ -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)) diff --git a/src/core/side_action_designer.py b/src/core/side_action_designer.py new file mode 100644 index 0000000..2e60f1b --- /dev/null +++ b/src/core/side_action_designer.py @@ -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 diff --git a/src/models/database.py b/src/models/database.py index cc89e7a..93d9f23 100644 --- a/src/models/database.py +++ b/src/models/database.py @@ -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) @@ -39,7 +40,11 @@ class User(Base): if perm.code == permission_code: 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"" @@ -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") diff --git a/static/index.html b/static/index.html index 7b645ad..d8b4cd2 100644 --- a/static/index.html +++ b/static/index.html @@ -62,7 +62,7 @@ })(); - +
diff --git a/static/style.css b/static/style.css index 858ad7a..ffcb019 100644 --- a/static/style.css +++ b/static/style.css @@ -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 { diff --git a/static/vue-app.js b/static/vue-app.js index e93c2f9..52fe304 100644 --- a/static/vue-app.js +++ b/static/vue-app.js @@ -1260,8 +1260,40 @@ const ResultView = { const isFoamMaterial = (material) => { return material === 'aluminum_foam'; }; + + 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 }; + return { state, formatFileSize, formatDateTime, formatNumber, getPriorityText, isFoamMaterial, exportCAD }; }, template: `
@@ -1284,6 +1316,20 @@ const ResultView = { {{ state.task.status }} +
+ + + + +