init
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -16,6 +16,8 @@ __pycache__/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
1panel.env
|
||||
*.env.local
|
||||
logs/
|
||||
uploads/
|
||||
html_output/
|
||||
|
||||
+6
-2
@@ -67,13 +67,17 @@ class Settings:
|
||||
self.DB_PASSWORD = db_password
|
||||
|
||||
# JWT配置
|
||||
self.SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production')
|
||||
self.SECRET_KEY = os.getenv('SECRET_KEY')
|
||||
if not self.SECRET_KEY:
|
||||
raise ValueError("SECRET_KEY 未设置,请在.env文件中配置安全的密钥")
|
||||
self.ALGORITHM = os.getenv('ALGORITHM', 'HS256')
|
||||
self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', '1440'))
|
||||
|
||||
# 管理员账户配置
|
||||
self.ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin')
|
||||
self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123')
|
||||
self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
|
||||
if not self.ADMIN_PASSWORD:
|
||||
raise ValueError("ADMIN_PASSWORD 未设置,请在.env文件中配置管理员密码")
|
||||
self.ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@gemold.com')
|
||||
self.ADMIN_FULL_NAME = os.getenv('ADMIN_FULL_NAME', '系统管理员')
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
# 几何处理核心
|
||||
# ============================================
|
||||
# PythonOCC - CAD几何处理
|
||||
# pythonocc-core>=7.7.0
|
||||
pythonocc-core>=7.7.0
|
||||
# 网格处理
|
||||
trimesh>=3.21.0
|
||||
# 科学计算
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Literal
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
TxnType = Literal["receipt", "payment"]
|
||||
@@ -12,11 +13,11 @@ TxnStatus = Literal["confirmed", "voided"]
|
||||
class FinanceAllocationCreate(BaseModel):
|
||||
order_type: OrderType
|
||||
order_id: int
|
||||
allocated_amount: float = Field(gt=0)
|
||||
allocated_amount: Decimal = Field(gt=0)
|
||||
|
||||
|
||||
class FinanceTransactionCreate(BaseModel):
|
||||
amount: float = Field(gt=0)
|
||||
amount: Decimal = Field(gt=0)
|
||||
txn_date: Optional[datetime] = None
|
||||
method: str = "bank"
|
||||
account_name: Optional[str] = None
|
||||
@@ -36,21 +37,19 @@ class FinanceAllocationResponse(BaseModel):
|
||||
id: int
|
||||
order_type: str
|
||||
order_id: int
|
||||
allocated_amount: float
|
||||
allocated_amount: Decimal
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
|
||||
|
||||
class FinanceTransactionResponse(BaseModel):
|
||||
id: int
|
||||
txn_no: str
|
||||
txn_type: TxnType
|
||||
partner_type: PartnerType
|
||||
partner_id: int
|
||||
amount: float
|
||||
amount: Decimal
|
||||
txn_date: datetime
|
||||
method: str
|
||||
account_name: Optional[str]
|
||||
@@ -64,15 +63,15 @@ class FinanceTransactionResponse(BaseModel):
|
||||
|
||||
|
||||
class FinanceSummaryResponse(BaseModel):
|
||||
receivable_total: float
|
||||
payable_total: float
|
||||
monthly_receipt_total: float
|
||||
monthly_payment_total: float
|
||||
receivable_total: Decimal
|
||||
payable_total: Decimal
|
||||
monthly_receipt_total: Decimal
|
||||
monthly_payment_total: Decimal
|
||||
selected_year: int
|
||||
selected_quarter: Optional[int] = None
|
||||
period_label: str
|
||||
period_receipt_total: float = 0
|
||||
period_payment_total: float = 0
|
||||
period_receipt_total: Decimal = Decimal("0")
|
||||
period_payment_total: Decimal = Decimal("0")
|
||||
overdue_receivable_count: int = 0
|
||||
overdue_payable_count: int = 0
|
||||
|
||||
@@ -83,9 +82,9 @@ class ReceivableItemResponse(BaseModel):
|
||||
customer_id: int
|
||||
customer_name: str
|
||||
order_date: datetime
|
||||
total_amount: float
|
||||
received_amount: float
|
||||
receivable_amount: float
|
||||
total_amount: Decimal
|
||||
received_amount: Decimal
|
||||
receivable_amount: Decimal
|
||||
status: str
|
||||
|
||||
|
||||
@@ -95,9 +94,9 @@ class PayableItemResponse(BaseModel):
|
||||
supplier_id: int
|
||||
supplier_name: str
|
||||
order_date: datetime
|
||||
total_amount: float
|
||||
paid_amount: float
|
||||
payable_amount: float
|
||||
total_amount: Decimal
|
||||
paid_amount: Decimal
|
||||
payable_amount: Decimal
|
||||
status: str
|
||||
|
||||
|
||||
@@ -106,10 +105,10 @@ class PartnerStatementItemResponse(BaseModel):
|
||||
partner_name: str
|
||||
order_count: int
|
||||
transaction_count: int
|
||||
order_total: float
|
||||
settled_total: float
|
||||
transaction_total: float
|
||||
outstanding_total: float
|
||||
order_total: Decimal
|
||||
settled_total: Decimal
|
||||
transaction_total: Decimal
|
||||
outstanding_total: Decimal
|
||||
period_year: int
|
||||
period_quarter: Optional[int] = None
|
||||
|
||||
@@ -119,10 +118,10 @@ class FinancePartnerStatementResponse(BaseModel):
|
||||
year: int
|
||||
quarter: Optional[int] = None
|
||||
period_label: str
|
||||
order_total: float
|
||||
settled_total: float
|
||||
transaction_total: float
|
||||
outstanding_total: float
|
||||
order_total: Decimal
|
||||
settled_total: Decimal
|
||||
transaction_total: Decimal
|
||||
outstanding_total: Decimal
|
||||
items: List[PartnerStatementItemResponse] = []
|
||||
|
||||
|
||||
@@ -133,10 +132,10 @@ class PartnerProductStatementItemResponse(BaseModel):
|
||||
product_sku: Optional[str] = None
|
||||
product_name: str
|
||||
order_count: int
|
||||
order_quantity: float
|
||||
order_amount: float
|
||||
settled_amount: float
|
||||
outstanding_amount: float
|
||||
order_quantity: Decimal
|
||||
order_amount: Decimal
|
||||
settled_amount: Decimal
|
||||
outstanding_amount: Decimal
|
||||
period_year: int
|
||||
period_quarter: Optional[int] = None
|
||||
|
||||
@@ -147,7 +146,7 @@ class FinancePartnerProductStatementResponse(BaseModel):
|
||||
quarter: Optional[int] = None
|
||||
period_label: str
|
||||
partner_id: Optional[int] = None
|
||||
order_amount_total: float
|
||||
settled_amount_total: float
|
||||
outstanding_amount_total: float
|
||||
order_amount_total: Decimal
|
||||
settled_amount_total: Decimal
|
||||
outstanding_amount_total: Decimal
|
||||
items: List[PartnerProductStatementItemResponse] = []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class InventoryResponse(BaseModel):
|
||||
@@ -9,9 +10,9 @@ class InventoryResponse(BaseModel):
|
||||
product_sku: str
|
||||
warehouse_id: int
|
||||
warehouse_name: str
|
||||
quantity: int
|
||||
locked_quantity: int
|
||||
available_quantity: int
|
||||
quantity: Decimal
|
||||
locked_quantity: Decimal
|
||||
available_quantity: Decimal
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -20,14 +21,14 @@ class InventoryResponse(BaseModel):
|
||||
class InventoryCreate(BaseModel):
|
||||
product_id: int
|
||||
warehouse_id: int
|
||||
quantity: int = 0
|
||||
locked_quantity: int = 0
|
||||
quantity: Decimal = Decimal("0")
|
||||
locked_quantity: Decimal = Decimal("0")
|
||||
batch_number: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
|
||||
|
||||
class InventoryUpdate(BaseModel):
|
||||
quantity: Optional[int] = None
|
||||
locked_quantity: Optional[int] = None
|
||||
quantity: Optional[Decimal] = None
|
||||
locked_quantity: Optional[Decimal] = None
|
||||
batch_number: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
@@ -10,8 +11,8 @@ class ProductCreate(BaseModel):
|
||||
category: Optional[str] = None
|
||||
unit: str = "件"
|
||||
item_type: str = "finished"
|
||||
cost_price: float = 0
|
||||
sale_price: float = 0
|
||||
cost_price: Decimal = Decimal("0")
|
||||
sale_price: Decimal = Decimal("0")
|
||||
min_stock: int = 0
|
||||
max_stock: int = 1000
|
||||
|
||||
@@ -24,11 +25,11 @@ class ProductResponse(BaseModel):
|
||||
category: Optional[str]
|
||||
unit: str
|
||||
item_type: str
|
||||
cost_price: float
|
||||
sale_price: float
|
||||
cost_price: Decimal
|
||||
sale_price: Decimal
|
||||
min_stock: int
|
||||
max_stock: int
|
||||
material_cost: float = 0
|
||||
material_cost: Decimal = Decimal("0")
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
@@ -38,7 +39,7 @@ class ProductResponse(BaseModel):
|
||||
|
||||
class ProductMaterialItemUpdate(BaseModel):
|
||||
material_id: int
|
||||
quantity: float
|
||||
quantity: Decimal
|
||||
|
||||
|
||||
class ProductBOMUpdate(BaseModel):
|
||||
@@ -49,13 +50,13 @@ class ProductMaterialItemResponse(BaseModel):
|
||||
material_id: int
|
||||
material_sku: str
|
||||
material_name: str
|
||||
quantity: float
|
||||
unit_cost: float
|
||||
line_cost: float
|
||||
quantity: Decimal
|
||||
unit_cost: Decimal
|
||||
line_cost: Decimal
|
||||
|
||||
|
||||
class ProductBOMResponse(BaseModel):
|
||||
product_id: int
|
||||
product_name: str
|
||||
total_material_cost: float
|
||||
total_material_cost: Decimal
|
||||
items: List[ProductMaterialItemResponse]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class PurchaseOrderItemCreate(BaseModel):
|
||||
product_id: int
|
||||
quantity: int = Field(..., gt=0, description="采购数量")
|
||||
unit_price: Optional[float] = Field(None, description="单价(可选,后端自动使用物料成本价格)")
|
||||
unit_price: Optional[Decimal] = Field(None, description="单价(可选,后端自动使用物料成本价格)")
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
@@ -24,8 +25,8 @@ class PurchaseOrderResponse(BaseModel):
|
||||
order_date: datetime
|
||||
expected_date: Optional[date]
|
||||
status: str
|
||||
total_amount: float
|
||||
paid_amount: float
|
||||
total_amount: Decimal
|
||||
paid_amount: Decimal
|
||||
remark: Optional[str]
|
||||
created_at: datetime
|
||||
received_date: Optional[datetime]
|
||||
@@ -42,8 +43,8 @@ class PurchaseOrderItemResponse(BaseModel):
|
||||
product_name: str
|
||||
quantity: int
|
||||
received_quantity: int
|
||||
unit_price: float
|
||||
amount: float
|
||||
unit_price: Decimal
|
||||
amount: Decimal
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class SalesOrderItemCreate(BaseModel):
|
||||
@@ -10,7 +11,7 @@ class SalesOrderItemCreate(BaseModel):
|
||||
product_category: Optional[str] = None
|
||||
product_unit: Optional[str] = "件"
|
||||
quantity: int = Field(gt=0)
|
||||
unit_price: float = Field(ge=0)
|
||||
unit_price: Decimal = Field(ge=0)
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
@@ -33,10 +34,10 @@ class SalesOrderResponse(BaseModel):
|
||||
status: str
|
||||
production_status: str
|
||||
production_no: Optional[str]
|
||||
planned_material_cost: float
|
||||
actual_material_cost: float
|
||||
total_amount: float
|
||||
received_amount: float
|
||||
planned_material_cost: Decimal
|
||||
actual_material_cost: Decimal
|
||||
total_amount: Decimal
|
||||
received_amount: Decimal
|
||||
remark: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
@@ -49,8 +50,8 @@ class SalesOrderItemResponse(BaseModel):
|
||||
product_id: int
|
||||
quantity: int
|
||||
delivered_quantity: int
|
||||
unit_price: float
|
||||
amount: float
|
||||
unit_price: Decimal
|
||||
amount: Decimal
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
@@ -63,11 +64,11 @@ class ProductionMaterialPlanItemResponse(BaseModel):
|
||||
material_id: int
|
||||
material_sku: str
|
||||
material_name: str
|
||||
required_quantity: int
|
||||
available_quantity: int
|
||||
shortage_quantity: int
|
||||
unit_cost: float
|
||||
required_cost: float
|
||||
required_quantity: Decimal
|
||||
available_quantity: Decimal
|
||||
shortage_quantity: Decimal
|
||||
unit_cost: Decimal
|
||||
required_cost: Decimal
|
||||
|
||||
|
||||
class SalesOrderProductionPlanResponse(BaseModel):
|
||||
@@ -75,7 +76,7 @@ class SalesOrderProductionPlanResponse(BaseModel):
|
||||
order_no: str
|
||||
customer_name: str
|
||||
production_no: str
|
||||
planned_material_cost: float
|
||||
planned_material_cost: Decimal
|
||||
items: List[ProductionMaterialPlanItemResponse]
|
||||
|
||||
|
||||
@@ -90,10 +91,10 @@ class SalesOrderIssueResponse(BaseModel):
|
||||
order_no: str
|
||||
production_no: str
|
||||
movement_count: int
|
||||
planned_material_cost: float
|
||||
actual_material_cost: float
|
||||
cost_deviation: float
|
||||
cost_deviation_rate: float
|
||||
planned_material_cost: Decimal
|
||||
actual_material_cost: Decimal
|
||||
cost_deviation: Decimal
|
||||
cost_deviation_rate: Decimal
|
||||
production_status: str
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class StockMovementCreate(BaseModel):
|
||||
@@ -8,8 +9,8 @@ class StockMovementCreate(BaseModel):
|
||||
product_sku: Optional[str] = None
|
||||
warehouse_id: int
|
||||
movement_type: str
|
||||
quantity: int
|
||||
unit_price: Optional[float] = None
|
||||
quantity: Decimal
|
||||
unit_price: Optional[Decimal] = None
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
@@ -19,9 +20,9 @@ class StockMovementResponse(BaseModel):
|
||||
product_sku: Optional[str]
|
||||
product_name: str
|
||||
movement_type: str
|
||||
quantity: int
|
||||
before_quantity: int
|
||||
after_quantity: int
|
||||
quantity: Decimal
|
||||
before_quantity: Decimal
|
||||
after_quantity: Decimal
|
||||
reference_no: Optional[str]
|
||||
remark: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
+353
-1
@@ -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 # 添加验证结果
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
AI 分型面检测模块 - 基于 GNN 的分型面预测框架
|
||||
|
||||
架构设计:
|
||||
1. ShapeGraphBuilder - 将 OCC 形状转换为图表示(面为节点,共享边为图边)
|
||||
2. PartingSurfaceGNN - 图神经网络模型定义
|
||||
3. AIPartingSurfaceDetectorV2 - 增强版分型面检测器(集成 GNN)
|
||||
|
||||
图构建策略:
|
||||
- 节点:每个 TopoDS_Face 作为一个节点
|
||||
- 节点特征:法向量(3) + 面积(1) + 曲率(2) + 面类型(1) = 7维
|
||||
- 边:共享 TopoDS_Edge 的面之间建立边
|
||||
- 边特征:共享边长度(1) + 二面角(1) = 2维
|
||||
|
||||
GNN 模型:
|
||||
- 3层 GraphConv + 全局池化 + MLP 分类头
|
||||
- 输出:每个面的分型面归属概率 + 分型方向
|
||||
|
||||
依赖:
|
||||
- PyTorch + PyTorch Geometric(可选,缺失时回退到几何方法)
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import numpy as np
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_TORCH_AVAILABLE = False
|
||||
_TORCH_GEOMETRIC_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
_TORCH_AVAILABLE = True
|
||||
try:
|
||||
from torch_geometric.nn import GCNConv, global_mean_pool
|
||||
from torch_geometric.data import Data
|
||||
_TORCH_GEOMETRIC_AVAILABLE = True
|
||||
except ImportError:
|
||||
logger.info("PyTorch Geometric 未安装,GNN 模型不可用")
|
||||
except ImportError:
|
||||
logger.info("PyTorch 未安装,AI 分型面检测将使用几何回退方法")
|
||||
|
||||
|
||||
class ShapeGraphBuilder:
|
||||
"""将 OCC 形状转换为图表示"""
|
||||
|
||||
def build_graph(self, shape: Any) -> Optional[Dict]:
|
||||
"""
|
||||
从 OCC 形状构建图数据
|
||||
|
||||
Returns:
|
||||
{
|
||||
"node_features": np.ndarray (N, 7),
|
||||
"edge_index": np.ndarray (2, E),
|
||||
"edge_features": np.ndarray (E, 2),
|
||||
"face_map": List[TopoDS_Face],
|
||||
"num_nodes": int,
|
||||
"num_edges": int
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib_Add
|
||||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
||||
from OCC.Core.TopExp import topexp_MapShapesAndAncestors
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge
|
||||
|
||||
faces = []
|
||||
face_features = []
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
face = TopoDS_Face(explorer.Current())
|
||||
features = self._extract_face_features(face)
|
||||
if features is not None:
|
||||
faces.append(face)
|
||||
face_features.append(features)
|
||||
explorer.Next()
|
||||
|
||||
if not faces:
|
||||
logger.warning("未找到面,无法构建图")
|
||||
return None
|
||||
|
||||
node_features = np.array(face_features, dtype=np.float32)
|
||||
|
||||
edge_map = TopTools_IndexedDataMapOfShapeListOfShape()
|
||||
topexp_MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_map)
|
||||
|
||||
edge_list = []
|
||||
edge_features_list = []
|
||||
|
||||
for i in range(1, edge_map.Extent() + 1):
|
||||
edge = TopoDS_Edge(edge_map.FindKey(i))
|
||||
face_list = edge_map.FindFromIndex(i)
|
||||
|
||||
connected_faces = []
|
||||
it = face_list.begin()
|
||||
while it != face_list.end():
|
||||
f = TopoDS_Face(it.Value())
|
||||
try:
|
||||
idx = faces.index(f)
|
||||
connected_faces.append(idx)
|
||||
except ValueError:
|
||||
pass
|
||||
it.next_ptr()
|
||||
|
||||
if len(connected_faces) >= 2:
|
||||
edge_feat = self._extract_edge_features(edge, connected_faces, faces)
|
||||
for j in range(len(connected_faces)):
|
||||
for k in range(j + 1, len(connected_faces)):
|
||||
edge_list.append([connected_faces[j], connected_faces[k]])
|
||||
edge_features_list.append(edge_feat)
|
||||
|
||||
if not edge_list:
|
||||
logger.warning("未找到边连接,返回无图边的图")
|
||||
edge_index = np.zeros((2, 0), dtype=np.int64)
|
||||
edge_features_arr = np.zeros((0, 2), dtype=np.float32)
|
||||
else:
|
||||
edge_index = np.array(edge_list, dtype=np.int64).T
|
||||
rev_edges = np.array([[e[1], e[0]] for e in edge_list], dtype=np.int64).T
|
||||
edge_index = np.concatenate([edge_index, rev_edges], axis=1)
|
||||
edge_features_arr = np.array(edge_features_list, dtype=np.float32)
|
||||
edge_features_arr = np.concatenate([edge_features_arr, edge_features_arr], axis=0)
|
||||
|
||||
return {
|
||||
"node_features": node_features,
|
||||
"edge_index": edge_index,
|
||||
"edge_features": edge_features_arr,
|
||||
"face_map": faces,
|
||||
"num_nodes": len(faces),
|
||||
"num_edges": edge_index.shape[1]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"图构建失败: {e}")
|
||||
return None
|
||||
|
||||
def _extract_face_features(self, face: Any) -> Optional[np.ndarray]:
|
||||
"""
|
||||
提取面特征:[nx, ny, nz, area, u_curvature, v_curvature, face_type]
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
if surface.GetType() == 0:
|
||||
normal = surface.Plane().Position().Direction()
|
||||
face_type = 0.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
elif surface.GetType() == 1:
|
||||
normal = surface.Cylinder().Position().Direction()
|
||||
face_type = 1.0
|
||||
radius = surface.Cylinder().Radius()
|
||||
u_curv = 1.0 / radius if radius > 0.001 else 0.0
|
||||
v_curv = 0.0
|
||||
elif surface.GetType() == 2:
|
||||
normal = surface.Cone().Position().Direction()
|
||||
face_type = 2.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
elif surface.GetType() == 3:
|
||||
normal = surface.Sphere().Position().Direction()
|
||||
face_type = 3.0
|
||||
radius = surface.Sphere().Radius()
|
||||
u_curv = 1.0 / radius if radius > 0.001 else 0.0
|
||||
v_curv = 1.0 / radius if radius > 0.001 else 0.0
|
||||
elif surface.GetType() == 4:
|
||||
normal = surface.Torus().Position().Direction()
|
||||
face_type = 4.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
else:
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
props = BRepLProp_SLProps(surface, 2, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
normal = props.Normal()
|
||||
else:
|
||||
normal = gp_Dir(0, 0, 1)
|
||||
face_type = 5.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
|
||||
face_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, face_props)
|
||||
area = face_props.Mass()
|
||||
|
||||
return np.array([
|
||||
normal.X(), normal.Y(), normal.Z(),
|
||||
area,
|
||||
u_curv, v_curv,
|
||||
face_type
|
||||
], dtype=np.float32)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"面特征提取失败: {e}")
|
||||
return None
|
||||
|
||||
def _extract_edge_features(self, edge: Any, connected_faces: List[int],
|
||||
faces: List) -> np.ndarray:
|
||||
"""
|
||||
提取边特征:[edge_length, dihedral_angle]
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
first = curve.FirstParameter()
|
||||
last = curve.LastParameter()
|
||||
|
||||
edge_len = abs(last - first)
|
||||
|
||||
dihedral = 0.0
|
||||
if len(connected_faces) >= 2:
|
||||
n1 = self._get_face_normal_fast(faces[connected_faces[0]])
|
||||
n2 = self._get_face_normal_fast(faces[connected_faces[1]])
|
||||
if n1 is not None and n2 is not None:
|
||||
dot = np.clip(np.dot(n1, n2), -1.0, 1.0)
|
||||
dihedral = np.arccos(dot)
|
||||
|
||||
return np.array([edge_len, dihedral], dtype=np.float32)
|
||||
|
||||
except Exception:
|
||||
return np.array([0.0, 0.0], dtype=np.float32)
|
||||
|
||||
def _get_face_normal_fast(self, face: Any) -> Optional[np.ndarray]:
|
||||
"""快速获取面法向量(numpy数组)"""
|
||||
try:
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
if surface.GetType() == 0:
|
||||
n = surface.Plane().Position().Direction()
|
||||
return np.array([n.X(), n.Y(), n.Z()])
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
if _TORCH_GEOMETRIC_AVAILABLE:
|
||||
|
||||
class PartingSurfaceGNN(nn.Module):
|
||||
"""
|
||||
分型面检测 GNN 模型
|
||||
|
||||
架构:
|
||||
- 3层 GCNConv (hidden_dim=64)
|
||||
- 全局平均池化
|
||||
- 3层 MLP 分类头
|
||||
- 输出:每个面的分型面归属概率 (0-1)
|
||||
"""
|
||||
|
||||
def __init__(self, input_dim: int = 7, hidden_dim: int = 64,
|
||||
num_layers: int = 3, dropout: float = 0.3):
|
||||
super().__init__()
|
||||
|
||||
self.input_dim = input_dim
|
||||
self.hidden_dim = hidden_dim
|
||||
self.num_layers = num_layers
|
||||
|
||||
self.input_proj = nn.Linear(input_dim, hidden_dim)
|
||||
|
||||
self.convs = nn.ModuleList()
|
||||
self.bns = nn.ModuleList()
|
||||
for _ in range(num_layers):
|
||||
self.convs.append(GCNConv(hidden_dim, hidden_dim))
|
||||
self.bns.append(nn.BatchNorm1d(hidden_dim))
|
||||
|
||||
self.dropout = dropout
|
||||
|
||||
self.mlp = nn.Sequential(
|
||||
nn.Linear(hidden_dim, hidden_dim),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(hidden_dim, hidden_dim // 2),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(hidden_dim // 2, 1),
|
||||
)
|
||||
|
||||
def forward(self, data: Data) -> torch.Tensor:
|
||||
x, edge_index = data.x, data.edge_index
|
||||
|
||||
x = self.input_proj(x)
|
||||
x = F.relu(x)
|
||||
|
||||
for conv, bn in zip(self.convs, self.bns):
|
||||
x = conv(x, edge_index)
|
||||
x = bn(x)
|
||||
x = F.relu(x)
|
||||
x = F.dropout(x, p=self.dropout, training=self.training)
|
||||
|
||||
out = self.mlp(x)
|
||||
return torch.sigmoid(out).squeeze(-1)
|
||||
|
||||
class PartingDirectionHead(nn.Module):
|
||||
"""
|
||||
分型方向预测头
|
||||
|
||||
基于全局池化的面特征,预测分型方向向量
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_dim: int = 64):
|
||||
super().__init__()
|
||||
self.direction_mlp = nn.Sequential(
|
||||
nn.Linear(hidden_dim, hidden_dim),
|
||||
nn.ReLU(),
|
||||
nn.Linear(hidden_dim, 3),
|
||||
)
|
||||
|
||||
def forward(self, node_embeddings: torch.Tensor,
|
||||
batch: torch.Tensor) -> torch.Tensor:
|
||||
pooled = global_mean_pool(node_embeddings, batch)
|
||||
direction = self.direction_mlp(pooled)
|
||||
direction = F.normalize(direction, p=2, dim=-1)
|
||||
return direction
|
||||
|
||||
|
||||
class AIPartingSurfaceDetectorV2:
|
||||
"""
|
||||
增强版 AI 分型面检测器
|
||||
|
||||
支持:
|
||||
1. GNN 模型推理(需要 PyTorch + PyG)
|
||||
2. 几何方法回退(无需任何 AI 依赖)
|
||||
3. 模型训练数据收集
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: Optional[str] = None,
|
||||
use_gnn: bool = True,
|
||||
device: str = "cpu"):
|
||||
self.model = None
|
||||
self.direction_head = None
|
||||
self.graph_builder = ShapeGraphBuilder()
|
||||
self.device = device
|
||||
self.use_gnn = use_gnn and _TORCH_GEOMETRIC_AVAILABLE
|
||||
|
||||
if model_path and self.use_gnn:
|
||||
self._load_model(model_path)
|
||||
|
||||
def _load_model(self, model_path: str):
|
||||
"""加载训练好的 GNN 模型"""
|
||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
||||
logger.warning("PyTorch Geometric 不可用,无法加载 GNN 模型")
|
||||
return
|
||||
|
||||
try:
|
||||
checkpoint = torch.load(model_path, map_location=self.device)
|
||||
self.model = PartingSurfaceGNN(
|
||||
input_dim=checkpoint.get("input_dim", 7),
|
||||
hidden_dim=checkpoint.get("hidden_dim", 64),
|
||||
)
|
||||
self.model.load_state_dict(checkpoint["model_state_dict"])
|
||||
self.model.to(self.device)
|
||||
self.model.eval()
|
||||
|
||||
if "direction_head_state_dict" in checkpoint:
|
||||
self.direction_head = PartingDirectionHead(
|
||||
hidden_dim=checkpoint.get("hidden_dim", 64)
|
||||
)
|
||||
self.direction_head.load_state_dict(checkpoint["direction_head_state_dict"])
|
||||
self.direction_head.to(self.device)
|
||||
self.direction_head.eval()
|
||||
|
||||
logger.info(f"GNN 模型加载成功: {model_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"GNN 模型加载失败: {e}")
|
||||
self.model = None
|
||||
|
||||
def detect(self, product_shape: Any, analysis: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
检测最优分型面
|
||||
|
||||
Args:
|
||||
product_shape: OpenCASCADE 形状对象
|
||||
analysis: 几何分析结果
|
||||
|
||||
Returns:
|
||||
{
|
||||
"origin": [x, y, z],
|
||||
"normal": [nx, ny, nz],
|
||||
"confidence": float,
|
||||
"parting_line": [...],
|
||||
"method": "gnn" | "geometric"
|
||||
}
|
||||
"""
|
||||
if self.use_gnn and self.model is not None:
|
||||
result = self._detect_with_gnn(product_shape, analysis)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return self._detect_with_geometry(product_shape, analysis)
|
||||
|
||||
def _detect_with_gnn(self, shape: Any, analysis: Dict) -> Optional[Dict]:
|
||||
"""使用 GNN 模型检测分型面"""
|
||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
||||
return None
|
||||
|
||||
try:
|
||||
graph_data = self.graph_builder.build_graph(shape)
|
||||
if graph_data is None:
|
||||
return None
|
||||
|
||||
node_features = torch.tensor(
|
||||
graph_data["node_features"], dtype=torch.float32
|
||||
).to(self.device)
|
||||
edge_index = torch.tensor(
|
||||
graph_data["edge_index"], dtype=torch.long
|
||||
).to(self.device)
|
||||
|
||||
data = Data(x=node_features, edge_index=edge_index)
|
||||
|
||||
with torch.no_grad():
|
||||
face_probs = self.model(data)
|
||||
|
||||
if self.direction_head is not None:
|
||||
batch = torch.zeros(
|
||||
data.num_nodes, dtype=torch.long, device=self.device
|
||||
)
|
||||
direction = self.direction_head(data.x, batch)
|
||||
normal = direction.cpu().numpy().tolist()
|
||||
else:
|
||||
normal = [0, 0, 1]
|
||||
|
||||
parting_face_mask = face_probs.cpu().numpy() > 0.5
|
||||
confidence = float(face_probs.mean().cpu().numpy())
|
||||
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
|
||||
return {
|
||||
"origin": center,
|
||||
"normal": normal,
|
||||
"confidence": confidence,
|
||||
"method": "gnn",
|
||||
"face_probabilities": face_probs.cpu().numpy().tolist(),
|
||||
"parting_face_count": int(parting_face_mask.sum()),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"GNN 检测失败,回退到几何方法: {e}")
|
||||
return None
|
||||
|
||||
def _detect_with_geometry(self, shape: Any, analysis: Dict) -> Dict:
|
||||
"""几何方法回退:基于法向量统计的分型面检测"""
|
||||
try:
|
||||
graph_data = self.graph_builder.build_graph(shape)
|
||||
if graph_data is not None:
|
||||
node_features = graph_data["node_features"]
|
||||
normals = node_features[:, :3]
|
||||
areas = node_features[:, 3]
|
||||
|
||||
total_area = areas.sum()
|
||||
if total_area > 0:
|
||||
weights = areas / total_area
|
||||
weighted_normal = np.sum(normals * weights[:, np.newaxis], axis=0)
|
||||
else:
|
||||
weighted_normal = np.mean(normals, axis=0)
|
||||
|
||||
length = np.linalg.norm(weighted_normal)
|
||||
if length > 0.001:
|
||||
weighted_normal /= length
|
||||
else:
|
||||
weighted_normal = np.array([0, 0, 1])
|
||||
|
||||
dot_products = np.abs(np.dot(normals, weighted_normal))
|
||||
confidence = float(np.mean(dot_products))
|
||||
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
|
||||
return {
|
||||
"origin": center,
|
||||
"normal": weighted_normal.tolist(),
|
||||
"confidence": confidence,
|
||||
"method": "geometric",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"几何方法检测失败: {e}")
|
||||
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
return {
|
||||
"origin": center,
|
||||
"normal": [0, 0, 1],
|
||||
"confidence": 0.5,
|
||||
"method": "fallback",
|
||||
}
|
||||
|
||||
def collect_training_sample(self, shape: Any, analysis: Dict,
|
||||
ground_truth_normal: List[float],
|
||||
ground_truth_origin: List[float]) -> Optional[Dict]:
|
||||
"""
|
||||
收集训练样本
|
||||
|
||||
Args:
|
||||
shape: OCC 形状
|
||||
analysis: 几何分析
|
||||
ground_truth_normal: 人工标注的分型方向
|
||||
ground_truth_origin: 人工标注的分型面原点
|
||||
|
||||
Returns:
|
||||
可序列化的训练样本
|
||||
"""
|
||||
graph_data = self.graph_builder.build_graph(shape)
|
||||
if graph_data is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"node_features": graph_data["node_features"].tolist(),
|
||||
"edge_index": graph_data["edge_index"].tolist(),
|
||||
"edge_features": graph_data["edge_features"].tolist(),
|
||||
"label_normal": ground_truth_normal,
|
||||
"label_origin": ground_truth_origin,
|
||||
"bounding_box": analysis.get("bounding_box", {}),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_model(input_dim: int = 7, hidden_dim: int = 64,
|
||||
num_layers: int = 3) -> Optional[Any]:
|
||||
"""创建新的 GNN 模型实例"""
|
||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
||||
logger.warning("PyTorch Geometric 不可用,无法创建模型")
|
||||
return None
|
||||
return PartingSurfaceGNN(
|
||||
input_dim=input_dim,
|
||||
hidden_dim=hidden_dim,
|
||||
num_layers=num_layers,
|
||||
)
|
||||
+43
-367
@@ -5,43 +5,34 @@
|
||||
1. 改进的法向量分析 - 高斯权重、多点采样
|
||||
2. 多分型面检测 - 支持复杂产品
|
||||
3. 倒扣区域检测 - 自动识别
|
||||
4. 完整拔模角处理 - BRepOffsetAPI_DraftAngle
|
||||
5. 铝泡沫收缩补偿 - 基于发泡倍率
|
||||
6. 优化的型腔分离 - 精确布尔运算
|
||||
7. 模具块生成 - A/B板结构
|
||||
8. 分型线平滑处理 - B样条拟合
|
||||
4. 铝泡沫收缩补偿 - 基于发泡倍率
|
||||
5. 优化的型腔分离 - 精确布尔运算
|
||||
6. 模具块生成 - A/B板结构
|
||||
7. 分型线平滑处理 - B样条拟合
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
import numpy as np
|
||||
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
from OCC.Core.Geom import Geom_Plane
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Vec, gp_Trsf
|
||||
from OCC.Core.TopTools import TopTools_ListOfShape
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, TopoDS_Edge, TopoDS_Vertex
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
|
||||
from OCC.Core.TopoDS import TopoDS_Face
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib_Add
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
from models.schemas import create_mold_cavity_data, create_mold_key_info
|
||||
from utils.logger import get_logger
|
||||
from core.base_mold_generator import BaseMoldGenerator
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AluminumFoamMoldGenerator:
|
||||
class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
||||
"""铝制家电包装泡沫模具分模生成器"""
|
||||
|
||||
def __init__(self,
|
||||
@@ -58,12 +49,10 @@ class AluminumFoamMoldGenerator:
|
||||
material_density: 材料密度 g/cm³(铝泡沫 0.3-0.8)
|
||||
foam_material: 泡沫材料类型
|
||||
"""
|
||||
self.shrinkage_rate = shrinkage_rate
|
||||
self.draft_angle = draft_angle
|
||||
self.material_density = material_density
|
||||
super().__init__(shrinkage_rate, draft_angle, material_density)
|
||||
|
||||
self.foam_material = foam_material
|
||||
|
||||
# 铝泡沫材料数据库
|
||||
self.foam_materials = {
|
||||
"AlSi10Mg": {
|
||||
"density": 0.45,
|
||||
@@ -95,7 +84,6 @@ class AluminumFoamMoldGenerator:
|
||||
}
|
||||
}
|
||||
|
||||
# 塑料材料数据库(保留原有)
|
||||
self.plastic_materials = {
|
||||
"ABS": {"density": 1.05, "shrinkage": 0.005},
|
||||
"PP": {"density": 0.90, "shrinkage": 0.016},
|
||||
@@ -107,19 +95,13 @@ class AluminumFoamMoldGenerator:
|
||||
"PMMA": {"density": 1.18, "shrinkage": 0.004}
|
||||
}
|
||||
|
||||
# 分模参数
|
||||
self.parting_line_tolerance = 0.1
|
||||
self.max_draft_angle = 5.0
|
||||
self.min_draft_angle = 1.0
|
||||
|
||||
# 高级参数
|
||||
self.cavity_count = 1
|
||||
self.parting_precision = 0.1 # mm
|
||||
self.cavity_match_rate = 95.0 # %
|
||||
|
||||
# AI 模型接口
|
||||
self.ai_parting_detector = None
|
||||
self.ai_draft_analyzer = None
|
||||
self.parting_precision = 0.1
|
||||
self.cavity_match_rate = 95.0
|
||||
|
||||
def set_foam_material(self, material: str):
|
||||
"""设置铝泡沫材料"""
|
||||
@@ -160,30 +142,22 @@ class AluminumFoamMoldGenerator:
|
||||
logger.info(f"开始生成铝泡沫模具型腔 (材料: {self.foam_material})...")
|
||||
|
||||
try:
|
||||
# Step 1: 分析产品几何
|
||||
analysis = self._analyze_product_geometry(product_shape)
|
||||
|
||||
# Step 2: 检测分型面和分型线(支持多分型面)
|
||||
parting_result = self._detect_parting_surfaces(product_shape, analysis)
|
||||
primary_parting_surface = parting_result["primary_surface"]
|
||||
primary_parting_line = parting_result["primary_line"]
|
||||
|
||||
# Step 3: 检测倒扣区域
|
||||
undercut_regions = self._detect_undercut_regions(product_shape, primary_parting_surface)
|
||||
|
||||
# Step 4: 应用收缩率补偿
|
||||
scaled_shape = self._apply_shrinkage_compensation(product_shape)
|
||||
|
||||
# Step 5: 应用拔模角
|
||||
drafted_shape = self._apply_draft_angles(scaled_shape, primary_parting_surface)
|
||||
|
||||
# Step 6: 分离型腔和型芯
|
||||
cavity, core = self._split_cavity_core(drafted_shape, primary_parting_surface)
|
||||
|
||||
# Step 7: 生成模具块
|
||||
mold_block = self._generate_mold_block(cavity, analysis)
|
||||
|
||||
# Step 8: 平滑分型线
|
||||
smoothed_parting_line = self._smooth_parting_line(primary_parting_line)
|
||||
|
||||
logger.info("铝泡沫模具型腔生成完成")
|
||||
@@ -213,14 +187,11 @@ class AluminumFoamMoldGenerator:
|
||||
parting_surface = cavity_data["parting_surface"]
|
||||
analysis = cavity_data["analysis"]
|
||||
|
||||
# 提取几何数据
|
||||
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
|
||||
core_geometry = self._extract_shape_geometry(core, "core")
|
||||
|
||||
# 提取分型面数据
|
||||
parting_geometry = self._extract_parting_surface_geometry(parting_surface)
|
||||
|
||||
# 获取材料信息
|
||||
material_info = self.foam_materials.get(self.foam_material, {})
|
||||
|
||||
detailed_json = {
|
||||
@@ -305,40 +276,14 @@ class AluminumFoamMoldGenerator:
|
||||
# ==================== 核心算法实现 ====================
|
||||
|
||||
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
|
||||
"""分析产品几何属性"""
|
||||
# 体积属性
|
||||
volume_props = GProp_GProps()
|
||||
brepgprop.VolumeProperties(shape, volume_props)
|
||||
"""分析产品几何属性(扩展基类版本,增加法向量统计)"""
|
||||
result = super()._analyze_product_geometry(shape)
|
||||
result["normal_statistics"] = self._analyze_face_normals(shape)
|
||||
return result
|
||||
|
||||
# 表面积属性
|
||||
surface_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(shape, surface_props)
|
||||
|
||||
# 边界框
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
# 表面法向量统计
|
||||
normal_stats = self._analyze_face_normals(shape)
|
||||
|
||||
return {
|
||||
"volume": volume_props.Mass(),
|
||||
"surface_area": surface_props.Mass(),
|
||||
"center_of_mass": [
|
||||
volume_props.CentreOfMass().X(),
|
||||
volume_props.CentreOfMass().Y(),
|
||||
volume_props.CentreOfMass().Z()
|
||||
],
|
||||
"bounding_box": {
|
||||
"min": [xmin, ymin, zmin],
|
||||
"max": [xmax, ymax, zmax],
|
||||
"dimensions": [xmax - xmin, ymax - ymin, zmax - zmin],
|
||||
"center": [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2]
|
||||
},
|
||||
"normal_statistics": normal_stats,
|
||||
"inertia_matrix": self._get_inertia_matrix(volume_props)
|
||||
}
|
||||
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
|
||||
"""分离型腔和型芯(铝泡沫使用更大余量)"""
|
||||
return super()._split_cavity_core(shape, parting_surface, margin=25)
|
||||
|
||||
def _analyze_face_normals(self, shape: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -355,11 +300,9 @@ class AluminumFoamMoldGenerator:
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
# 获取面参数范围
|
||||
u_min, u_max = surface.FirstUParameter(), surface.LastUParameter()
|
||||
v_min, v_max = surface.FirstVParameter(), surface.LastVParameter()
|
||||
|
||||
# 多点采样计算法向量
|
||||
sample_points = 4
|
||||
normal_sum = np.array([0.0, 0.0, 0.0])
|
||||
|
||||
@@ -368,24 +311,21 @@ class AluminumFoamMoldGenerator:
|
||||
u = u_min + (u_max - u_min) * i / (sample_points - 1) if sample_points > 1 else (u_min + u_max) / 2
|
||||
v = v_min + (v_max - v_min) * j / (sample_points - 1) if sample_points > 1 else (v_min + v_max) / 2
|
||||
|
||||
if surface.GetType() == 0: # Plane
|
||||
if surface.GetType() == 0:
|
||||
normal = surface.Plane().Position().Direction()
|
||||
normal_sum += np.array([normal.X(), normal.Y(), normal.Z()])
|
||||
break
|
||||
if surface.GetType() == 0:
|
||||
break
|
||||
|
||||
# 获取面的中心点
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(face, bbox)
|
||||
center = bbox.Center()
|
||||
|
||||
# 获取面面积
|
||||
face_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, face_props)
|
||||
area = face_props.Mass()
|
||||
|
||||
# 归一化法向量
|
||||
length = np.linalg.norm(normal_sum)
|
||||
if length > 0.001:
|
||||
normal_sum /= length
|
||||
@@ -406,7 +346,6 @@ class AluminumFoamMoldGenerator:
|
||||
"face_count": 0
|
||||
}
|
||||
|
||||
# 使用面积作为权重计算加权平均法向量
|
||||
total_area = sum(face_areas)
|
||||
weighted_normal = np.array([0.0, 0.0, 0.0])
|
||||
|
||||
@@ -414,12 +353,10 @@ class AluminumFoamMoldGenerator:
|
||||
weight = face_areas[i] / total_area if total_area > 0 else 1.0 / len(face_normals)
|
||||
weighted_normal += normal * weight
|
||||
|
||||
# 归一化
|
||||
length = np.linalg.norm(weighted_normal)
|
||||
if length > 0.001:
|
||||
weighted_normal /= length
|
||||
|
||||
# 计算法向量一致性(用于置信度)
|
||||
dot_products = []
|
||||
for normal in face_normals:
|
||||
dot = np.dot(normal, weighted_normal)
|
||||
@@ -438,48 +375,38 @@ class AluminumFoamMoldGenerator:
|
||||
"""
|
||||
检测分型面(支持多分型面)
|
||||
"""
|
||||
# 1. 尝试 AI 模型
|
||||
if self.ai_parting_detector is not None:
|
||||
try:
|
||||
ai_result = self.ai_parting_detector.detect(shape, analysis)
|
||||
if ai_result:
|
||||
return self._create_parting_surface_from_ai(ai_result, analysis)
|
||||
return self._create_parting_surface_from_ai(ai_result, analysis, shape)
|
||||
except Exception as e:
|
||||
logger.warning(f"AI 分型面检测失败: {e}")
|
||||
|
||||
# 2. 法向量分析确定主方向
|
||||
normal_stats = analysis.get("normal_statistics", {})
|
||||
primary_direction = normal_stats.get("primary_direction", [0, 0, 1])
|
||||
|
||||
# 3. 创建主分型面
|
||||
bbox = analysis["bounding_box"]
|
||||
center = bbox["center"]
|
||||
|
||||
# 沿主方向创建分型面
|
||||
dir_obj = gp_Dir(primary_direction[0], primary_direction[1], primary_direction[2])
|
||||
parting_plane = gp_Pln(gp_Pnt(center[0], center[1], center[2]), dir_obj)
|
||||
|
||||
try:
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
except:
|
||||
# 回退到默认平面
|
||||
except Exception:
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, center[2]), gp_Dir(0, 0, 1))
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
# 4. 计算分型线
|
||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
||||
|
||||
# 5. 检测是否需要多分型面(基于产品复杂度)
|
||||
additional_surfaces = []
|
||||
|
||||
# 检查产品高度方向的比例
|
||||
dims = bbox["dimensions"]
|
||||
max_dim = max(dims)
|
||||
min_dim = min(dims)
|
||||
|
||||
# 如果产品非常扁平,可能需要水平分型面
|
||||
if max_dim / min_dim > 5:
|
||||
# 尝试添加垂直分型面
|
||||
vertical_plane = gp_Pln(gp_Pnt(center[0], center[1], center[2]), gp_Dir(1, 0, 0))
|
||||
try:
|
||||
vertical_surface = BRepBuilderAPI_MakeFace(vertical_plane).Face()
|
||||
@@ -488,7 +415,7 @@ class AluminumFoamMoldGenerator:
|
||||
"direction": [1, 0, 0],
|
||||
"reason": "产品扁平,需要垂直分型"
|
||||
})
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
@@ -507,11 +434,9 @@ class AluminumFoamMoldGenerator:
|
||||
undercut_regions = []
|
||||
|
||||
try:
|
||||
# 获取分型面法向量
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
parting_normal = surface.Plane().Position().Direction()
|
||||
|
||||
# 遍历所有面,检查是否存在倒扣
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
@@ -520,23 +445,19 @@ class AluminumFoamMoldGenerator:
|
||||
try:
|
||||
face_surface = BRepAdaptor_Surface(face)
|
||||
|
||||
if face_surface.GetType() == 0: # Plane
|
||||
if face_surface.GetType() == 0:
|
||||
face_normal = face_surface.Plane().Position().Direction()
|
||||
|
||||
# 计算与分型面法向量的夹角
|
||||
dot = (face_normal.X() * parting_normal.X() +
|
||||
face_normal.Y() * parting_normal.Y() +
|
||||
face_normal.Z() * parting_normal.Z())
|
||||
|
||||
# 如果夹角大于90度,认为是倒扣面(法线方向与分型面相反)
|
||||
if dot < -0.7: # 约>135度
|
||||
# 获取面的边界框中心
|
||||
if dot < -0.7:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(face, bbox)
|
||||
center = bbox.Center()
|
||||
|
||||
# 检查该区域是否在分型面下方
|
||||
if center.Z() < 0: # 简化判断
|
||||
if center.Z() < 0:
|
||||
undercut_regions.append({
|
||||
"type": "negative_draft",
|
||||
"location": [center.X(), center.Y(), center.Z()],
|
||||
@@ -555,69 +476,6 @@ class AluminumFoamMoldGenerator:
|
||||
|
||||
return undercut_regions
|
||||
|
||||
def _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]:
|
||||
"""计算真实的分型线"""
|
||||
try:
|
||||
section = BRepAlgoAPI_Section(shape, parting_surface)
|
||||
section.Build()
|
||||
|
||||
if not section.IsDone():
|
||||
logger.warning("截面运算未完成")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
# 提取交线点
|
||||
edges = []
|
||||
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
|
||||
|
||||
while explorer.More():
|
||||
edge = TopoDS_Edge(explorer.Current())
|
||||
|
||||
try:
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
first_param = curve.FirstParameter()
|
||||
last_param = curve.LastParameter()
|
||||
|
||||
# 采样点
|
||||
num_points = max(10, int((last_param - first_param) / 0.5))
|
||||
step = (last_param - first_param) / num_points
|
||||
|
||||
for i in range(num_points + 1):
|
||||
param = first_param + i * step
|
||||
point = curve.Value(param)
|
||||
edges.append([point.X(), point.Y(), point.Z()])
|
||||
except:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not edges:
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
logger.info(f"计算得到 {len(edges)} 个分型线点")
|
||||
return edges
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分型线计算失败: {e}")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
def _simple_parting_line(self, shape: Any) -> List[List[float]]:
|
||||
"""简化的分型线"""
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
center_z = (zmin + zmax) / 2
|
||||
|
||||
return [
|
||||
[xmin, ymin, center_z],
|
||||
[xmax, ymin, center_z],
|
||||
[xmax, ymax, center_z],
|
||||
[xmin, ymax, center_z],
|
||||
[xmin, ymin, center_z]
|
||||
]
|
||||
except:
|
||||
return [[0, 0, 0], [100, 0, 0], [100, 100, 0], [0, 100, 0], [0, 0, 0]]
|
||||
|
||||
def _smooth_parting_line(self, parting_line: List[List[float]]) -> List[List[float]]:
|
||||
"""
|
||||
分型线平滑处理 - 使用B样条拟合
|
||||
@@ -628,7 +486,6 @@ class AluminumFoamMoldGenerator:
|
||||
try:
|
||||
points = np.array(parting_line)
|
||||
|
||||
# 简化的平滑算法:移动平均
|
||||
smoothed = []
|
||||
window_size = 3
|
||||
|
||||
@@ -655,7 +512,6 @@ class AluminumFoamMoldGenerator:
|
||||
try:
|
||||
points = np.array(parting_line)
|
||||
|
||||
# 计算相邻线段角度变化
|
||||
angles = []
|
||||
for i in range(1, len(points) - 1):
|
||||
v1 = points[i] - points[i-1]
|
||||
@@ -672,102 +528,25 @@ class AluminumFoamMoldGenerator:
|
||||
|
||||
if angles:
|
||||
avg_angle_change = np.mean(angles)
|
||||
# 角度变化越小越平滑
|
||||
smoothness = max(0, 100 - avg_angle_change * 2)
|
||||
return smoothness
|
||||
|
||||
return 50.0
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
return 50.0
|
||||
|
||||
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
|
||||
"""应用收缩率补偿(铝泡沫版本)"""
|
||||
# 铝泡沫收缩率通常较大
|
||||
scale_factor = 1.0 + self.shrinkage_rate
|
||||
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
|
||||
|
||||
try:
|
||||
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
|
||||
logger.info(f"收缩率补偿应用: {self.shrinkage_rate*100:.2f}%")
|
||||
return scaled_shape
|
||||
except Exception as e:
|
||||
logger.error(f"收缩补偿失败: {e}")
|
||||
return shape
|
||||
|
||||
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
|
||||
"""应用拔模角(改进版)"""
|
||||
# 获取分型面法向量作为拔模方向
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
draft_direction = surface.Plane().Position().Direction()
|
||||
|
||||
logger.info(f"应用拔模角: {self.draft_angle}°, 方向: ({draft_direction.X():.3f}, {draft_direction.Y():.3f}, {draft_direction.Z():.3f})")
|
||||
|
||||
# 注意:完整的拔模实现需要更复杂的 BRepOffsetAPI_DraftAngle
|
||||
# 这里简化处理,返回原始形状
|
||||
return shape
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模角处理失败: {e}")
|
||||
return shape
|
||||
|
||||
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
|
||||
"""分离型腔和型芯(优化版)"""
|
||||
try:
|
||||
# 获取边界框
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
# 计算模具块尺寸
|
||||
margin = 25 # 铝泡沫模具需要更大余量
|
||||
mold_xmin = xmin - margin
|
||||
mold_ymin = ymin - margin
|
||||
mold_zmin = zmin - margin
|
||||
mold_xmax = xmax + margin
|
||||
mold_ymax = ymax + margin
|
||||
mold_zmax = zmax + margin
|
||||
|
||||
# 创建模具块
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
|
||||
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
|
||||
).Shape()
|
||||
|
||||
# 型腔 = 模具块 - 产品
|
||||
cavity_operation = BRepAlgoAPI_Cut(mold_block, shape)
|
||||
if cavity_operation.IsDone():
|
||||
cavity = cavity_operation.Shape()
|
||||
logger.info("型腔生成成功")
|
||||
else:
|
||||
logger.warning("型腔布尔运算失败")
|
||||
cavity = mold_block
|
||||
|
||||
# 型芯 = 产品形状
|
||||
core = shape
|
||||
|
||||
return cavity, core
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"型腔分离失败: {e}")
|
||||
return shape, shape
|
||||
|
||||
def _generate_mold_block(self, cavity: Any, analysis: Dict) -> Any:
|
||||
"""生成完整的模具块(包含A/B板结构)"""
|
||||
try:
|
||||
bbox = analysis["bounding_box"]
|
||||
dims = bbox["dimensions"]
|
||||
|
||||
# 模具总尺寸
|
||||
margin = 30
|
||||
length = dims[0] + 2 * margin
|
||||
width = dims[1] + 2 * margin
|
||||
height = dims[2] + margin + 80 # 增加模架高度
|
||||
height = dims[2] + margin + 80
|
||||
|
||||
# 创建模具块
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(-length/2, -width/2, -80),
|
||||
gp_Pnt(length/2, width/2, height)
|
||||
@@ -780,64 +559,6 @@ class AluminumFoamMoldGenerator:
|
||||
logger.error(f"模具块生成失败: {e}")
|
||||
return cavity
|
||||
|
||||
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
|
||||
"""提取形状几何数据"""
|
||||
try:
|
||||
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
|
||||
mesh.Perform()
|
||||
|
||||
vertices = []
|
||||
faces = []
|
||||
vertex_index = 0
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
face = TopoDS_Face(explorer.Current())
|
||||
location = TopLoc_Location()
|
||||
triangulation = BRep_Tool.Triangulation(face, location)
|
||||
|
||||
if triangulation:
|
||||
nb_nodes = triangulation.NbNodes()
|
||||
for i in range(1, nb_nodes + 1):
|
||||
node = triangulation.Node(i)
|
||||
transformed = node.Transformed(location.Transformation())
|
||||
vertices.extend([
|
||||
float(transformed.X()),
|
||||
float(transformed.Y()),
|
||||
float(transformed.Z())
|
||||
])
|
||||
|
||||
nb_triangles = triangulation.NbTriangles()
|
||||
for i in range(1, nb_triangles + 1):
|
||||
triangle = triangulation.Triangle(i)
|
||||
idx1 = triangle.Value(1) + vertex_index - 1
|
||||
idx2 = triangle.Value(2) + vertex_index - 1
|
||||
idx3 = triangle.Value(3) + vertex_index - 1
|
||||
faces.extend([int(idx1), int(idx2), int(idx3)])
|
||||
|
||||
vertex_index += nb_nodes
|
||||
|
||||
explorer.Next()
|
||||
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": vertices,
|
||||
"faces": faces,
|
||||
"vertex_count": len(vertices) // 3,
|
||||
"face_count": len(faces) // 3
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{shape_type}几何提取失败: {e}")
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": [],
|
||||
"faces": [],
|
||||
"vertex_count": 0,
|
||||
"face_count": 0
|
||||
}
|
||||
|
||||
def _extract_parting_surface_geometry(self, surface: Any) -> Dict[str, Any]:
|
||||
"""提取分型面几何数据"""
|
||||
try:
|
||||
@@ -850,7 +571,7 @@ class AluminumFoamMoldGenerator:
|
||||
"origin": [0, 0, 0],
|
||||
"bounds": {"u_range": [-200, 200], "v_range": [-200, 200]}
|
||||
}
|
||||
except:
|
||||
except Exception:
|
||||
return {
|
||||
"type": "plane",
|
||||
"normal": [0, 0, 1],
|
||||
@@ -858,7 +579,8 @@ class AluminumFoamMoldGenerator:
|
||||
"bounds": {"u_range": [-200, 200], "v_range": [-200, 200]}
|
||||
}
|
||||
|
||||
def _create_parting_surface_from_ai(self, ai_result: Dict, analysis: Dict) -> Dict:
|
||||
def _create_parting_surface_from_ai(self, ai_result: Dict, analysis: Dict,
|
||||
shape: Any = None) -> Dict:
|
||||
"""从 AI 结果创建分型面"""
|
||||
origin = ai_result.get("origin", [0, 0, 0])
|
||||
normal = ai_result.get("normal", [0, 0, 1])
|
||||
@@ -870,14 +592,14 @@ class AluminumFoamMoldGenerator:
|
||||
|
||||
try:
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
except:
|
||||
except Exception:
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1))
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
parting_line = self._calculate_parting_line(
|
||||
analysis.get("shape", None),
|
||||
parting_surface
|
||||
)
|
||||
if shape is not None:
|
||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
||||
else:
|
||||
parting_line = []
|
||||
|
||||
return {
|
||||
"primary_surface": parting_surface,
|
||||
@@ -915,12 +637,6 @@ class AluminumFoamMoldGenerator:
|
||||
else:
|
||||
return "200+ 吨"
|
||||
|
||||
def _calculate_product_weight(self, analysis: Dict) -> str:
|
||||
"""计算产品重量"""
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
weight_g = volume_cm3 * self.material_density
|
||||
return f"{weight_g:.2f} g"
|
||||
|
||||
def _estimate_wall_thickness(self, analysis: Dict) -> str:
|
||||
"""估算壁厚范围"""
|
||||
volume = analysis.get("volume", 0)
|
||||
@@ -961,53 +677,13 @@ class AluminumFoamMoldGenerator:
|
||||
"""识别缩痕风险"""
|
||||
return "中 - 铝泡沫壁厚大,需控制发泡均匀性"
|
||||
|
||||
def _assess_warpage_risk(self, analysis: Dict) -> str:
|
||||
"""评估翘曲风险"""
|
||||
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
|
||||
aspect_ratio = max(bbox) / min(bbox)
|
||||
|
||||
if aspect_ratio > 5:
|
||||
return "高 - 建议增加加强筋"
|
||||
elif aspect_ratio > 3:
|
||||
return "中 - 需优化冷却"
|
||||
else:
|
||||
return "低"
|
||||
|
||||
def _assess_venting_requirement(self, analysis: Dict) -> str:
|
||||
"""评估排气需求"""
|
||||
volume = analysis.get("volume", 0)
|
||||
|
||||
if volume > 50000000: # > 50 cm³
|
||||
if volume > 50000000:
|
||||
return "高 - 需要加强排气系统"
|
||||
elif volume > 10000000: # > 10 cm³
|
||||
elif volume > 10000000:
|
||||
return "中 - 建议标准排气"
|
||||
else:
|
||||
return "低 - 常规排气即可"
|
||||
|
||||
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
|
||||
"""获取惯性矩阵"""
|
||||
inertia = props.MatrixOfInertia()
|
||||
return [
|
||||
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
|
||||
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
|
||||
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
|
||||
]
|
||||
|
||||
def _calculate_parting_line_length(self, parting_line: List) -> float:
|
||||
"""计算分型线长度"""
|
||||
if not parting_line or len(parting_line) < 2:
|
||||
return 0.0
|
||||
|
||||
total_length = 0.0
|
||||
for i in range(1, len(parting_line)):
|
||||
p1 = np.array(parting_line[i-1])
|
||||
p2 = np.array(parting_line[i])
|
||||
total_length += np.linalg.norm(p2 - p1)
|
||||
|
||||
return total_length
|
||||
|
||||
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
|
||||
"""设置 AI 模型接口"""
|
||||
self.ai_parting_detector = parting_detector
|
||||
self.ai_draft_analyzer = draft_analyzer
|
||||
logger.info("AI 模型接口已设置")
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
多型腔布局优化模块
|
||||
|
||||
功能:
|
||||
1. 支持矩形、圆形、H型等常见多型腔排列方式
|
||||
2. 基于产品尺寸和模架尺寸自动计算最优布局
|
||||
3. 流道系统自动设计
|
||||
4. 流动平衡评估
|
||||
5. 材料利用率计算
|
||||
|
||||
布局策略:
|
||||
- 1穴:中心单型腔
|
||||
- 2穴:对称排列
|
||||
- 4穴:2x2 矩阵排列
|
||||
- 8穴:2x4 矩阵排列
|
||||
- 16穴:4x4 矩阵排列
|
||||
- 圆形排列:适用于圆形产品
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import math
|
||||
import numpy as np
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CavityLayoutOptimizer:
|
||||
"""多型腔布局优化器"""
|
||||
|
||||
LAYOUT_RECTANGULAR = "rectangular"
|
||||
LAYOUT_CIRCULAR = "circular"
|
||||
LAYOUT_H_SHAPE = "h_shape"
|
||||
LAYOUT_INLINE = "inline"
|
||||
|
||||
def __init__(self):
|
||||
self.runner_diameter = 5.0
|
||||
self.gate_diameter = 1.5
|
||||
self.cavity_margin = 15.0
|
||||
self.runner_margin = 25.0
|
||||
|
||||
def optimize_layout(self, product_bbox: Dict, cavity_count: int,
|
||||
mold_base_size: Optional[Dict] = None,
|
||||
layout_type: str = "auto",
|
||||
product_shape: Any = None) -> Dict[str, Any]:
|
||||
"""
|
||||
优化多型腔布局
|
||||
|
||||
Args:
|
||||
product_bbox: 产品边界框 {"dimensions": [dx, dy, dz]}
|
||||
cavity_count: 型腔数量
|
||||
mold_base_size: 模架尺寸 {"length": L, "width": W}
|
||||
layout_type: 布局类型 (auto/rectangular/circular/h_shape/inline)
|
||||
product_shape: 产品形状(可选,用于精确计算)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"layout_type": str,
|
||||
"cavity_positions": List[[x, y, z]],
|
||||
"cavity_rotations": List[[rx, ry, rz]],
|
||||
"runner_system": Dict,
|
||||
"balance_score": float,
|
||||
"material_efficiency": float,
|
||||
"mold_size": Dict,
|
||||
"recommendations": List[str]
|
||||
}
|
||||
"""
|
||||
logger.info(f"开始多型腔布局优化: {cavity_count}穴, 布局={layout_type}")
|
||||
|
||||
dims = product_bbox.get("dimensions", [100, 100, 50])
|
||||
|
||||
if layout_type == "auto":
|
||||
layout_type = self._recommend_layout(cavity_count, dims)
|
||||
|
||||
if layout_type == self.LAYOUT_RECTANGULAR:
|
||||
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
|
||||
elif layout_type == self.LAYOUT_CIRCULAR:
|
||||
result = self._layout_circular(dims, cavity_count, mold_base_size)
|
||||
elif layout_type == self.LAYOUT_H_SHAPE:
|
||||
result = self._layout_h_shape(dims, cavity_count, mold_base_size)
|
||||
elif layout_type == self.LAYOUT_INLINE:
|
||||
result = self._layout_inline(dims, cavity_count, mold_base_size)
|
||||
else:
|
||||
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
|
||||
|
||||
result["runner_system"] = self._design_runner_system(
|
||||
result["cavity_positions"], cavity_count, layout_type
|
||||
)
|
||||
result["balance_score"] = self._evaluate_flow_balance(
|
||||
result["cavity_positions"], result["runner_system"]
|
||||
)
|
||||
result["material_efficiency"] = self._calculate_material_efficiency(
|
||||
dims, cavity_count, result["mold_size"]
|
||||
)
|
||||
result["recommendations"] = self._generate_recommendations(
|
||||
result, cavity_count, dims
|
||||
)
|
||||
|
||||
logger.info(f"布局优化完成: {layout_type}, 平衡度={result['balance_score']:.2f}, "
|
||||
f"材料利用率={result['material_efficiency']:.2%}")
|
||||
|
||||
return result
|
||||
|
||||
def _recommend_layout(self, cavity_count: int, dims: List[float]) -> str:
|
||||
"""根据型腔数量和产品尺寸推荐布局方式"""
|
||||
aspect_ratio = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
|
||||
|
||||
if cavity_count == 1:
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
elif cavity_count == 2:
|
||||
if aspect_ratio > 2:
|
||||
return self.LAYOUT_INLINE
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
elif cavity_count <= 4:
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
elif cavity_count <= 8:
|
||||
if aspect_ratio > 2:
|
||||
return self.LAYOUT_H_SHAPE
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
else:
|
||||
return self.LAYOUT_H_SHAPE
|
||||
|
||||
def _layout_rectangular(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""矩形矩阵排列"""
|
||||
rows, cols = self._calculate_grid(cavity_count)
|
||||
|
||||
spacing_x = dims[0] + 2 * self.cavity_margin
|
||||
spacing_y = dims[1] + 2 * self.cavity_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if len(positions) >= cavity_count:
|
||||
break
|
||||
x = (c - (cols - 1) / 2) * spacing_x
|
||||
y = (r - (rows - 1) / 2) * spacing_y
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
total_length = cols * spacing_x + 2 * self.runner_margin
|
||||
total_width = rows * spacing_y + 2 * self.runner_margin
|
||||
|
||||
mold_size = {
|
||||
"length": max(total_length, mold_base_size.get("length", 0)) if mold_base_size else total_length,
|
||||
"width": max(total_width, mold_base_size.get("width", 0)) if mold_base_size else total_width,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_RECTANGULAR,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"grid": {"rows": rows, "cols": cols},
|
||||
"spacing": {"x": spacing_x, "y": spacing_y},
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _layout_circular(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""圆形排列"""
|
||||
max_dim = max(dims[:2])
|
||||
radius = max_dim / 2 + self.cavity_margin + self.runner_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
for i in range(cavity_count):
|
||||
angle = 2 * math.pi * i / cavity_count
|
||||
x = radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
rot_z = -math.degrees(angle)
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, rot_z])
|
||||
|
||||
total_diameter = 2 * radius + max_dim + 2 * self.cavity_margin
|
||||
|
||||
mold_size = {
|
||||
"length": total_diameter,
|
||||
"width": total_diameter,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_CIRCULAR,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"radius": radius,
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _layout_h_shape(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""H型排列(适用于多型腔,流道平衡性好)"""
|
||||
left_count = cavity_count // 2
|
||||
right_count = cavity_count - left_count
|
||||
|
||||
spacing_x = dims[0] + 2 * self.cavity_margin
|
||||
spacing_y = dims[1] + 2 * self.cavity_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
left_rows, left_cols = self._calculate_grid(left_count)
|
||||
for r in range(left_rows):
|
||||
for c in range(left_cols):
|
||||
if len(positions) >= left_count:
|
||||
break
|
||||
x = -(c + 1) * spacing_x - spacing_x / 2
|
||||
y = (r - (left_rows - 1) / 2) * spacing_y
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
right_rows, right_cols = self._calculate_grid(right_count)
|
||||
for r in range(right_rows):
|
||||
for c in range(right_cols):
|
||||
if len(positions) >= cavity_count:
|
||||
break
|
||||
x = (c + 1) * spacing_x + spacing_x / 2
|
||||
y = (r - (right_rows - 1) / 2) * spacing_y
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
total_length = (max(left_cols, right_cols) + 1) * spacing_x * 2 + 2 * self.runner_margin
|
||||
total_width = max(left_rows, right_rows) * spacing_y + 2 * self.runner_margin
|
||||
|
||||
mold_size = {
|
||||
"length": total_length,
|
||||
"width": total_width,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_H_SHAPE,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _layout_inline(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""直线排列(适用于细长产品)"""
|
||||
spacing = max(dims[:2]) + 2 * self.cavity_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
for i in range(cavity_count):
|
||||
offset = (i - (cavity_count - 1) / 2) * spacing
|
||||
if dims[0] > dims[1]:
|
||||
positions.append([offset, 0, 0])
|
||||
else:
|
||||
positions.append([0, offset, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
if dims[0] > dims[1]:
|
||||
total_length = cavity_count * spacing + 2 * self.runner_margin
|
||||
total_width = dims[1] + 2 * self.cavity_margin + 2 * self.runner_margin
|
||||
else:
|
||||
total_length = dims[0] + 2 * self.cavity_margin + 2 * self.runner_margin
|
||||
total_width = cavity_count * spacing + 2 * self.runner_margin
|
||||
|
||||
mold_size = {
|
||||
"length": total_length,
|
||||
"width": total_width,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_INLINE,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _calculate_grid(self, count: int) -> Tuple[int, int]:
|
||||
"""计算最接近正方形的网格排列"""
|
||||
if count <= 0:
|
||||
return 1, 1
|
||||
|
||||
best_rows = 1
|
||||
best_cols = count
|
||||
best_ratio = float("inf")
|
||||
|
||||
for r in range(1, count + 1):
|
||||
if count % r == 0:
|
||||
c = count // r
|
||||
ratio = abs(r - c)
|
||||
if ratio < best_ratio:
|
||||
best_ratio = ratio
|
||||
best_rows = r
|
||||
best_cols = c
|
||||
|
||||
return best_rows, best_cols
|
||||
|
||||
def _design_runner_system(self, positions: List[List[float]],
|
||||
cavity_count: int,
|
||||
layout_type: str) -> Dict:
|
||||
"""
|
||||
设计流道系统
|
||||
|
||||
Returns:
|
||||
{
|
||||
"type": "cold_runner" | "hot_runner",
|
||||
"main_runner": Dict,
|
||||
"sub_runners": List[Dict],
|
||||
"gates": List[Dict],
|
||||
"total_volume": float
|
||||
}
|
||||
"""
|
||||
if cavity_count == 1:
|
||||
return self._design_single_cavity_runner(positions[0])
|
||||
|
||||
main_runner = {
|
||||
"start": [0, -positions[0][1] - 20, 0],
|
||||
"end": [0, positions[0][1] + 20, 0] if len(positions) > 0 else [0, 20, 0],
|
||||
"diameter": self.runner_diameter,
|
||||
"length": 0,
|
||||
}
|
||||
|
||||
sub_runners = []
|
||||
gates = []
|
||||
total_volume = 0
|
||||
|
||||
for i, pos in enumerate(positions):
|
||||
sub_runner = {
|
||||
"start": [0, pos[1], 0],
|
||||
"end": pos,
|
||||
"diameter": self.runner_diameter * 0.8,
|
||||
"length": float(np.linalg.norm(np.array(pos))),
|
||||
}
|
||||
sub_runners.append(sub_runner)
|
||||
total_volume += math.pi * (sub_runner["diameter"] / 2) ** 2 * sub_runner["length"]
|
||||
|
||||
gate = {
|
||||
"position": pos,
|
||||
"diameter": self.gate_diameter,
|
||||
"type": "side_gate",
|
||||
"length": 2.0,
|
||||
}
|
||||
gates.append(gate)
|
||||
total_volume += math.pi * (gate["diameter"] / 2) ** 2 * gate["length"]
|
||||
|
||||
main_runner["length"] = max(
|
||||
abs(p[1]) for p in positions
|
||||
) * 2 + 40 if positions else 40
|
||||
total_volume += math.pi * (main_runner["diameter"] / 2) ** 2 * main_runner["length"]
|
||||
|
||||
return {
|
||||
"type": "cold_runner",
|
||||
"main_runner": main_runner,
|
||||
"sub_runners": sub_runners,
|
||||
"gates": gates,
|
||||
"total_volume": total_volume,
|
||||
}
|
||||
|
||||
def _design_single_cavity_runner(self, position: List[float]) -> Dict:
|
||||
"""单型腔流道设计"""
|
||||
gate = {
|
||||
"position": position,
|
||||
"diameter": self.gate_diameter,
|
||||
"type": "center_gate",
|
||||
"length": 3.0,
|
||||
}
|
||||
|
||||
return {
|
||||
"type": "cold_runner",
|
||||
"main_runner": None,
|
||||
"sub_runners": [],
|
||||
"gates": [gate],
|
||||
"total_volume": math.pi * (gate["diameter"] / 2) ** 2 * gate["length"],
|
||||
}
|
||||
|
||||
def _evaluate_flow_balance(self, positions: List[List[float]],
|
||||
runner_system: Dict) -> float:
|
||||
"""
|
||||
评估流动平衡度 (0-1)
|
||||
|
||||
基于各型腔到主流道的距离差异
|
||||
"""
|
||||
if len(positions) <= 1:
|
||||
return 1.0
|
||||
|
||||
distances = []
|
||||
for pos in positions:
|
||||
dist = float(np.linalg.norm(np.array(pos)))
|
||||
distances.append(dist)
|
||||
|
||||
max_dist = max(distances)
|
||||
min_dist = min(distances)
|
||||
|
||||
if max_dist == 0:
|
||||
return 1.0
|
||||
|
||||
imbalance = (max_dist - min_dist) / max_dist
|
||||
balance_score = max(0, 1.0 - imbalance)
|
||||
|
||||
return round(balance_score, 3)
|
||||
|
||||
def _calculate_material_efficiency(self, product_dims: List[float],
|
||||
cavity_count: int,
|
||||
mold_size: Dict) -> float:
|
||||
"""计算材料利用率"""
|
||||
product_area = product_dims[0] * product_dims[1]
|
||||
total_product_area = product_area * cavity_count
|
||||
mold_area = mold_size.get("length", 0) * mold_size.get("width", 0)
|
||||
|
||||
if mold_area <= 0:
|
||||
return 0.0
|
||||
|
||||
return min(1.0, total_product_area / mold_area)
|
||||
|
||||
def _generate_recommendations(self, result: Dict, cavity_count: int,
|
||||
dims: List[float]) -> List[str]:
|
||||
"""生成优化建议"""
|
||||
recommendations = []
|
||||
|
||||
balance = result.get("balance_score", 0)
|
||||
if balance < 0.8:
|
||||
recommendations.append("流动平衡度偏低,建议调整型腔间距或使用热流道系统")
|
||||
|
||||
efficiency = result.get("material_efficiency", 0)
|
||||
if efficiency < 0.4:
|
||||
recommendations.append("材料利用率偏低,建议减少模架尺寸或增加型腔数量")
|
||||
|
||||
if cavity_count > 8:
|
||||
recommendations.append("多型腔模具建议使用热流道系统以保证填充平衡")
|
||||
|
||||
if cavity_count > 16:
|
||||
recommendations.append("型腔数量过多,建议分模评估加工可行性")
|
||||
|
||||
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
|
||||
if aspect > 3:
|
||||
recommendations.append("产品长宽比大,建议使用侧浇口或扇形浇口")
|
||||
|
||||
if not recommendations:
|
||||
recommendations.append("布局方案合理,建议进行模流分析验证")
|
||||
|
||||
return recommendations
|
||||
+547
-52
@@ -1,7 +1,5 @@
|
||||
# core/geometry_analyzer.py
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# import features # 暂时注释掉,避免导入错误
|
||||
from typing import Dict, List, Any, Optional
|
||||
import math
|
||||
import numpy as np
|
||||
from models.schemas import (
|
||||
create_mold_feature,
|
||||
@@ -14,7 +12,7 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class GeometryAnalyzer:
|
||||
"""几何分析器 - 简化版"""
|
||||
"""几何分析器 - 基于 OCC Shape 的精确分析"""
|
||||
|
||||
def __init__(self):
|
||||
self.feature_thresholds = {
|
||||
@@ -39,31 +37,30 @@ class GeometryAnalyzer:
|
||||
|
||||
def analyze_mold_design(self, geometry_data: Dict[str, Any],
|
||||
product_material: str = "ABS",
|
||||
mold_material: str = "Aluminum"
|
||||
mold_material: str = "Aluminum",
|
||||
shape: Any = None
|
||||
) -> Dict[str, Any]:
|
||||
"""分析模具设计"""
|
||||
"""分析模具设计
|
||||
|
||||
Args:
|
||||
geometry_data: 几何数据字典(来自 stp_parser)
|
||||
product_material: 产品材料
|
||||
mold_material: 模具材料
|
||||
shape: OCC TopoDS_Shape 对象(可选,提供后启用精确分析)
|
||||
"""
|
||||
logger.info("开始模具设计分析")
|
||||
|
||||
# 检测特征
|
||||
features = self._detect_features(geometry_data)
|
||||
features = self._detect_features(geometry_data, shape)
|
||||
|
||||
# 使用产品材料属性
|
||||
product_props = self.product_materials.get(product_material, {})
|
||||
shrinkage = product_props.get("shrinkage", 0.005)
|
||||
|
||||
# 使用模具材料属性
|
||||
mold_props = self.mold_materials.get(mold_material, {})
|
||||
thermal_cond = mold_props.get("thermal_conductivity", 200)
|
||||
|
||||
# 生成设计建议
|
||||
recommendations = self._generate_recommendations(
|
||||
geometry_data, features, product_material
|
||||
)
|
||||
|
||||
# 计算质量指标
|
||||
quality_metrics = self._calculate_quality_metrics(geometry_data, features)
|
||||
|
||||
# 生成分析摘要
|
||||
analysis_summary = self._generate_analysis_summary(geometry_data, features, recommendations)
|
||||
|
||||
return create_analysis_result(
|
||||
@@ -74,32 +71,111 @@ class GeometryAnalyzer:
|
||||
analysis_summary=analysis_summary
|
||||
)
|
||||
|
||||
def _detect_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
def _detect_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Any = None) -> List[Dict[str, Any]]:
|
||||
"""检测模具特征"""
|
||||
features = []
|
||||
|
||||
# 壁厚分析
|
||||
wall_features = self._detect_wall_features(geometry_data)
|
||||
wall_features = self._detect_wall_features(geometry_data, shape)
|
||||
features.extend(wall_features)
|
||||
|
||||
# 加强筋检测
|
||||
rib_features = self._detect_rib_features(geometry_data)
|
||||
rib_features = self._detect_rib_features(geometry_data, shape)
|
||||
features.extend(rib_features)
|
||||
|
||||
# BOSS柱检测
|
||||
boss_features = self._detect_boss_features(geometry_data)
|
||||
boss_features = self._detect_boss_features(geometry_data, shape)
|
||||
features.extend(boss_features)
|
||||
|
||||
# 拔模角度分析
|
||||
draft_features = self._analyze_draft_angles(geometry_data)
|
||||
draft_features = self._analyze_draft_angles(geometry_data, shape)
|
||||
features.extend(draft_features)
|
||||
|
||||
if shape is not None:
|
||||
curvature_features = self._detect_curvature_features(shape)
|
||||
features.extend(curvature_features)
|
||||
|
||||
fillet_features = self._detect_fillet_features(shape)
|
||||
features.extend(fillet_features)
|
||||
|
||||
logger.info(f"检测到 {len(features)} 个特征")
|
||||
return features
|
||||
|
||||
def _detect_wall_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
def _detect_wall_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Any = None) -> List[Dict[str, Any]]:
|
||||
"""检测壁厚特征"""
|
||||
features = []
|
||||
|
||||
if shape is not None:
|
||||
precise_result = self._compute_precise_wall_thickness(shape)
|
||||
if precise_result is not None:
|
||||
min_thickness = precise_result["min_thickness"]
|
||||
max_thickness = precise_result["max_thickness"]
|
||||
avg_thickness = precise_result["avg_thickness"]
|
||||
thickness_map = precise_result.get("thickness_map", {})
|
||||
estimation_method = "precise"
|
||||
|
||||
if min_thickness < self.feature_thresholds["thin_wall"]:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="thin_wall",
|
||||
confidence=0.92,
|
||||
location=precise_result.get("min_location",
|
||||
geometry_data.get("center_of_mass", [0, 0, 0])),
|
||||
dimensions=[min_thickness, avg_thickness, max_thickness],
|
||||
parameters={
|
||||
"min_thickness": round(min_thickness, 3),
|
||||
"max_thickness": round(max_thickness, 3),
|
||||
"avg_thickness": round(avg_thickness, 3),
|
||||
"estimation_method": estimation_method,
|
||||
"measured_pairs": len(thickness_map),
|
||||
},
|
||||
recommendations=[
|
||||
f"最小壁厚 {min_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
|
||||
"薄壁区域可能导致注塑填充不充分",
|
||||
"考虑增加加强筋以提高结构强度"
|
||||
]
|
||||
))
|
||||
elif max_thickness > self.feature_thresholds["thick_wall"]:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="thick_wall",
|
||||
confidence=0.88,
|
||||
location=precise_result.get("max_location",
|
||||
geometry_data.get("center_of_mass", [0, 0, 0])),
|
||||
dimensions=[min_thickness, avg_thickness, max_thickness],
|
||||
parameters={
|
||||
"min_thickness": round(min_thickness, 3),
|
||||
"max_thickness": round(max_thickness, 3),
|
||||
"avg_thickness": round(avg_thickness, 3),
|
||||
"estimation_method": estimation_method,
|
||||
"measured_pairs": len(thickness_map),
|
||||
},
|
||||
recommendations=[
|
||||
f"最大壁厚 {max_thickness:.2f}mm 过厚,可能产生缩痕",
|
||||
"考虑减薄壁厚或增加加强筋",
|
||||
"优化冷却系统设计"
|
||||
]
|
||||
))
|
||||
|
||||
if min_thickness > 0 and max_thickness > 0:
|
||||
uniformity = min_thickness / max_thickness if max_thickness > 0 else 1.0
|
||||
if uniformity < 0.5:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="wall_non_uniform",
|
||||
confidence=0.80,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[min_thickness, max_thickness, uniformity],
|
||||
parameters={
|
||||
"uniformity_ratio": round(uniformity, 3),
|
||||
"min_thickness": round(min_thickness, 3),
|
||||
"max_thickness": round(max_thickness, 3),
|
||||
"estimation_method": estimation_method,
|
||||
},
|
||||
recommendations=[
|
||||
f"壁厚均匀性比 {uniformity:.2f} 偏低(建议 > 0.5)",
|
||||
"壁厚差异过大可能导致翘曲和缩痕",
|
||||
"建议逐步过渡壁厚,避免突变"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
volume = geometry_data.get("volume", 0)
|
||||
surface_area = geometry_data.get("surface_area", 0)
|
||||
|
||||
@@ -112,7 +188,7 @@ class GeometryAnalyzer:
|
||||
confidence=0.85,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[avg_thickness, avg_thickness, avg_thickness],
|
||||
parameters={"average_thickness": avg_thickness},
|
||||
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
|
||||
recommendations=[
|
||||
f"平均壁厚 {avg_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
|
||||
"考虑增加加强筋以提高结构强度",
|
||||
@@ -125,7 +201,7 @@ class GeometryAnalyzer:
|
||||
confidence=0.75,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[avg_thickness, avg_thickness, avg_thickness],
|
||||
parameters={"average_thickness": avg_thickness},
|
||||
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
|
||||
recommendations=[
|
||||
f"平均壁厚 {avg_thickness:.2f}mm 过厚,可能产生缩痕",
|
||||
"考虑减薄壁厚或增加加强筋",
|
||||
@@ -133,7 +209,6 @@ class GeometryAnalyzer:
|
||||
]
|
||||
))
|
||||
elif volume > 0:
|
||||
# 如果没有surface_area,基于边界框估算壁厚
|
||||
bbox = geometry_data.get("bounding_box", {})
|
||||
dimensions = bbox.get("dimensions", [100, 100, 100])
|
||||
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
|
||||
@@ -155,7 +230,96 @@ class GeometryAnalyzer:
|
||||
|
||||
return features
|
||||
|
||||
def _detect_rib_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
def _compute_precise_wall_thickness(self, shape: Any) -> Optional[Dict[str, Any]]:
|
||||
"""使用 BRepExtrema_DistShapeShape 精确计算壁厚"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face
|
||||
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.gp import gp_Pnt
|
||||
|
||||
faces = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
faces.append(TopoDS_Face(explorer.Current()))
|
||||
explorer.Next()
|
||||
|
||||
if len(faces) < 2:
|
||||
return None
|
||||
|
||||
face_areas = []
|
||||
for face in faces:
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, props)
|
||||
face_areas.append(props.Mass())
|
||||
|
||||
indexed_faces = sorted(enumerate(faces), key=lambda x: face_areas[x[0]], reverse=True)
|
||||
|
||||
max_faces_to_check = min(len(indexed_faces), 30)
|
||||
|
||||
min_thickness = float('inf')
|
||||
max_thickness = 0.0
|
||||
thickness_values = []
|
||||
min_location = [0, 0, 0]
|
||||
max_location = [0, 0, 0]
|
||||
|
||||
for i in range(max_faces_to_check):
|
||||
for j in range(i + 1, max_faces_to_check):
|
||||
idx_i, face_i = indexed_faces[i]
|
||||
idx_j, face_j = indexed_faces[j]
|
||||
|
||||
try:
|
||||
dist_calc = BRepExtrema_DistShapeShape(face_i, face_j)
|
||||
if dist_calc.IsDone():
|
||||
dist = dist_calc.Value()
|
||||
if 0.1 < dist < 50.0:
|
||||
thickness_values.append(dist)
|
||||
if dist < min_thickness:
|
||||
min_thickness = dist
|
||||
try:
|
||||
p1 = dist_calc.PointOnShape1(1)
|
||||
min_location = [float(p1.X()), float(p1.Y()), float(p1.Z())]
|
||||
except Exception:
|
||||
pass
|
||||
if dist > max_thickness:
|
||||
max_thickness = dist
|
||||
try:
|
||||
p2 = dist_calc.PointOnShape2(1)
|
||||
max_location = [float(p2.X()), float(p2.Y()), float(p2.Z())]
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not thickness_values:
|
||||
return None
|
||||
|
||||
avg_thickness = sum(thickness_values) / len(thickness_values)
|
||||
|
||||
return {
|
||||
"min_thickness": min_thickness,
|
||||
"max_thickness": max_thickness,
|
||||
"avg_thickness": avg_thickness,
|
||||
"thickness_map": {f"pair_{i}": v for i, v in enumerate(thickness_values[:50])},
|
||||
"measured_pairs": len(thickness_values),
|
||||
"min_location": min_location,
|
||||
"max_location": max_location,
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
logger.warning("pythonOCC 不可用,无法进行精确壁厚检测")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"精确壁厚检测失败: {e}")
|
||||
return None
|
||||
|
||||
def _detect_rib_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Any = None) -> List[Dict[str, Any]]:
|
||||
"""检测加强筋特征"""
|
||||
features = []
|
||||
topology = geometry_data.get("topology", {})
|
||||
@@ -165,9 +329,13 @@ class GeometryAnalyzer:
|
||||
complexity_ratio = edge_count / max(face_count, 1)
|
||||
|
||||
if complexity_ratio > 3.0:
|
||||
confidence = 0.7
|
||||
if shape is not None:
|
||||
confidence = 0.78
|
||||
|
||||
features.append(create_mold_feature(
|
||||
feature_type="rib_structure",
|
||||
confidence=0.7,
|
||||
confidence=confidence,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[2.0, 8.0, 2.0],
|
||||
parameters={"complexity_ratio": complexity_ratio},
|
||||
@@ -181,7 +349,8 @@ class GeometryAnalyzer:
|
||||
|
||||
return features
|
||||
|
||||
def _detect_boss_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
def _detect_boss_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Any = None) -> List[Dict[str, Any]]:
|
||||
"""检测BOSS柱特征"""
|
||||
features = []
|
||||
volume = geometry_data.get("volume", 0)
|
||||
@@ -191,9 +360,13 @@ class GeometryAnalyzer:
|
||||
volume_efficiency = volume / (dimensions[0] * dimensions[1] * dimensions[2])
|
||||
|
||||
if volume_efficiency < 0.3:
|
||||
confidence = 0.65
|
||||
if shape is not None:
|
||||
confidence = 0.72
|
||||
|
||||
features.append(create_mold_feature(
|
||||
feature_type="boss_feature",
|
||||
confidence=0.65,
|
||||
confidence=confidence,
|
||||
location=bbox.get("center", [50, 50, 50]),
|
||||
dimensions=[6.0, 12.0, 6.0],
|
||||
parameters={"volume_efficiency": volume_efficiency},
|
||||
@@ -208,16 +381,66 @@ class GeometryAnalyzer:
|
||||
|
||||
return features
|
||||
|
||||
def _analyze_draft_angles(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
def _analyze_draft_angles(self, geometry_data: Dict[str, Any],
|
||||
shape: Any = None) -> List[Dict[str, Any]]:
|
||||
"""分析拔模角度"""
|
||||
features = []
|
||||
|
||||
if shape is not None:
|
||||
draft_result = self._compute_draft_angles_from_shape(shape)
|
||||
if draft_result is not None:
|
||||
min_draft = draft_result["min_draft_angle"]
|
||||
max_draft = draft_result["max_draft_angle"]
|
||||
undrafted_count = draft_result["undrafted_faces"]
|
||||
total_side_faces = draft_result["total_side_faces"]
|
||||
|
||||
if undrafted_count > 0:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="draft_angle",
|
||||
confidence=0.90,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[min_draft, max_draft, undrafted_count],
|
||||
parameters={
|
||||
"min_draft_angle": round(min_draft, 2),
|
||||
"max_draft_angle": round(max_draft, 2),
|
||||
"undrafted_faces": undrafted_count,
|
||||
"total_side_faces": total_side_faces,
|
||||
"estimation_method": "precise",
|
||||
},
|
||||
recommendations=[
|
||||
f"检测到 {undrafted_count} 个面需要拔模(当前最小拔模角 {min_draft:.1f}°)",
|
||||
"建议所有垂直面添加1-2度拔模角度",
|
||||
"纹理表面需要3-5度拔模角度",
|
||||
"深腔结构需要更大的拔模角度"
|
||||
]
|
||||
))
|
||||
else:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="draft_angle",
|
||||
confidence=0.90,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[min_draft, max_draft, 0],
|
||||
parameters={
|
||||
"min_draft_angle": round(min_draft, 2),
|
||||
"max_draft_angle": round(max_draft, 2),
|
||||
"undrafted_faces": 0,
|
||||
"total_side_faces": total_side_faces,
|
||||
"estimation_method": "precise",
|
||||
},
|
||||
recommendations=[
|
||||
f"所有侧壁面已有拔模角(最小 {min_draft:.1f}°)",
|
||||
"拔模角度满足要求"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
features.append(create_mold_feature(
|
||||
feature_type="draft_angle",
|
||||
confidence=0.8,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[1.0, 2.0, 1.0],
|
||||
parameters={"recommended_angle": 2.0},
|
||||
parameters={"recommended_angle": 2.0, "estimation_method": "heuristic"},
|
||||
recommendations=[
|
||||
"建议所有垂直面添加1-2度拔模角度",
|
||||
"纹理表面需要3-5度拔模角度",
|
||||
@@ -227,18 +450,238 @@ class GeometryAnalyzer:
|
||||
|
||||
return features
|
||||
|
||||
def _compute_draft_angles_from_shape(self, shape: Any) -> Optional[Dict[str, Any]]:
|
||||
"""基于面法向量分析计算各面的拔模角度"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
from OCC.Core.gp import gp_Dir
|
||||
|
||||
draft_direction = gp_Dir(0, 0, 1)
|
||||
|
||||
draft_angles = []
|
||||
side_face_count = 0
|
||||
undrafted_count = 0
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
face = TopoDS_Face(explorer.Current())
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
try:
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
normal = None
|
||||
if surface.GetType() == 0:
|
||||
normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
props = BRepLProp_SLProps(surface, 1, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
normal = props.Normal()
|
||||
|
||||
if normal is not None:
|
||||
dot = abs(normal.Dot(draft_direction))
|
||||
angle_from_vertical = math.degrees(math.acos(min(dot, 1.0)))
|
||||
|
||||
if 5.0 < angle_from_vertical < 85.0:
|
||||
side_face_count += 1
|
||||
draft_angle = 90.0 - angle_from_vertical
|
||||
draft_angles.append(draft_angle)
|
||||
if draft_angle < 0.5:
|
||||
undrafted_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not draft_angles:
|
||||
return None
|
||||
|
||||
return {
|
||||
"min_draft_angle": min(draft_angles),
|
||||
"max_draft_angle": max(draft_angles),
|
||||
"avg_draft_angle": sum(draft_angles) / len(draft_angles),
|
||||
"undrafted_faces": undrafted_count,
|
||||
"total_side_faces": side_face_count,
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模角度计算失败: {e}")
|
||||
return None
|
||||
|
||||
def _detect_curvature_features(self, shape: Any) -> List[Dict[str, Any]]:
|
||||
"""检测高曲率区域(可能导致应力集中)"""
|
||||
features = []
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
high_curvature_count = 0
|
||||
max_curvature_overall = 0.0
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
face = TopoDS_Face(explorer.Current())
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
if surface.GetType() == 0:
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
try:
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, props)
|
||||
face_area = props.Mass()
|
||||
|
||||
u_range = (surface.FirstUParameter(), surface.LastUParameter())
|
||||
v_range = (surface.FirstVParameter(), surface.LastVParameter())
|
||||
|
||||
max_curvature = 0.0
|
||||
sample_count = 5
|
||||
|
||||
for ui in range(sample_count):
|
||||
for vi in range(sample_count):
|
||||
u = u_range[0] + (u_range[1] - u_range[0]) * (ui + 0.5) / sample_count
|
||||
v = v_range[0] + (v_range[1] - v_range[0]) * (vi + 0.5) / sample_count
|
||||
|
||||
try:
|
||||
lprops = BRepLProp_SLProps(surface, 2, 0.001)
|
||||
lprops.SetParameters(u, v)
|
||||
if lprops.IsCurvatureDefined():
|
||||
k1 = abs(lprops.MinCurvature())
|
||||
k2 = abs(lprops.MaxCurvature())
|
||||
max_curvature = max(max_curvature, k1, k2)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if max_curvature > max_curvature_overall:
|
||||
max_curvature_overall = max_curvature
|
||||
|
||||
if max_curvature > 0.5:
|
||||
high_curvature_count += 1
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if high_curvature_count > 0:
|
||||
risk_level = "high" if high_curvature_count > 5 else "medium"
|
||||
features.append(create_mold_feature(
|
||||
feature_type="high_curvature",
|
||||
confidence=0.82,
|
||||
location=[0, 0, 0],
|
||||
dimensions=[high_curvature_count, max_curvature_overall, 0],
|
||||
parameters={
|
||||
"high_curvature_faces": high_curvature_count,
|
||||
"max_curvature": round(max_curvature_overall, 4),
|
||||
"risk_level": risk_level,
|
||||
},
|
||||
recommendations=[
|
||||
f"检测到 {high_curvature_count} 个高曲率区域",
|
||||
"高曲率区域可能导致应力集中和填充困难",
|
||||
"建议增加圆角半径以降低曲率",
|
||||
"注意这些区域的冷却设计"
|
||||
]
|
||||
))
|
||||
|
||||
except ImportError:
|
||||
logger.debug("pythonOCC 不可用,跳过曲率检测")
|
||||
except Exception as e:
|
||||
logger.warning(f"曲率检测失败: {e}")
|
||||
|
||||
return features
|
||||
|
||||
def _detect_fillet_features(self, shape: Any) -> List[Dict[str, Any]]:
|
||||
"""检测圆角/倒角特征"""
|
||||
features = []
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_EDGE
|
||||
from OCC.Core.TopoDS import TopoDS_Edge
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
|
||||
|
||||
fillet_count = 0
|
||||
small_fillet_count = 0
|
||||
min_fillet_radius = float('inf')
|
||||
radii = []
|
||||
|
||||
edge_explorer = TopExp_Explorer(shape, TopAbs_EDGE)
|
||||
while edge_explorer.More():
|
||||
edge = TopoDS_Edge(edge_explorer.Current())
|
||||
try:
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
curve_type = curve.GetType()
|
||||
|
||||
if curve_type == 2: # GeomAbs_Circle
|
||||
circle = curve.Circle()
|
||||
radius = circle.Radius()
|
||||
if 0.05 < radius < 50:
|
||||
fillet_count += 1
|
||||
radii.append(radius)
|
||||
if radius < min_fillet_radius:
|
||||
min_fillet_radius = radius
|
||||
if radius < 0.5:
|
||||
small_fillet_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
edge_explorer.Next()
|
||||
|
||||
if fillet_count > 0:
|
||||
avg_radius = sum(radii) / len(radii)
|
||||
features.append(create_mold_feature(
|
||||
feature_type="fillet",
|
||||
confidence=0.85,
|
||||
location=[0, 0, 0],
|
||||
dimensions=[min_fillet_radius, avg_radius, max(radii)],
|
||||
parameters={
|
||||
"fillet_count": fillet_count,
|
||||
"min_radius": round(min_fillet_radius, 3),
|
||||
"max_radius": round(max(radii), 3),
|
||||
"avg_radius": round(avg_radius, 3),
|
||||
"small_fillet_count": small_fillet_count,
|
||||
},
|
||||
recommendations=[
|
||||
f"检测到 {fillet_count} 个圆角特征" +
|
||||
(f",其中 {small_fillet_count} 个半径过小" if small_fillet_count > 0 else ""),
|
||||
"小圆角(R<0.5mm)可能导致应力集中" if small_fillet_count > 0 else "",
|
||||
"建议圆角半径不小于 0.5mm" if small_fillet_count > 0 else "",
|
||||
] if small_fillet_count > 0 else [
|
||||
f"检测到 {fillet_count} 个圆角特征",
|
||||
"圆角半径范围合理"
|
||||
]
|
||||
))
|
||||
|
||||
except ImportError:
|
||||
logger.debug("pythonOCC 不可用,跳过圆角检测")
|
||||
except Exception as e:
|
||||
logger.warning(f"圆角检测失败: {e}")
|
||||
|
||||
return features
|
||||
|
||||
def _generate_recommendations(self, geometry_data: Dict[str, Any],
|
||||
features: List[Dict[str, Any]],
|
||||
material: str) -> List[Dict[str, Any]]:
|
||||
"""生成设计建议"""
|
||||
recommendations = []
|
||||
|
||||
# 壁厚建议
|
||||
wall_rec = self._get_wall_thickness_recommendation(geometry_data, material)
|
||||
wall_rec = self._get_wall_thickness_recommendation(geometry_data, material, features)
|
||||
if wall_rec:
|
||||
recommendations.append(wall_rec)
|
||||
|
||||
# 拔模角度建议
|
||||
recommendations.append(create_design_recommendation(
|
||||
rec_type="draft_angle",
|
||||
priority="high",
|
||||
@@ -247,31 +690,60 @@ class GeometryAnalyzer:
|
||||
reason="确保顺利脱模"
|
||||
))
|
||||
|
||||
# 基于检测到的特征生成建议
|
||||
for feature in features:
|
||||
if feature["feature_type"] == "thin_wall":
|
||||
params = feature.get("parameters", {})
|
||||
current = params.get("min_thickness", params.get("average_thickness", 0))
|
||||
rec = create_design_recommendation(
|
||||
rec_type="wall_thickness",
|
||||
priority="high",
|
||||
description="增加壁厚",
|
||||
parameters={
|
||||
"current": feature["parameters"]["average_thickness"],
|
||||
"current": current,
|
||||
"recommended": self.feature_thresholds["thin_wall"]
|
||||
},
|
||||
reason="壁厚不足影响结构强度"
|
||||
)
|
||||
recommendations.append(rec)
|
||||
elif feature["feature_type"] == "high_curvature":
|
||||
recommendations.append(create_design_recommendation(
|
||||
rec_type="curvature",
|
||||
priority="medium",
|
||||
description="优化高曲率区域",
|
||||
parameters={"max_curvature": feature["parameters"].get("max_curvature", 0)},
|
||||
reason="高曲率区域可能导致应力集中"
|
||||
))
|
||||
elif feature["feature_type"] == "fillet" and feature["parameters"].get("small_fillet_count", 0) > 0:
|
||||
recommendations.append(create_design_recommendation(
|
||||
rec_type="fillet",
|
||||
priority="medium",
|
||||
description="增大过小圆角半径",
|
||||
parameters={"min_radius": feature["parameters"].get("min_radius", 0)},
|
||||
reason="小圆角导致应力集中和加工困难"
|
||||
))
|
||||
|
||||
return recommendations
|
||||
|
||||
def _get_wall_thickness_recommendation(self, geometry_data: Dict[str, Any],
|
||||
material: str) -> Dict[str, Any]:
|
||||
material: str,
|
||||
features: List[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""获取壁厚建议"""
|
||||
avg_thickness = None
|
||||
|
||||
if features:
|
||||
for f in features:
|
||||
if f["feature_type"] in ("thin_wall", "thick_wall"):
|
||||
params = f.get("parameters", {})
|
||||
avg_thickness = params.get("avg_thickness", params.get("average_thickness"))
|
||||
break
|
||||
|
||||
if avg_thickness is None:
|
||||
volume = geometry_data.get("volume", 0)
|
||||
surface_area = geometry_data.get("surface_area", 0)
|
||||
|
||||
if volume > 0 and surface_area > 0:
|
||||
avg_thickness = (volume / surface_area) * 0.6
|
||||
|
||||
if avg_thickness is not None:
|
||||
material_props = self.product_materials.get(material, self.product_materials["ABS"])
|
||||
min_wall = material_props["min_wall"]
|
||||
|
||||
@@ -291,7 +763,6 @@ class GeometryAnalyzer:
|
||||
"""计算质量指标"""
|
||||
metrics = {}
|
||||
|
||||
# 体积利用率
|
||||
bbox = geometry_data.get("bounding_box", {})
|
||||
dimensions = bbox.get("dimensions", [100, 100, 100])
|
||||
volume = geometry_data.get("volume", 0)
|
||||
@@ -299,22 +770,42 @@ class GeometryAnalyzer:
|
||||
|
||||
metrics["volume_utilization"] = volume / bbox_volume if bbox_volume > 0 else 0
|
||||
|
||||
# 拓扑复杂度
|
||||
topology = geometry_data.get("topology", {})
|
||||
face_count = topology.get("faces", 0)
|
||||
metrics["topology_complexity"] = face_count / 100.0
|
||||
|
||||
# 壁厚均匀性评分
|
||||
wall_uniformity = 0.5
|
||||
for f in features:
|
||||
if f["feature_type"] in ("thin_wall", "thick_wall", "wall_non_uniform"):
|
||||
params = f.get("parameters", {})
|
||||
if "uniformity_ratio" in params:
|
||||
wall_uniformity = params["uniformity_ratio"]
|
||||
break
|
||||
min_t = params.get("min_thickness", params.get("average_thickness", 0))
|
||||
max_t = params.get("max_thickness", params.get("average_thickness", 0))
|
||||
if min_t > 0 and max_t > 0:
|
||||
wall_uniformity = min_t / max_t
|
||||
break
|
||||
|
||||
if wall_uniformity == 0.5:
|
||||
surface_area = geometry_data.get("surface_area", 0)
|
||||
if volume > 0 and surface_area > 0:
|
||||
thickness_ratio = (volume / surface_area) * 0.6
|
||||
ideal_thickness = 3.0
|
||||
metrics["wall_uniformity"] = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
|
||||
elif volume > 0 and bbox_volume > 0:
|
||||
# 如果没有surface_area,基于体积利用率估算
|
||||
metrics["wall_uniformity"] = max(0.5, metrics["volume_utilization"])
|
||||
else:
|
||||
metrics["wall_uniformity"] = 0.5
|
||||
wall_uniformity = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
|
||||
|
||||
metrics["wall_uniformity"] = max(0, min(1, wall_uniformity))
|
||||
|
||||
draft_score = 1.0
|
||||
for f in features:
|
||||
if f["feature_type"] == "draft_angle":
|
||||
params = f.get("parameters", {})
|
||||
undrafted = params.get("undrafted_faces", None)
|
||||
total = params.get("total_side_faces", 1)
|
||||
if undrafted is not None and total > 0:
|
||||
draft_score = 1.0 - (undrafted / total)
|
||||
break
|
||||
metrics["draft_score"] = round(draft_score, 3)
|
||||
|
||||
return metrics
|
||||
|
||||
@@ -334,6 +825,10 @@ class GeometryAnalyzer:
|
||||
feature_types = set(f["feature_type"] for f in features)
|
||||
summary_parts.append(f"检测到 {len(feature_types)} 类特征")
|
||||
|
||||
precise_features = [f for f in features if f.get("parameters", {}).get("estimation_method") == "precise"]
|
||||
if precise_features:
|
||||
summary_parts.append(f"其中 {len(precise_features)} 个特征为精确检测")
|
||||
|
||||
if high_priority_recs > 0:
|
||||
summary_parts.append(f"有 {high_priority_recs} 个高优先级建议")
|
||||
|
||||
|
||||
@@ -0,0 +1,646 @@
|
||||
"""
|
||||
模具刀路设计与G代码生成模块
|
||||
|
||||
架构:
|
||||
1. ToolLibrary - 刀具库与切削参数管理
|
||||
2. CuttingParamsCalculator - 切削参数自动计算
|
||||
3. RoughingToolpathGenerator - 粗加工刀路生成
|
||||
4. FinishingToolpathGenerator - 精加工刀路生成
|
||||
5. GCodePostProcessor - G代码后处理器
|
||||
6. MoldCAMDesigner - 模具CAM综合设计器
|
||||
|
||||
加工策略:
|
||||
- 粗加工:Z层等高粗加工(自适应清根)
|
||||
- 半精加工:等高线铣削
|
||||
- 精加工:平行铣削/螺旋铣削/等高线精加工
|
||||
- 清角:笔式清角
|
||||
- 钻孔:冷却水路/顶针孔/螺丝孔
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import math
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ToolLibrary:
|
||||
"""刀具库"""
|
||||
|
||||
TOOLS = {
|
||||
"endmill_20mm": {
|
||||
"type": "endmill", "diameter": 20.0, "flute_length": 60.0,
|
||||
"cutting_edges": 4, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 100, "feed_per_tooth": 0.15,
|
||||
"axial_depth": 10.0, "radial_depth": 15.0
|
||||
}
|
||||
},
|
||||
"endmill_16mm": {
|
||||
"type": "endmill", "diameter": 16.0, "flute_length": 50.0,
|
||||
"cutting_edges": 4, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 120, "feed_per_tooth": 0.12,
|
||||
"axial_depth": 8.0, "radial_depth": 12.0
|
||||
}
|
||||
},
|
||||
"endmill_10mm": {
|
||||
"type": "endmill", "diameter": 10.0, "flute_length": 35.0,
|
||||
"cutting_edges": 4, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 130, "feed_per_tooth": 0.10,
|
||||
"axial_depth": 5.0, "radial_depth": 8.0
|
||||
}
|
||||
},
|
||||
"endmill_6mm": {
|
||||
"type": "endmill", "diameter": 6.0, "flute_length": 22.0,
|
||||
"cutting_edges": 3, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 140, "feed_per_tooth": 0.06,
|
||||
"axial_depth": 3.0, "radial_depth": 4.0
|
||||
}
|
||||
},
|
||||
"ballnose_10mm": {
|
||||
"type": "ballnose", "diameter": 10.0, "flute_length": 30.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 5.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 150, "feed_per_tooth": 0.08,
|
||||
"axial_depth": 0.5, "radial_depth": 1.0
|
||||
}
|
||||
},
|
||||
"ballnose_6mm": {
|
||||
"type": "ballnose", "diameter": 6.0, "flute_length": 22.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 3.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 160, "feed_per_tooth": 0.06,
|
||||
"axial_depth": 0.3, "radial_depth": 0.5
|
||||
}
|
||||
},
|
||||
"ballnose_3mm": {
|
||||
"type": "ballnose", "diameter": 3.0, "flute_length": 12.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 1.5,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 180, "feed_per_tooth": 0.03,
|
||||
"axial_depth": 0.15, "radial_depth": 0.3
|
||||
}
|
||||
},
|
||||
"ballnose_1mm": {
|
||||
"type": "ballnose", "diameter": 1.0, "flute_length": 5.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 0.5,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 200, "feed_per_tooth": 0.01,
|
||||
"axial_depth": 0.05, "radial_depth": 0.1
|
||||
}
|
||||
},
|
||||
"drill_8mm": {
|
||||
"type": "drill", "diameter": 8.0, "flute_length": 50.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 80, "feed_per_tooth": 0.10,
|
||||
"axial_depth": 50.0, "radial_depth": 0.0
|
||||
}
|
||||
},
|
||||
"drill_5mm": {
|
||||
"type": "drill", "diameter": 5.0, "flute_length": 35.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 90, "feed_per_tooth": 0.08,
|
||||
"axial_depth": 35.0, "radial_depth": 0.0
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
MOLD_STEEL = {
|
||||
"P20": {"hardness_hrc": 30, "cutting_speed_factor": 1.0, "feed_factor": 1.0},
|
||||
"718H": {"hardness_hrc": 35, "cutting_speed_factor": 0.85, "feed_factor": 0.9},
|
||||
"NAK80": {"hardness_hrc": 38, "cutting_speed_factor": 0.75, "feed_factor": 0.85},
|
||||
"S136": {"hardness_hrc": 50, "cutting_speed_factor": 0.5, "feed_factor": 0.7},
|
||||
"H13": {"hardness_hrc": 48, "cutting_speed_factor": 0.55, "feed_factor": 0.75},
|
||||
"Al7075": {"hardness_hrc": 15, "cutting_speed_factor": 2.0, "feed_factor": 1.5},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_tool(cls, tool_id: str) -> Optional[Dict]:
|
||||
return cls.TOOLS.get(tool_id)
|
||||
|
||||
@classmethod
|
||||
def select_roughing_tool(cls, cavity_volume_mm3: float,
|
||||
min_corner_radius: float = 0.0,
|
||||
steel: str = "P20") -> Dict:
|
||||
"""根据型腔体积和最小圆角选择粗加工刀具"""
|
||||
if cavity_volume_mm3 > 500000:
|
||||
tool_id = "endmill_20mm"
|
||||
elif cavity_volume_mm3 > 100000:
|
||||
tool_id = "endmill_16mm"
|
||||
elif cavity_volume_mm3 > 20000:
|
||||
tool_id = "endmill_10mm"
|
||||
else:
|
||||
tool_id = "endmill_6mm"
|
||||
|
||||
tool = cls.TOOLS[tool_id].copy()
|
||||
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
|
||||
|
||||
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
|
||||
tool["tool_id"] = tool_id
|
||||
return tool
|
||||
|
||||
@classmethod
|
||||
def select_finishing_tool(cls, surface_quality: str = "standard",
|
||||
min_corner_radius: float = 0.0,
|
||||
steel: str = "P20") -> Dict:
|
||||
"""根据表面质量要求选择精加工刀具"""
|
||||
if surface_quality == "mirror":
|
||||
tool_id = "ballnose_3mm" if min_corner_radius <= 3 else "ballnose_6mm"
|
||||
elif surface_quality == "fine":
|
||||
tool_id = "ballnose_6mm" if min_corner_radius <= 6 else "ballnose_10mm"
|
||||
else:
|
||||
tool_id = "ballnose_10mm"
|
||||
|
||||
tool = cls.TOOLS[tool_id].copy()
|
||||
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
|
||||
|
||||
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
|
||||
tool["tool_id"] = tool_id
|
||||
return tool
|
||||
|
||||
@classmethod
|
||||
def _adjust_for_steel(cls, speeds_feeds: Dict, steel_props: Dict) -> Dict:
|
||||
"""根据模具钢调整切削参数"""
|
||||
adjusted = speeds_feeds.copy()
|
||||
adjusted["cutting_speed"] *= steel_props["cutting_speed_factor"]
|
||||
adjusted["feed_per_tooth"] *= steel_props["feed_factor"]
|
||||
adjusted["axial_depth"] *= steel_props["feed_factor"]
|
||||
adjusted["radial_depth"] *= steel_props["feed_factor"]
|
||||
return adjusted
|
||||
|
||||
|
||||
class CuttingParamsCalculator:
|
||||
"""切削参数计算器"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_spindle_speed(cutting_speed_m_min: float, tool_diameter: float) -> int:
|
||||
"""N = (1000 × Vc) / (π × D)"""
|
||||
if tool_diameter <= 0:
|
||||
return 1000
|
||||
rpm = (1000 * cutting_speed_m_min) / (math.pi * tool_diameter)
|
||||
return int(min(max(rpm, 500), 24000))
|
||||
|
||||
@staticmethod
|
||||
def calculate_feed_rate(spindle_speed: int, feed_per_tooth: float,
|
||||
cutting_edges: int) -> float:
|
||||
"""F = N × fz × z"""
|
||||
return spindle_speed * feed_per_tooth * cutting_edges
|
||||
|
||||
@staticmethod
|
||||
def calculate_mrr(feed_rate: float, axial_depth: float,
|
||||
radial_depth: float) -> float:
|
||||
"""材料去除率 Q = ae × ap × F / 1000 (cm³/min)"""
|
||||
return axial_depth * radial_depth * feed_rate / 1000
|
||||
|
||||
@staticmethod
|
||||
def estimate_machining_time(toolpath_length: float, feed_rate: float,
|
||||
rapid_distance: float = 0,
|
||||
rapid_speed: float = 15000) -> float:
|
||||
"""估算加工时间(分钟)"""
|
||||
cutting_time = toolpath_length / feed_rate / 60 if feed_rate > 0 else 0
|
||||
rapid_time = rapid_distance / rapid_speed / 60 if rapid_speed > 0 else 0
|
||||
return cutting_time + rapid_time
|
||||
|
||||
@classmethod
|
||||
def calculate_all(cls, tool: Dict) -> Dict:
|
||||
"""计算完整切削参数"""
|
||||
sf = tool["speeds_feeds"]
|
||||
rpm = cls.calculate_spindle_speed(sf["cutting_speed"], tool["diameter"])
|
||||
feed = cls.calculate_feed_rate(rpm, sf["feed_per_tooth"], tool["cutting_edges"])
|
||||
mrr = cls.calculate_mrr(feed, sf["axial_depth"], sf["radial_depth"])
|
||||
|
||||
return {
|
||||
"tool_id": tool.get("tool_id", "unknown"),
|
||||
"tool_type": tool["type"],
|
||||
"tool_diameter": tool["diameter"],
|
||||
"spindle_speed_rpm": rpm,
|
||||
"feed_rate_mm_min": round(feed, 1),
|
||||
"axial_depth_mm": sf["axial_depth"],
|
||||
"radial_depth_mm": sf["radial_depth"],
|
||||
"material_removal_rate_cm3_min": round(mrr, 2),
|
||||
"cutting_speed_m_min": round(sf["cutting_speed"], 1),
|
||||
}
|
||||
|
||||
|
||||
class RoughingToolpathGenerator:
|
||||
"""粗加工刀路生成器"""
|
||||
|
||||
def generate_z_level_roughing(self, stock_bbox: Dict, cavity_bbox: Dict,
|
||||
tool: Dict, cutting_params: Dict,
|
||||
stock_allowance: float = 0.5) -> Dict[str, Any]:
|
||||
"""
|
||||
Z层等高粗加工
|
||||
|
||||
策略:从顶面逐层向下铣削,每层切深为 axial_depth
|
||||
|
||||
Args:
|
||||
stock_bbox: 毛坯边界框
|
||||
cavity_bbox: 型腔边界框
|
||||
tool: 刀具参数
|
||||
cutting_params: 切削参数
|
||||
stock_allowance: 精加工余量 mm
|
||||
|
||||
Returns:
|
||||
粗加工刀路方案
|
||||
"""
|
||||
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
|
||||
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
|
||||
total_depth = z_max - z_min
|
||||
|
||||
axial_depth = cutting_params["axial_depth_mm"]
|
||||
num_levels = max(1, math.ceil(total_depth / axial_depth))
|
||||
|
||||
actual_depth = total_depth / num_levels
|
||||
|
||||
stepover = cutting_params["radial_depth_mm"]
|
||||
|
||||
levels = []
|
||||
for i in range(num_levels):
|
||||
z_level = z_max - (i + 1) * actual_depth + stock_allowance
|
||||
levels.append({
|
||||
"z": round(z_level, 2),
|
||||
"depth": round(actual_depth, 2),
|
||||
"level_index": i + 1,
|
||||
})
|
||||
|
||||
toolpath_length = self._estimate_roughing_length(
|
||||
cavity_bbox, num_levels, stepover
|
||||
)
|
||||
|
||||
machining_time = CuttingParamsCalculator.estimate_machining_time(
|
||||
toolpath_length, cutting_params["feed_rate_mm_min"]
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "z_level_roughing",
|
||||
"tool": cutting_params,
|
||||
"levels": levels,
|
||||
"num_levels": num_levels,
|
||||
"stepover": stepover,
|
||||
"stock_allowance": stock_allowance,
|
||||
"total_depth": round(total_depth, 2),
|
||||
"estimated_toolpath_length": round(toolpath_length, 1),
|
||||
"estimated_time_min": round(machining_time, 1),
|
||||
"approach_type": "helical_ramp",
|
||||
"ramp_angle": 2.0,
|
||||
}
|
||||
|
||||
def _estimate_roughing_length(self, cavity_bbox: Dict, num_levels: int,
|
||||
stepover: float) -> float:
|
||||
"""估算粗加工刀路总长度"""
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
width = dims[0]
|
||||
length = dims[1]
|
||||
|
||||
passes_per_level = max(1, int(width / stepover))
|
||||
length_per_pass = length
|
||||
length_per_level = passes_per_level * length_per_pass * 1.1
|
||||
|
||||
return length_per_level * num_levels
|
||||
|
||||
|
||||
class FinishingToolpathGenerator:
|
||||
"""精加工刀路生成器"""
|
||||
|
||||
def generate_parallel_finishing(self, cavity_bbox: Dict, tool: Dict,
|
||||
cutting_params: Dict,
|
||||
stepover: float = 0.3,
|
||||
angle: float = 0.0) -> Dict[str, Any]:
|
||||
"""
|
||||
平行铣削精加工
|
||||
|
||||
Args:
|
||||
cavity_bbox: 型腔边界框
|
||||
tool: 刀具参数
|
||||
cutting_params: 切削参数
|
||||
stepover: 步距 mm
|
||||
angle: 加工角度
|
||||
|
||||
Returns:
|
||||
精加工刀路方案
|
||||
"""
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
width = dims[0]
|
||||
length = dims[1]
|
||||
|
||||
num_passes = max(1, int(width / stepover) + 1)
|
||||
|
||||
surface_roughness = self._estimate_surface_roughness(
|
||||
tool["diameter"], stepover
|
||||
)
|
||||
|
||||
toolpath_length = num_passes * length * 1.05
|
||||
|
||||
machining_time = CuttingParamsCalculator.estimate_machining_time(
|
||||
toolpath_length, cutting_params["feed_rate_mm_min"]
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "parallel_finishing",
|
||||
"tool": cutting_params,
|
||||
"stepover": stepover,
|
||||
"angle": angle,
|
||||
"num_passes": num_passes,
|
||||
"surface_roughness_ra": round(surface_roughness, 3),
|
||||
"estimated_toolpath_length": round(toolpath_length, 1),
|
||||
"estimated_time_min": round(machining_time, 1),
|
||||
"cutting_direction": "one_way",
|
||||
"stepover_type": "scallop",
|
||||
}
|
||||
|
||||
def generate_contour_finishing(self, cavity_bbox: Dict, tool: Dict,
|
||||
cutting_params: Dict,
|
||||
z_step: float = 0.5) -> Dict[str, Any]:
|
||||
"""
|
||||
等高线精加工
|
||||
|
||||
Args:
|
||||
cavity_bbox: 型腔边界框
|
||||
tool: 刀具参数
|
||||
cutting_params: 切削参数
|
||||
z_step: Z方向步距 mm
|
||||
|
||||
Returns:
|
||||
等高线精加工方案
|
||||
"""
|
||||
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
|
||||
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
|
||||
total_depth = z_max - z_min
|
||||
|
||||
num_levels = max(1, int(total_depth / z_step) + 1)
|
||||
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
perimeter = 2 * (dims[0] + dims[1])
|
||||
|
||||
toolpath_length = num_levels * perimeter * 1.1
|
||||
|
||||
machining_time = CuttingParamsCalculator.estimate_machining_time(
|
||||
toolpath_length, cutting_params["feed_rate_mm_min"]
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "contour_finishing",
|
||||
"tool": cutting_params,
|
||||
"z_step": z_step,
|
||||
"num_levels": num_levels,
|
||||
"estimated_toolpath_length": round(toolpath_length, 1),
|
||||
"estimated_time_min": round(machining_time, 1),
|
||||
}
|
||||
|
||||
def _estimate_surface_roughness(self, tool_diameter: float,
|
||||
stepover: float) -> float:
|
||||
"""估算表面粗糙度 Ra"""
|
||||
if tool_diameter <= 0:
|
||||
return 1.0
|
||||
r = tool_diameter / 2
|
||||
h = stepover ** 2 / (8 * r) if r > 0 else stepover
|
||||
return h * 0.25
|
||||
|
||||
|
||||
class GCodePostProcessor:
|
||||
"""G代码后处理器"""
|
||||
|
||||
def __init__(self, controller: str = "fanuc"):
|
||||
self.controller = controller
|
||||
self.dialects = {
|
||||
"fanuc": {
|
||||
"rapid": "G00", "linear": "G01",
|
||||
"cw_arc": "G02", "ccw_arc": "G03",
|
||||
"absolute": "G90", "incremental": "G91",
|
||||
"tool_change": "M06", "spindle_on": "M03",
|
||||
"spindle_off": "M05", "coolant_on": "M08",
|
||||
"coolant_off": "M09", "program_end": "M30",
|
||||
"length_comp": "G43", "xy_plane": "G17",
|
||||
"cancel_comp": "G40", "cancel_canned": "G80",
|
||||
},
|
||||
"siemens": {
|
||||
"rapid": "G00", "linear": "G01",
|
||||
"cw_arc": "G02", "ccw_arc": "G03",
|
||||
"absolute": "G90", "incremental": "G91",
|
||||
"tool_change": "M06", "spindle_on": "M03",
|
||||
"spindle_off": "M05", "coolant_on": "M08",
|
||||
"coolant_off": "M09", "program_end": "M30",
|
||||
"length_comp": "G43", "xy_plane": "G17",
|
||||
"cancel_comp": "G40", "cancel_canned": "G80",
|
||||
},
|
||||
}
|
||||
|
||||
def generate_gcode(self, operations: List[Dict],
|
||||
program_number: int = 1000,
|
||||
program_name: str = "MOLD_CAVITY") -> str:
|
||||
"""
|
||||
生成完整G代码程序
|
||||
|
||||
Args:
|
||||
operations: 加工操作列表
|
||||
program_number: 程序号
|
||||
program_name: 程序名
|
||||
|
||||
Returns:
|
||||
G代码字符串
|
||||
"""
|
||||
d = self.dialects.get(self.controller, self.dialects["fanuc"])
|
||||
lines = []
|
||||
|
||||
lines.append(f"%")
|
||||
lines.append(f"O{program_number} ({program_name})")
|
||||
lines.append(f"{d['xy_plane']} {d['cancel_comp']} {d['cancel_canned']} {d['absolute']}")
|
||||
lines.append(f"G54")
|
||||
lines.append("")
|
||||
|
||||
for op_idx, op in enumerate(operations):
|
||||
strategy = op.get("strategy", "unknown")
|
||||
tool_info = op.get("tool", {})
|
||||
tool_id = tool_info.get("tool_id", "T01")
|
||||
tool_num = op_idx + 1
|
||||
|
||||
lines.append(f"(=== 操作 {tool_num}: {strategy} ===)")
|
||||
|
||||
tool_type = tool_info.get("tool_type", "endmill")
|
||||
tool_dia = tool_info.get("tool_diameter", 10)
|
||||
lines.append(f"(刀具: {tool_type} D{tool_dia:.1f}mm)")
|
||||
|
||||
lines.append(f"T{tool_num:02d} {d['tool_change']}")
|
||||
lines.append(f"{d['length_comp']} H{tool_num:02d} Z100.0")
|
||||
|
||||
rpm = tool_info.get("spindle_speed_rpm", 3000)
|
||||
lines.append(f"S{rpm} {d['spindle_on']}")
|
||||
|
||||
lines.append(f"{d['rapid']} X0 Y0 Z10.0")
|
||||
lines.append(f"{d['coolant_on']}")
|
||||
lines.append("")
|
||||
|
||||
feed = tool_info.get("feed_rate_mm_min", 500)
|
||||
levels = op.get("levels", [])
|
||||
|
||||
if strategy == "z_level_roughing" and levels:
|
||||
for level in levels:
|
||||
z = level["z"]
|
||||
lines.append(f"(--- Z层 {level['level_index']}: Z={z:.2f} ---)")
|
||||
lines.append(f"{d['linear']} Z{z:.2f} F{int(feed * 0.5)}")
|
||||
lines.append(f"{d['linear']} X50.0 Y30.0 F{feed}")
|
||||
lines.append(f"{d['linear']} X-50.0 Y30.0")
|
||||
lines.append(f"{d['linear']} X-50.0 Y-30.0")
|
||||
lines.append(f"{d['linear']} X50.0 Y-30.0")
|
||||
lines.append(f"{d['rapid']} Z10.0")
|
||||
lines.append("")
|
||||
|
||||
elif strategy in ("parallel_finishing", "contour_finishing"):
|
||||
num_passes = op.get("num_passes", 10)
|
||||
stepover = op.get("stepover", 0.3)
|
||||
|
||||
for i in range(num_passes):
|
||||
y = i * stepover - 30
|
||||
lines.append(f"{d['linear']} Z-5.0 F{int(feed * 0.3)}")
|
||||
lines.append(f"{d['linear']} X50.0 Y{y:.2f} F{feed}")
|
||||
lines.append(f"{d['linear']} X-50.0 Y{y:.2f}")
|
||||
lines.append(f"{d['rapid']} Z5.0")
|
||||
lines.append("")
|
||||
|
||||
else:
|
||||
lines.append(f"(策略 {strategy} 的刀路数据)")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"{d['coolant_off']}")
|
||||
lines.append(f"{d['spindle_off']}")
|
||||
lines.append(f"{d['rapid']} Z100.0")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"{d['coolant_off']}")
|
||||
lines.append(f"{d['spindle_off']}")
|
||||
lines.append(f"G28 G91 Z0")
|
||||
lines.append(f"G28 G91 X0 Y0")
|
||||
lines.append(f"{d['program_end']}")
|
||||
lines.append(f"%")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class MoldCAMDesigner:
|
||||
"""模具CAM综合设计器"""
|
||||
|
||||
def __init__(self):
|
||||
self.tool_lib = ToolLibrary()
|
||||
self.params_calc = CuttingParamsCalculator()
|
||||
self.roughing_gen = RoughingToolpathGenerator()
|
||||
self.finishing_gen = FinishingToolpathGenerator()
|
||||
self.post_processor = GCodePostProcessor()
|
||||
|
||||
def design_mold_cam(self, cavity_bbox: Dict, stock_bbox: Dict,
|
||||
mold_steel: str = "P20",
|
||||
surface_quality: str = "standard",
|
||||
controller: str = "fanuc",
|
||||
program_number: int = 1000) -> Dict[str, Any]:
|
||||
"""
|
||||
综合设计模具CAM方案
|
||||
|
||||
Args:
|
||||
cavity_bbox: 型腔边界框
|
||||
stock_bbox: 毛坯边界框
|
||||
mold_steel: 模具钢材料
|
||||
surface_quality: 表面质量要求
|
||||
controller: 数控系统
|
||||
program_number: 程序号
|
||||
|
||||
Returns:
|
||||
完整的CAM方案
|
||||
"""
|
||||
logger.info(f"开始模具CAM设计: 钢材={mold_steel}, 质量={surface_quality}")
|
||||
|
||||
roughing_tool = ToolLibrary.select_roughing_tool(
|
||||
self._estimate_cavity_volume(cavity_bbox),
|
||||
steel=mold_steel
|
||||
)
|
||||
roughing_params = CuttingParamsCalculator.calculate_all(roughing_tool)
|
||||
|
||||
finishing_tool = ToolLibrary.select_finishing_tool(
|
||||
surface_quality=surface_quality,
|
||||
steel=mold_steel
|
||||
)
|
||||
finishing_params = CuttingParamsCalculator.calculate_all(finishing_tool)
|
||||
|
||||
roughing_op = self.roughing_gen.generate_z_level_roughing(
|
||||
stock_bbox, cavity_bbox, roughing_tool, roughing_params
|
||||
)
|
||||
|
||||
finishing_op = self.finishing_gen.generate_parallel_finishing(
|
||||
cavity_bbox, finishing_tool, finishing_params
|
||||
)
|
||||
|
||||
operations = [roughing_op, finishing_op]
|
||||
|
||||
gcode = self.post_processor.generate_gcode(
|
||||
operations, program_number=program_number
|
||||
)
|
||||
|
||||
total_time = (
|
||||
roughing_op.get("estimated_time_min", 0) +
|
||||
finishing_op.get("estimated_time_min", 0)
|
||||
)
|
||||
|
||||
result = {
|
||||
"operations": operations,
|
||||
"tools": {
|
||||
"roughing": roughing_params,
|
||||
"finishing": finishing_params,
|
||||
},
|
||||
"gcode": gcode,
|
||||
"gcode_lines": len(gcode.split("\n")),
|
||||
"summary": {
|
||||
"total_operations": len(operations),
|
||||
"total_estimated_time_min": round(total_time, 1),
|
||||
"mold_steel": mold_steel,
|
||||
"surface_quality": surface_quality,
|
||||
"controller": controller,
|
||||
},
|
||||
"recommendations": self._generate_cam_recommendations(
|
||||
roughing_op, finishing_op, mold_steel
|
||||
),
|
||||
}
|
||||
|
||||
logger.info(f"CAM设计完成: {len(operations)} 个工序, "
|
||||
f"预计 {total_time:.1f} 分钟")
|
||||
|
||||
return result
|
||||
|
||||
def _estimate_cavity_volume(self, cavity_bbox: Dict) -> float:
|
||||
"""估算型腔体积"""
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
return dims[0] * dims[1] * dims[2]
|
||||
|
||||
def _generate_cam_recommendations(self, roughing: Dict, finishing: Dict,
|
||||
steel: str) -> List[str]:
|
||||
"""生成CAM建议"""
|
||||
recs = []
|
||||
|
||||
roughing_time = roughing.get("estimated_time_min", 0)
|
||||
if roughing_time > 120:
|
||||
recs.append("粗加工时间较长,建议使用更大直径刀具或增加切削深度")
|
||||
|
||||
finishing_roughness = finishing.get("surface_roughness_ra", 0)
|
||||
if finishing_roughness > 0.8:
|
||||
recs.append("表面粗糙度偏高,建议减小步距或使用更小直径球头刀")
|
||||
|
||||
if steel in ("S136", "H13"):
|
||||
recs.append(f"高硬度钢材({steel}),建议使用涂层刀具并降低切削速度")
|
||||
recs.append("建议增加半精加工工序减少精加工余量")
|
||||
|
||||
recs.append("加工前需确认工件坐标系零点位置")
|
||||
recs.append("首件加工建议降低进给率20%进行试切")
|
||||
|
||||
return recs
|
||||
+133
-566
@@ -1,48 +1,28 @@
|
||||
# src/core/mold_generator.py
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Tuple, Optional, Callable
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
import numpy as np
|
||||
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
from OCC.Core.Geom import Geom_Plane
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Vec, gp_Trsf
|
||||
from OCC.Core.TopTools import TopTools_ListOfShape
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, TopoDS_Edge, TopoDS_Vertex
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
|
||||
from OCC.Core.TopoDS import TopoDS_Face
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib_Add
|
||||
|
||||
from models.schemas import create_mold_cavity_data, create_mold_key_info
|
||||
from utils.logger import get_logger
|
||||
from core.base_mold_generator import BaseMoldGenerator
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MoldCavityGenerator:
|
||||
class MoldCavityGenerator(BaseMoldGenerator):
|
||||
"""模具型腔生成器 - 基于产品模型生成Cavity和Core"""
|
||||
|
||||
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
|
||||
material_density: float = 1.05):
|
||||
"""
|
||||
初始化模具生成器
|
||||
super().__init__(shrinkage_rate, draft_angle, material_density)
|
||||
|
||||
Args:
|
||||
shrinkage_rate: 收缩率(默认0.5% for ABS)
|
||||
draft_angle: 拔模角(默认2度)
|
||||
material_density: 材料密度 g/cm³(默认1.05 for ABS)
|
||||
"""
|
||||
self.shrinkage_rate = shrinkage_rate
|
||||
self.draft_angle = draft_angle # 度
|
||||
self.material_density = material_density # g/cm³
|
||||
|
||||
# 常用塑料材料密度(g/cm³)
|
||||
self.material_densities = {
|
||||
"ABS": 1.05,
|
||||
"PP": 0.90,
|
||||
@@ -54,26 +34,9 @@ class MoldCavityGenerator:
|
||||
"PMMA": 1.18
|
||||
}
|
||||
|
||||
# 分型面检测参数
|
||||
self.parting_line_tolerance = 0.1
|
||||
self.max_draft_angle = 5.0
|
||||
|
||||
# AI 模型接口(预留)
|
||||
self.ai_parting_detector: Optional[Any] = None
|
||||
self.ai_draft_analyzer: Optional[Any] = None
|
||||
|
||||
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
|
||||
"""
|
||||
设置 AI 模型接口(预留)
|
||||
|
||||
Args:
|
||||
parting_detector: 分型面检测 AI 模型
|
||||
draft_analyzer: 拔模分析 AI 模型
|
||||
"""
|
||||
self.ai_parting_detector = parting_detector
|
||||
self.ai_draft_analyzer = draft_analyzer
|
||||
logger.info("AI 模型接口已设置")
|
||||
|
||||
def set_material(self, material: str):
|
||||
"""设置产品材料"""
|
||||
if material in self.material_densities:
|
||||
@@ -88,30 +51,25 @@ class MoldCavityGenerator:
|
||||
|
||||
Returns:
|
||||
{
|
||||
"cavity": cavity_shape, # 型腔(产品外部)
|
||||
"core": core_shape, # 型芯(产品内部)
|
||||
"parting_surface": parting_surface, # 分型面
|
||||
"parting_line": parting_line # 分型线
|
||||
"cavity": cavity_shape,
|
||||
"core": core_shape,
|
||||
"parting_surface": parting_surface,
|
||||
"parting_line": parting_line
|
||||
}
|
||||
"""
|
||||
logger.info("开始生成模具型腔...")
|
||||
|
||||
try:
|
||||
# Step 1: 分析产品几何
|
||||
analysis = self._analyze_product_geometry(product_shape)
|
||||
|
||||
# Step 2: 检测分型面和分型线
|
||||
parting_surface, parting_line = self._detect_parting_surface(
|
||||
product_shape, analysis
|
||||
)
|
||||
|
||||
# Step 3: 应用收缩率补偿
|
||||
scaled_shape = self._apply_shrinkage_compensation(product_shape)
|
||||
|
||||
# Step 4: 添加拔模角
|
||||
drafted_shape = self._apply_draft_angles(scaled_shape, parting_surface)
|
||||
|
||||
# Step 5: 分离型腔和型芯
|
||||
cavity, core = self._split_cavity_core(drafted_shape, parting_surface)
|
||||
|
||||
logger.info("模具型腔生成完成")
|
||||
@@ -140,11 +98,9 @@ class MoldCavityGenerator:
|
||||
parting_surface = cavity_data["parting_surface"]
|
||||
analysis = cavity_data["analysis"]
|
||||
|
||||
# 提取型腔几何数据
|
||||
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
|
||||
core_geometry = self._extract_shape_geometry(core, "core")
|
||||
|
||||
# 提取分型面数据
|
||||
parting_geometry = self._extract_parting_surface_geometry(
|
||||
parting_surface
|
||||
)
|
||||
@@ -158,10 +114,10 @@ class MoldCavityGenerator:
|
||||
"unit": "mm"
|
||||
},
|
||||
"product_analysis": {
|
||||
"bounding_box": analysis.get("bounding_box", {}), # 使用get方法
|
||||
"volume": analysis.get("volume", 0), # 使用get方法
|
||||
"surface_area": analysis.get("surface_area", 0), # 使用get方法
|
||||
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0]) # 使用get方法
|
||||
"bounding_box": analysis.get("bounding_box", {}),
|
||||
"volume": analysis.get("volume", 0),
|
||||
"surface_area": analysis.get("surface_area", 0),
|
||||
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0])
|
||||
},
|
||||
"mold_cavities": {
|
||||
"cavity": cavity_geometry,
|
||||
@@ -219,41 +175,6 @@ class MoldCavityGenerator:
|
||||
|
||||
# ==================== 内部方法 ====================
|
||||
|
||||
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
|
||||
"""分析产品几何属性"""
|
||||
# 计算体积属性
|
||||
volume_props = GProp_GProps()
|
||||
brepgprop.VolumeProperties(shape, volume_props)
|
||||
|
||||
# 计算表面积属性
|
||||
surface_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(shape, surface_props)
|
||||
|
||||
# 计算边界框
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
return {
|
||||
"volume": volume_props.Mass(),
|
||||
"surface_area": surface_props.Mass(),
|
||||
"center_of_mass": [
|
||||
volume_props.CentreOfMass().X(),
|
||||
volume_props.CentreOfMass().Y(),
|
||||
volume_props.CentreOfMass().Z()
|
||||
],
|
||||
"bounding_box": {
|
||||
"min": [xmin, ymin, zmin],
|
||||
"max": [xmax, ymax, zmax],
|
||||
"dimensions": [xmax - xmin, ymax - ymin, zmax - zmin],
|
||||
"center": [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2]
|
||||
},
|
||||
"inertia_matrix": self._get_inertia_matrix(volume_props)
|
||||
}
|
||||
|
||||
def _detect_parting_surface(self, shape: Any, analysis: Dict) -> Tuple[Any, List]:
|
||||
"""
|
||||
检测分型面和分型线
|
||||
@@ -263,17 +184,15 @@ class MoldCavityGenerator:
|
||||
2. 基于法向量分析的几何方法
|
||||
3. 简化方法(基于边界框)
|
||||
"""
|
||||
# 1. 尝试使用 AI 模型
|
||||
if self.ai_parting_detector is not None:
|
||||
try:
|
||||
logger.info("使用 AI 模型检测分型面")
|
||||
ai_result = self.ai_parting_detector.detect(shape, analysis)
|
||||
if ai_result:
|
||||
return self._create_parting_surface_from_ai(ai_result, analysis)
|
||||
return self._create_parting_surface_from_ai(ai_result, analysis, shape)
|
||||
except Exception as e:
|
||||
logger.warning(f"AI 分型面检测失败,回退到几何方法:{e}")
|
||||
|
||||
# 2. 基于法向量分析的几何方法
|
||||
try:
|
||||
logger.info("使用法向量分析检测分型面")
|
||||
optimal_direction = self._analyze_face_normals(shape)
|
||||
@@ -282,7 +201,6 @@ class MoldCavityGenerator:
|
||||
)
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
# 计算真实分型线(产品与分型面的交线)
|
||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
||||
|
||||
return parting_surface, parting_line
|
||||
@@ -290,164 +208,134 @@ class MoldCavityGenerator:
|
||||
except Exception as e:
|
||||
logger.warning(f"法向量分析失败,使用简化方法:{e}")
|
||||
|
||||
# 3. 简化方法(回退)
|
||||
logger.info("使用简化方法检测分型面")
|
||||
return self._simple_parting_surface(analysis)
|
||||
return self._simple_parting_surface(shape, analysis)
|
||||
|
||||
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
|
||||
"""应用收缩率补偿(放大模型)"""
|
||||
scale_factor = 1.0 + self.shrinkage_rate
|
||||
|
||||
# 创建缩放变换
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
|
||||
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
|
||||
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
|
||||
|
||||
return scaled_shape
|
||||
|
||||
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
|
||||
"""添加拔模角(简化实现)"""
|
||||
# 实际实现需要复杂的拔模面处理
|
||||
# 这里返回原始形状(假设已在CAD中处理)
|
||||
logger.warning("拔模角处理为简化实现,建议在设计阶段处理")
|
||||
return shape
|
||||
|
||||
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
|
||||
"""分离型腔和型芯
|
||||
|
||||
型腔(Cavity): 模具中形成产品外表面的部分,是产品形状的负形
|
||||
型芯(Core): 模具中形成产品内表面的部分,是产品形状的正形
|
||||
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_SOLID
|
||||
|
||||
# 获取产品边界框
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
# 计算模具块尺寸(比产品大一定余量)
|
||||
margin = 20 # mm
|
||||
mold_xmin = xmin - margin
|
||||
mold_ymin = ymin - margin
|
||||
mold_zmin = zmin - margin
|
||||
mold_xmax = xmax + margin
|
||||
mold_ymax = ymax + margin
|
||||
mold_zmax = zmax + margin
|
||||
|
||||
# 创建模具块
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
|
||||
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
|
||||
).Shape()
|
||||
|
||||
# 型腔 = 模具块 - 产品(布尔减法)
|
||||
cavity_operation = BRepAlgoAPI_Cut(mold_block, shape)
|
||||
if cavity_operation.IsDone():
|
||||
cavity = cavity_operation.Shape()
|
||||
logger.info("型腔生成成功(模具块减去产品)")
|
||||
else:
|
||||
logger.warning("型腔布尔运算失败,使用原始形状")
|
||||
cavity = mold_block
|
||||
|
||||
# 型芯 = 产品形状本身(收缩补偿后)
|
||||
core = shape
|
||||
logger.info("型芯 = 产品形状")
|
||||
|
||||
return cavity, core
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"型腔分离失败: {e}")
|
||||
return shape, shape
|
||||
|
||||
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
|
||||
"""提取形状几何数据为JSON格式"""
|
||||
try:
|
||||
# 网格化
|
||||
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
|
||||
mesh.Perform()
|
||||
|
||||
# 提取顶点和面
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.Poly import Poly_Triangulation
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
|
||||
vertices = []
|
||||
faces = []
|
||||
分析产品表面的法向量分布,找出最优分型方向
|
||||
|
||||
原理:
|
||||
- 统计所有面的法向量
|
||||
- 选择法向量变化最小的方向作为分型方向
|
||||
- 避免倒扣(undercut)区域
|
||||
"""
|
||||
face_normals = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
vertex_index = 0
|
||||
|
||||
while explorer.More():
|
||||
# 使用 explorer.Current() 直接获取面
|
||||
face = explorer.Current()
|
||||
location = TopLoc_Location()
|
||||
triangulation = BRep_Tool.Triangulation(face, location)
|
||||
face = TopoDS_Face(explorer.Current())
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
if triangulation:
|
||||
# 提取顶点
|
||||
nb_nodes = triangulation.NbNodes()
|
||||
for i in range(1, nb_nodes + 1):
|
||||
node = triangulation.Node(i)
|
||||
# 应用位置变换
|
||||
transformed = node.Transformed(location.Transformation())
|
||||
vertices.extend([
|
||||
float(transformed.X()),
|
||||
float(transformed.Y()),
|
||||
float(transformed.Z())
|
||||
])
|
||||
try:
|
||||
if surface.GetType() == 0:
|
||||
normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(face, bbox)
|
||||
normal = gp_Dir(0, 0, 1)
|
||||
|
||||
# 提取三角形面
|
||||
nb_triangles = triangulation.NbTriangles()
|
||||
for i in range(1, nb_triangles + 1):
|
||||
triangle = triangulation.Triangle(i)
|
||||
# 三角形顶点索引需要加上之前的顶点数量
|
||||
idx1 = triangle.Value(1) + vertex_index - 1
|
||||
idx2 = triangle.Value(2) + vertex_index - 1
|
||||
idx3 = triangle.Value(3) + vertex_index - 1
|
||||
faces.extend([int(idx1), int(idx2), int(idx3)])
|
||||
|
||||
vertex_index += nb_nodes
|
||||
face_normals.append(normal)
|
||||
except Exception as e:
|
||||
logger.debug(f"面法向量计算失败:{e}")
|
||||
|
||||
explorer.Next()
|
||||
|
||||
vertex_count = len(vertices) // 3
|
||||
face_count = len(faces) // 3
|
||||
if not face_normals:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": vertices,
|
||||
"faces": faces,
|
||||
"vertex_count": vertex_count,
|
||||
"face_count": face_count,
|
||||
"triangulation": "BRepMesh三角化"
|
||||
}
|
||||
avg_x = sum(n.X() for n in face_normals) / len(face_normals)
|
||||
avg_y = sum(n.Y() for n in face_normals) / len(face_normals)
|
||||
avg_z = sum(n.Z() for n in face_normals) / len(face_normals)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{shape_type}几何提取失败: {e}")
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": [],
|
||||
"faces": [],
|
||||
"vertex_count": 0,
|
||||
"face_count": 0,
|
||||
"triangulation": f"提取失败: {str(e)}"
|
||||
}
|
||||
length = np.sqrt(avg_x**2 + avg_y**2 + avg_z**2)
|
||||
if length > 0.001:
|
||||
return gp_Dir(avg_x/length, avg_y/length, avg_z/length)
|
||||
else:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
def _create_optimal_parting_plane(self, shape: Any, analysis: Dict,
|
||||
direction: gp_Dir) -> gp_Pln:
|
||||
"""
|
||||
创建最优分型面
|
||||
|
||||
Args:
|
||||
shape: 产品形状
|
||||
analysis: 几何分析结果
|
||||
direction: 分型方向(法向量)
|
||||
|
||||
Returns:
|
||||
gp_Pln: 分型面方程
|
||||
"""
|
||||
bbox = analysis["bounding_box"]
|
||||
center = bbox["center"]
|
||||
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(center[0], center[1], center[2]),
|
||||
direction
|
||||
)
|
||||
|
||||
logger.info(f"创建分型面:原点=({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f}), "
|
||||
f"法向量=({direction.X():.3f}, {direction.Y():.3f}, {direction.Z():.3f})")
|
||||
|
||||
return parting_plane
|
||||
|
||||
def _simple_parting_surface(self, shape: Any, analysis: Dict) -> Tuple[Any, List]:
|
||||
"""简化的分型面检测(回退方案)"""
|
||||
bbox = analysis["bounding_box"]
|
||||
center_z = bbox["center"][2]
|
||||
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(0, 0, center_z),
|
||||
gp_Dir(0, 0, 1)
|
||||
)
|
||||
parting_surface = BRepBuilderAPI_MakeFace(
|
||||
parting_plane,
|
||||
bbox["min"][0] - 10, bbox["max"][0] + 10,
|
||||
bbox["min"][1] - 10, bbox["max"][1] + 10
|
||||
).Face()
|
||||
|
||||
parting_line = self._simple_parting_line(shape)
|
||||
|
||||
return parting_surface, parting_line
|
||||
|
||||
def _create_parting_surface_from_ai(self, ai_result: Dict,
|
||||
analysis: Dict, shape: Any = None) -> Tuple[Any, List]:
|
||||
"""
|
||||
从 AI 模型结果创建分型面(预留接口)
|
||||
|
||||
Args:
|
||||
ai_result: AI 模型输出,应包含:
|
||||
- origin: [x, y, z] 平面原点
|
||||
- normal: [nx, ny, nz] 法向量
|
||||
analysis: 几何分析结果
|
||||
shape: 产品形状(用于计算分型线)
|
||||
|
||||
Returns:
|
||||
(parting_surface, parting_line)
|
||||
"""
|
||||
origin = ai_result.get("origin", [0, 0, 0])
|
||||
normal = ai_result.get("normal", [0, 0, 1])
|
||||
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(origin[0], origin[1], origin[2]),
|
||||
gp_Dir(normal[0], normal[1], normal[2])
|
||||
)
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
if "parting_line" in ai_result:
|
||||
parting_line = ai_result["parting_line"]
|
||||
elif shape is not None:
|
||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
||||
else:
|
||||
parting_line = []
|
||||
|
||||
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
|
||||
return parting_surface, parting_line
|
||||
|
||||
def _extract_parting_surface_geometry(self, surface: Any) -> Dict[str, Any]:
|
||||
"""提取分型面几何数据"""
|
||||
# 尝试从surface获取边界信息,失败则使用默认值
|
||||
try:
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
adaptor = BRepAdaptor_Surface(surface)
|
||||
u_min, u_max = adaptor.FirstUParameter(), adaptor.LastUParameter()
|
||||
v_min, v_max = adaptor.FirstVParameter(), adaptor.LastVParameter()
|
||||
@@ -463,14 +351,6 @@ class MoldCavityGenerator:
|
||||
"v_range": [-200, 200]
|
||||
}
|
||||
|
||||
# 分型面是水平面,法向量为 [0, 0, 1],原点在 Z 轴中心
|
||||
return {
|
||||
"type": "plane",
|
||||
"normal": [0, 0, 1],
|
||||
"origin": [0, 0, 0],
|
||||
"bounds": bounds
|
||||
}
|
||||
|
||||
return {
|
||||
"type": "plane",
|
||||
"normal": [0, 0, 1],
|
||||
@@ -481,23 +361,19 @@ class MoldCavityGenerator:
|
||||
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
|
||||
"""估算模具尺寸"""
|
||||
product_bbox = analysis["bounding_box"]["dimensions"]
|
||||
|
||||
# 模具通常比产品大20-50mm
|
||||
margin = 30 # mm
|
||||
margin = 30
|
||||
|
||||
return {
|
||||
"length": product_bbox[0] + 2 * margin,
|
||||
"width": product_bbox[1] + 2 * margin,
|
||||
"height": product_bbox[2] + 2 * margin + 100, # 增加100mm用于模架
|
||||
"height": product_bbox[2] + 2 * margin + 100,
|
||||
"margin": margin
|
||||
}
|
||||
|
||||
def _calculate_clamping_force(self, analysis: Dict) -> str:
|
||||
"""估算锁模力"""
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000 # mm³ → cm³
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
|
||||
# 经验公式: 锁模力 ≈ 投影面积 × 压力 × 安全系数
|
||||
# 简化估算
|
||||
if volume_cm3 < 10:
|
||||
return "50-100 吨"
|
||||
elif volume_cm3 < 100:
|
||||
@@ -509,18 +385,8 @@ class MoldCavityGenerator:
|
||||
|
||||
def _get_recommended_material(self) -> str:
|
||||
"""推荐模具材料"""
|
||||
# 根据产品产量推荐模具材料
|
||||
# 小批量 (<5000件): 铝合金
|
||||
# 中批量 (5000-50000件): P20钢
|
||||
# 大批量 (>50000件): H13钢
|
||||
return "Aluminum Alloy 7075 (铝合金模具)"
|
||||
|
||||
def _calculate_product_weight(self, analysis: Dict) -> str:
|
||||
"""计算产品重量(使用当前材料密度)"""
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
weight_g = volume_cm3 * self.material_density
|
||||
return f"{weight_g:.2f} g"
|
||||
|
||||
def _estimate_wall_thickness(self, analysis: Dict) -> str:
|
||||
"""估算壁厚范围"""
|
||||
volume = analysis.get("volume", 0)
|
||||
@@ -530,7 +396,6 @@ class MoldCavityGenerator:
|
||||
avg_thickness = (volume / surface_area) * 0.6
|
||||
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
|
||||
elif volume > 0:
|
||||
# 如果没有surface_area,基于体积估算
|
||||
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
|
||||
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
|
||||
if bbox_volume > 0:
|
||||
@@ -542,7 +407,6 @@ class MoldCavityGenerator:
|
||||
|
||||
def _calculate_complexity_score(self, analysis: Dict) -> float:
|
||||
"""计算复杂度评分(0-10)"""
|
||||
# 基于体积、表面积比、边界框等
|
||||
volume = analysis.get("volume", 0)
|
||||
surface_area = analysis.get("surface_area", 0)
|
||||
|
||||
@@ -551,7 +415,6 @@ class MoldCavityGenerator:
|
||||
complexity = min(thickness_ratio / 5.0, 10.0)
|
||||
return round(complexity, 1)
|
||||
elif volume > 0:
|
||||
# 如果没有surface_area,基于拓扑复杂度评分
|
||||
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
|
||||
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
|
||||
if bbox_volume > 0:
|
||||
@@ -576,7 +439,6 @@ class MoldCavityGenerator:
|
||||
|
||||
def _identify_weld_line_risk(self, analysis: Dict) -> str:
|
||||
"""识别熔接痕风险"""
|
||||
# 基于几何复杂度判断
|
||||
complexity = self._calculate_complexity_score(analysis)
|
||||
|
||||
if complexity > 7:
|
||||
@@ -588,299 +450,4 @@ class MoldCavityGenerator:
|
||||
|
||||
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
|
||||
"""识别缩痕风险"""
|
||||
thickness = self._estimate_wall_thickness(analysis)
|
||||
# 简化的风险评估
|
||||
return "中 - 建议壁厚均匀性检查"
|
||||
|
||||
def _assess_warpage_risk(self, analysis: Dict) -> str:
|
||||
"""评估翘曲风险"""
|
||||
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
|
||||
aspect_ratio = max(bbox) / min(bbox)
|
||||
|
||||
if aspect_ratio > 5:
|
||||
return "高 - 建议增加加强筋"
|
||||
elif aspect_ratio > 3:
|
||||
return "中 - 需优化冷却"
|
||||
else:
|
||||
return "低"
|
||||
|
||||
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
|
||||
"""获取惯性矩阵"""
|
||||
inertia = props.MatrixOfInertia()
|
||||
return [
|
||||
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
|
||||
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
|
||||
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
|
||||
]
|
||||
|
||||
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
|
||||
"""
|
||||
分析产品表面的法向量分布,找出最优分型方向
|
||||
|
||||
原理:
|
||||
- 统计所有面的法向量
|
||||
- 选择法向量变化最小的方向作为分型方向
|
||||
- 避免倒扣(undercut)区域
|
||||
"""
|
||||
from OCC.Core.TopoDS import TopoDS_Compound
|
||||
from OCC.Core.TopTools import TopTools_IndexedMapOfShape
|
||||
|
||||
# 收集所有面的法向量
|
||||
face_normals = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
face = TopoDS_Face(explorer.Current())
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
# 获取面的法向量(在参数中心点)
|
||||
try:
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
normal = gp_Dir()
|
||||
# 从曲面获取法向量
|
||||
if surface.GetType() == 0: # Plane
|
||||
normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
# 对于非平面,使用微分几何计算法向量
|
||||
from OCC.Core.GCPnts import GCPnts_AbscissaPoint
|
||||
from OCC.Core.BRepGProp import brepgprop_VolumeProperties
|
||||
|
||||
# 简化:使用面的边界框中心法向量
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib_Add
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(face, bbox)
|
||||
center = bbox.Center()
|
||||
|
||||
# 估算面法向量(简化)
|
||||
normal = gp_Dir(0, 0, 1) # 默认 Z 方向
|
||||
|
||||
face_normals.append(normal)
|
||||
except Exception as e:
|
||||
logger.debug(f"面法向量计算失败:{e}")
|
||||
|
||||
explorer.Next()
|
||||
|
||||
# 如果没有法向量,返回默认 Z 方向
|
||||
if not face_normals:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
# 统计法向量分布,选择最优方向
|
||||
# 简化实现:计算平均法向量
|
||||
avg_x = sum(n.X() for n in face_normals) / len(face_normals)
|
||||
avg_y = sum(n.Y() for n in face_normals) / len(face_normals)
|
||||
avg_z = sum(n.Z() for n in face_normals) / len(face_normals)
|
||||
|
||||
# 归一化
|
||||
length = np.sqrt(avg_x**2 + avg_y**2 + avg_z**2)
|
||||
if length > 0.001:
|
||||
return gp_Dir(avg_x/length, avg_y/length, avg_z/length)
|
||||
else:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
def _create_optimal_parting_plane(self, shape: Any, analysis: Dict,
|
||||
direction: gp_Dir) -> gp_Pln:
|
||||
"""
|
||||
创建最优分型面
|
||||
|
||||
Args:
|
||||
shape: 产品形状
|
||||
analysis: 几何分析结果
|
||||
direction: 分型方向(法向量)
|
||||
|
||||
Returns:
|
||||
gp_Pln: 分型面方程
|
||||
"""
|
||||
bbox = analysis["bounding_box"]
|
||||
|
||||
# 分型面通过产品的质心
|
||||
center = bbox["center"]
|
||||
|
||||
# 创建平面:通过质心,法向量为分型方向
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(center[0], center[1], center[2]),
|
||||
direction
|
||||
)
|
||||
|
||||
logger.info(f"创建分型面:原点=({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f}), "
|
||||
f"法向量=({direction.X():.3f}, {direction.Y():.3f}, {direction.Z():.3f})")
|
||||
|
||||
return parting_plane
|
||||
|
||||
def _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]:
|
||||
"""
|
||||
计算真实的分型线(产品与分型面的交线)
|
||||
|
||||
使用 BRepAlgoAPI_Section 进行布尔运算求交
|
||||
"""
|
||||
try:
|
||||
# 创建截面运算
|
||||
section = BRepAlgoAPI_Section(shape, parting_surface)
|
||||
section.Build()
|
||||
|
||||
if not section.IsDone():
|
||||
logger.warning("截面运算未完成,使用简化分型线")
|
||||
return self._simple_parting_line(
|
||||
parting_surface,
|
||||
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
|
||||
)
|
||||
|
||||
# 提取交线(边)
|
||||
edges = []
|
||||
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
|
||||
|
||||
while explorer.More():
|
||||
edge = TopoDS_Edge(explorer.Current())
|
||||
|
||||
# 从边提取点
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
first_param = curve.FirstParameter()
|
||||
last_param = curve.LastParameter()
|
||||
|
||||
# 采样点(至少 10 个点)
|
||||
num_points = max(10, int((last_param - first_param) / 0.5))
|
||||
step = (last_param - first_param) / num_points
|
||||
|
||||
for i in range(num_points + 1):
|
||||
param = first_param + i * step
|
||||
point = curve.Value(param)
|
||||
edges.append([point.X(), point.Y(), point.Z()])
|
||||
|
||||
explorer.Next()
|
||||
|
||||
# 如果没有边,使用简化分型线
|
||||
if not edges:
|
||||
logger.warning("未找到交线,使用简化分型线")
|
||||
return self._simple_parting_line(
|
||||
parting_surface,
|
||||
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
|
||||
)
|
||||
|
||||
logger.info(f"计算得到 {len(edges)} 个分型线点")
|
||||
return edges
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分型线计算失败:{e}")
|
||||
return self._simple_parting_line(
|
||||
parting_surface,
|
||||
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
|
||||
)
|
||||
|
||||
def _simple_parting_surface(self, analysis: Dict) -> Tuple[Any, List]:
|
||||
"""简化的分型面检测(回退方案)"""
|
||||
bbox = analysis["bounding_box"]
|
||||
center_z = bbox["center"][2]
|
||||
|
||||
# 创建分型面(XY 平面)
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(0, 0, center_z),
|
||||
gp_Dir(0, 0, 1)
|
||||
)
|
||||
parting_surface = BRepBuilderAPI_MakeFace(
|
||||
parting_plane,
|
||||
bbox["min"][0] - 10, bbox["max"][0] + 10,
|
||||
bbox["min"][1] - 10, bbox["max"][1] + 10
|
||||
).Face()
|
||||
|
||||
# 简化分型线
|
||||
parting_line = self._simple_parting_line(parting_surface, analysis)
|
||||
|
||||
return parting_surface, parting_line
|
||||
|
||||
def _simple_parting_line(self, parting_surface: Any, analysis: Dict) -> List[List[float]]:
|
||||
"""简化的分型线(矩形)"""
|
||||
bbox = analysis["bounding_box"]
|
||||
center_z = bbox["center"][2]
|
||||
|
||||
return [
|
||||
[bbox["min"][0], bbox["min"][1], center_z],
|
||||
[bbox["max"][0], bbox["min"][1], center_z],
|
||||
[bbox["max"][0], bbox["max"][1], center_z],
|
||||
[bbox["min"][0], bbox["max"][1], center_z],
|
||||
[bbox["min"][0], bbox["min"][1], center_z]
|
||||
]
|
||||
|
||||
def _create_parting_surface_from_ai(self, ai_result: Dict,
|
||||
analysis: Dict) -> Tuple[Any, List]:
|
||||
"""
|
||||
从 AI 模型结果创建分型面(预留接口)
|
||||
|
||||
Args:
|
||||
ai_result: AI 模型输出,应包含:
|
||||
- origin: [x, y, z] 平面原点
|
||||
- normal: [nx, ny, nz] 法向量
|
||||
analysis: 几何分析结果
|
||||
|
||||
Returns:
|
||||
(parting_surface, parting_line)
|
||||
"""
|
||||
origin = ai_result.get("origin", [0, 0, 0])
|
||||
normal = ai_result.get("normal", [0, 0, 1])
|
||||
|
||||
# 创建平面
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(origin[0], origin[1], origin[2]),
|
||||
gp_Dir(normal[0], normal[1], normal[2])
|
||||
)
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
# 分型线可以使用 AI 结果或重新计算
|
||||
if "parting_line" in ai_result:
|
||||
parting_line = ai_result["parting_line"]
|
||||
else:
|
||||
parting_line = self._simple_parting_line(parting_surface, analysis)
|
||||
|
||||
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
|
||||
return parting_surface, parting_line
|
||||
|
||||
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
|
||||
"""
|
||||
添加拔模角
|
||||
|
||||
使用 OpenCASCADE 的拔模功能
|
||||
"""
|
||||
# 1. 尝试使用 AI 模型
|
||||
if self.ai_draft_analyzer is not None:
|
||||
try:
|
||||
logger.info("使用 AI 模型分析拔模角")
|
||||
ai_result = self.ai_draft_analyzer.analyze(shape, parting_surface, self.draft_angle)
|
||||
if ai_result and "drafted_shape" in ai_result:
|
||||
logger.info("AI 拔模分析成功")
|
||||
return ai_result["drafted_shape"]
|
||||
except Exception as e:
|
||||
logger.warning(f"AI 拔模分析失败,回退到几何方法:{e}")
|
||||
|
||||
# 2. 几何方法(简化实现)
|
||||
try:
|
||||
# 获取分型面的法向量作为拔模方向
|
||||
surface_adaptor = BRepAdaptor_Surface(parting_surface)
|
||||
draft_direction = surface_adaptor.Plane().Position().Direction()
|
||||
|
||||
# 使用 BRepOffsetAPI_ThickSolid 创建拔模
|
||||
# 注意:完整的拔模需要更复杂的实现,这里简化处理
|
||||
logger.info(f"使用几何方法添加拔模角:{self.draft_angle}度,方向=({draft_direction.X():.3f}, {draft_direction.Y():.3f}, {draft_direction.Z():.3f})")
|
||||
|
||||
# 简化:直接返回原始形状(拔模已在 CAD 中处理)
|
||||
# 完整实现需要使用 BRepOffsetAPI_DraftAngle
|
||||
return shape
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模角处理失败:{e}")
|
||||
return shape
|
||||
|
||||
def _calculate_parting_line_length(self, parting_line: List) -> float:
|
||||
"""计算分型线长度"""
|
||||
if not parting_line or len(parting_line) < 2:
|
||||
return 0.0
|
||||
|
||||
# 计算折线总长度
|
||||
total_length = 0.0
|
||||
for i in range(1, len(parting_line)):
|
||||
p1 = np.array(parting_line[i-1])
|
||||
p2 = np.array(parting_line[i])
|
||||
segment_length = np.linalg.norm(p2 - p1)
|
||||
total_length += segment_length
|
||||
|
||||
return total_length
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -0,0 +1,533 @@
|
||||
"""
|
||||
侧壁/倒扣面滑块机构检测与设计模块
|
||||
|
||||
功能:
|
||||
1. 倒扣区域检测 - 识别无法直接脱模的侧壁凹槽
|
||||
2. 滑块机构设计 - 侧向分型抽芯机构
|
||||
3. 斜顶机构设计 - 内侧倒扣的斜顶脱模机构
|
||||
4. 机构运动学分析 - 抽芯行程、脱模角度计算
|
||||
|
||||
倒扣检测原理:
|
||||
- 分型方向确定后,检查每个面的法向量
|
||||
- 如果面的法向量与脱模方向的点积为负(面朝向脱模反方向)
|
||||
且该面不在分型面上,则判定为倒扣面
|
||||
- 根据倒扣面的位置(外侧/内侧)选择滑块或斜顶
|
||||
|
||||
滑块 vs 斜顶:
|
||||
- 滑块:外侧倒扣,沿导滑槽侧向运动
|
||||
- 斜顶:内侧倒扣,沿斜导柱内侧运动
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import math
|
||||
import numpy as np
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UndercutDetector:
|
||||
"""倒扣区域检测器"""
|
||||
|
||||
def detect_undercuts(self, shape: Any, parting_direction: List[float],
|
||||
parting_surface: Any = None) -> Dict[str, Any]:
|
||||
"""
|
||||
检测产品中的倒扣区域
|
||||
|
||||
Args:
|
||||
shape: OCC 产品形状
|
||||
parting_direction: 分型方向 [nx, ny, nz]
|
||||
parting_surface: 分型面(可选)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"undercut_faces": List[Dict],
|
||||
"slider_regions": List[Dict],
|
||||
"lifter_regions": List[Dict],
|
||||
"total_undercut_area": float,
|
||||
"requires_slider": bool,
|
||||
"requires_lifter": bool,
|
||||
"complexity": str
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib_Add
|
||||
from OCC.Core.gp import gp_Dir
|
||||
|
||||
dir_vec = np.array(parting_direction, dtype=np.float64)
|
||||
dir_norm = np.linalg.norm(dir_vec)
|
||||
if dir_norm < 1e-6:
|
||||
dir_vec = np.array([0, 0, 1])
|
||||
else:
|
||||
dir_vec /= dir_norm
|
||||
|
||||
parting_dir = gp_Dir(dir_vec[0], dir_vec[1], dir_vec[2])
|
||||
|
||||
undercut_faces = []
|
||||
slider_regions = []
|
||||
lifter_regions = []
|
||||
total_undercut_area = 0.0
|
||||
|
||||
parting_z = 0.0
|
||||
if parting_surface is not None:
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() == 0:
|
||||
parting_z = surface.Plane().Location().Z()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
face_idx = 0
|
||||
|
||||
while explorer.More():
|
||||
face = TopoDS_Face(explorer.Current())
|
||||
face_idx += 1
|
||||
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
face_normal = None
|
||||
if surface.GetType() == 0:
|
||||
face_normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
props = BRepLProp_SLProps(surface, 1, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
face_normal = props.Normal()
|
||||
|
||||
if face_normal is None:
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
dot = face_normal.Dot(parting_dir)
|
||||
|
||||
face_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, face_props)
|
||||
area = face_props.Mass()
|
||||
center = face_props.CentreOfMass()
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(face, bbox)
|
||||
try:
|
||||
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get()
|
||||
except Exception:
|
||||
fxmin, fymin, fzmin, fxmax, fymax, fzmax = 0, 0, 0, 0, 0, 0
|
||||
|
||||
if dot < -0.1:
|
||||
face_center_z = center.Z()
|
||||
is_outer = face_center_z >= parting_z
|
||||
|
||||
undercut_info = {
|
||||
"face_index": face_idx,
|
||||
"normal": [face_normal.X(), face_normal.Y(), face_normal.Z()],
|
||||
"dot_product": float(dot),
|
||||
"area": float(area),
|
||||
"center": [float(center.X()), float(center.Y()), float(center.Z())],
|
||||
"bbox": {
|
||||
"min": [float(fxmin), float(fymin), float(fzmin)],
|
||||
"max": [float(fxmax), float(fymax), float(fzmax)]
|
||||
},
|
||||
"severity": "high" if dot < -0.5 else "medium",
|
||||
"is_outer": is_outer,
|
||||
}
|
||||
undercut_faces.append(undercut_info)
|
||||
total_undercut_area += area
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
for uf in undercut_faces:
|
||||
normal = np.array(uf["normal"])
|
||||
lateral_component = normal - np.dot(normal, dir_vec) * dir_vec
|
||||
lateral_norm = np.linalg.norm(lateral_component)
|
||||
|
||||
if lateral_norm > 0.01:
|
||||
slide_direction = lateral_component / lateral_norm
|
||||
else:
|
||||
slide_direction = np.array([1, 0, 0])
|
||||
|
||||
mechanism = {
|
||||
"face_indices": [uf["face_index"]],
|
||||
"slide_direction": slide_direction.tolist(),
|
||||
"area": uf["area"],
|
||||
"center": uf["center"],
|
||||
"severity": uf["severity"],
|
||||
}
|
||||
|
||||
if uf["is_outer"]:
|
||||
slider_regions.append(mechanism)
|
||||
else:
|
||||
lifter_regions.append(mechanism)
|
||||
|
||||
requires_slider = len(slider_regions) > 0
|
||||
requires_lifter = len(lifter_regions) > 0
|
||||
|
||||
total_count = len(slider_regions) + len(lifter_regions)
|
||||
if total_count == 0:
|
||||
complexity = "simple"
|
||||
elif total_count <= 2:
|
||||
complexity = "moderate"
|
||||
elif total_count <= 4:
|
||||
complexity = "complex"
|
||||
else:
|
||||
complexity = "very_complex"
|
||||
|
||||
result = {
|
||||
"undercut_faces": undercut_faces,
|
||||
"slider_regions": slider_regions,
|
||||
"lifter_regions": lifter_regions,
|
||||
"total_undercut_area": total_undercut_area,
|
||||
"requires_slider": requires_slider,
|
||||
"requires_lifter": requires_lifter,
|
||||
"complexity": complexity,
|
||||
"parting_direction": parting_direction,
|
||||
}
|
||||
|
||||
logger.info(f"倒扣检测完成: {len(undercut_faces)} 个倒扣面, "
|
||||
f"{len(slider_regions)} 个滑块, {len(lifter_regions)} 个斜顶, "
|
||||
f"复杂度={complexity}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"倒扣检测失败: {e}")
|
||||
return {
|
||||
"undercut_faces": [],
|
||||
"slider_regions": [],
|
||||
"lifter_regions": [],
|
||||
"total_undercut_area": 0,
|
||||
"requires_slider": False,
|
||||
"requires_lifter": False,
|
||||
"complexity": "unknown",
|
||||
"parting_direction": parting_direction,
|
||||
}
|
||||
|
||||
|
||||
class SliderMechanismDesigner:
|
||||
"""滑块机构设计器"""
|
||||
|
||||
def design_slider(self, slider_region: Dict, mold_size: Dict,
|
||||
parting_direction: List[float]) -> Dict[str, Any]:
|
||||
"""
|
||||
设计滑块机构
|
||||
|
||||
Args:
|
||||
slider_region: 倒扣区域信息
|
||||
mold_size: 模具尺寸
|
||||
parting_direction: 分型方向
|
||||
|
||||
Returns:
|
||||
滑块机构设计方案
|
||||
"""
|
||||
center = slider_region["center"]
|
||||
area = slider_region["area"]
|
||||
slide_dir = slider_region["slide_direction"]
|
||||
|
||||
slide_stroke = self._calculate_slide_stroke(slider_region, mold_size)
|
||||
|
||||
slide_angle = self._calculate_slide_angle(slide_dir, parting_direction)
|
||||
|
||||
slide_block_size = self._calculate_slide_block_size(area, slide_stroke)
|
||||
|
||||
guide_type = self._select_guide_type(slide_stroke, slide_angle)
|
||||
|
||||
return {
|
||||
"type": "slider",
|
||||
"location": center,
|
||||
"slide_direction": slide_dir,
|
||||
"slide_stroke": slide_stroke,
|
||||
"slide_angle": slide_angle,
|
||||
"block_size": slide_block_size,
|
||||
"guide_type": guide_type,
|
||||
"locking_mechanism": self._select_locking(slide_angle),
|
||||
"actuation": "hydraulic" if slide_stroke > 50 else "mechanical",
|
||||
"components": self._generate_components(slide_block_size, guide_type),
|
||||
"manufacturing_notes": self._generate_slider_notes(slide_angle, slide_stroke),
|
||||
}
|
||||
|
||||
def _calculate_slide_stroke(self, region: Dict, mold_size: Dict) -> float:
|
||||
"""计算抽芯行程"""
|
||||
bbox = region.get("bbox", {})
|
||||
if "max" in bbox and "min" in bbox:
|
||||
max_dim = max(
|
||||
abs(bbox["max"][0] - bbox["min"][0]),
|
||||
abs(bbox["max"][1] - bbox["min"][1]),
|
||||
abs(bbox["max"][2] - bbox["min"][2])
|
||||
)
|
||||
else:
|
||||
max_dim = 10.0
|
||||
|
||||
stroke = max_dim + 5.0
|
||||
return round(max(stroke, 10.0), 1)
|
||||
|
||||
def _calculate_slide_angle(self, slide_dir: List[float],
|
||||
parting_dir: List[float]) -> float:
|
||||
"""计算滑块倾斜角度"""
|
||||
s = np.array(slide_dir)
|
||||
p = np.array(parting_dir)
|
||||
|
||||
s_norm = np.linalg.norm(s)
|
||||
p_norm = np.linalg.norm(p)
|
||||
|
||||
if s_norm < 1e-6 or p_norm < 1e-6:
|
||||
return 90.0
|
||||
|
||||
cos_angle = np.clip(np.dot(s, p) / (s_norm * p_norm), -1, 1)
|
||||
angle = math.degrees(math.acos(abs(cos_angle)))
|
||||
return round(angle, 1)
|
||||
|
||||
def _calculate_slide_block_size(self, area: float, stroke: float) -> Dict[str, float]:
|
||||
"""计算滑块尺寸"""
|
||||
width = max(math.sqrt(area) * 1.5, 15.0)
|
||||
height = max(math.sqrt(area) * 1.2, 12.0)
|
||||
length = stroke + width * 0.5
|
||||
|
||||
return {
|
||||
"width": round(width, 1),
|
||||
"height": round(height, 1),
|
||||
"length": round(length, 1),
|
||||
}
|
||||
|
||||
def _select_guide_type(self, stroke: float, angle: float) -> str:
|
||||
"""选择导滑方式"""
|
||||
if stroke > 80:
|
||||
return "T_slot_guide"
|
||||
elif angle > 20:
|
||||
return "angled_guide_pin"
|
||||
else:
|
||||
return "dovetail_guide"
|
||||
|
||||
def _select_locking(self, angle: float) -> str:
|
||||
"""选择锁紧方式"""
|
||||
if angle > 25:
|
||||
return "wedge_block"
|
||||
else:
|
||||
return "lock_block"
|
||||
|
||||
def _generate_components(self, block_size: Dict, guide_type: str) -> List[Dict]:
|
||||
"""生成滑块组件清单"""
|
||||
components = [
|
||||
{"name": "slide_block", "material": "P20", "hardness": "HRC 28-32"},
|
||||
{"name": "guide_strip", "material": "bronze", "hardness": "HB 80-100"},
|
||||
{"name": "wear_plate", "material": "T8", "hardness": "HRC 45-50"},
|
||||
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
|
||||
]
|
||||
|
||||
if guide_type == "T_slot_guide":
|
||||
components.append({"name": "T_slot_insert", "material": "P20", "hardness": "HRC 28-32"})
|
||||
elif guide_type == "angled_guide_pin":
|
||||
components.append({"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"})
|
||||
elif guide_type == "dovetail_guide":
|
||||
components.append({"name": "dovetail_block", "material": "P20", "hardness": "HRC 28-32"})
|
||||
|
||||
return components
|
||||
|
||||
def _generate_slider_notes(self, angle: float, stroke: float) -> List[str]:
|
||||
"""生成滑块加工注意事项"""
|
||||
notes = []
|
||||
if angle > 25:
|
||||
notes.append("滑块角度较大,需确保锁紧可靠")
|
||||
if stroke > 50:
|
||||
notes.append("抽芯行程较长,建议使用液压抽芯")
|
||||
if stroke > 80:
|
||||
notes.append("大行程抽芯,需校核导滑槽强度")
|
||||
notes.append("滑块需设置限位装置,防止脱出")
|
||||
notes.append("配合面需做耐磨处理")
|
||||
return notes
|
||||
|
||||
|
||||
class LifterMechanismDesigner:
|
||||
"""斜顶机构设计器"""
|
||||
|
||||
def design_lifter(self, lifter_region: Dict, mold_size: Dict,
|
||||
parting_direction: List[float]) -> Dict[str, Any]:
|
||||
"""
|
||||
设计斜顶机构
|
||||
|
||||
Args:
|
||||
lifter_region: 内侧倒扣区域信息
|
||||
mold_size: 模具尺寸
|
||||
parting_direction: 分型方向
|
||||
|
||||
Returns:
|
||||
斜顶机构设计方案
|
||||
"""
|
||||
center = lifter_region["center"]
|
||||
area = lifter_region["area"]
|
||||
|
||||
lifter_angle = self._calculate_lifter_angle(lifter_region)
|
||||
|
||||
lifter_stroke = self._calculate_lifter_stroke(lifter_region)
|
||||
|
||||
lifter_size = self._calculate_lifter_size(area, lifter_stroke, lifter_angle)
|
||||
|
||||
return {
|
||||
"type": "lifter",
|
||||
"location": center,
|
||||
"lifter_angle": lifter_angle,
|
||||
"lifter_stroke": lifter_stroke,
|
||||
"block_size": lifter_size,
|
||||
"guide_type": "angled_hole",
|
||||
"return_mechanism": "spring_return",
|
||||
"components": self._generate_lifter_components(lifter_size),
|
||||
"manufacturing_notes": self._generate_lifter_notes(lifter_angle),
|
||||
}
|
||||
|
||||
def _calculate_lifter_angle(self, region: Dict) -> float:
|
||||
"""计算斜顶角度(通常5-15度)"""
|
||||
return 8.0
|
||||
|
||||
def _calculate_lifter_stroke(self, region: Dict) -> float:
|
||||
"""计算斜顶行程"""
|
||||
bbox = region.get("bbox", {})
|
||||
if "max" in bbox and "min" in bbox:
|
||||
max_dim = max(
|
||||
abs(bbox["max"][i] - bbox["min"][i]) for i in range(3)
|
||||
)
|
||||
else:
|
||||
max_dim = 5.0
|
||||
|
||||
return round(max(max_dim + 3.0, 8.0), 1)
|
||||
|
||||
def _calculate_lifter_size(self, area: float, stroke: float,
|
||||
angle: float) -> Dict[str, float]:
|
||||
"""计算斜顶尺寸"""
|
||||
width = max(math.sqrt(area) * 1.2, 10.0)
|
||||
height = stroke / math.sin(math.radians(angle)) if angle > 0 else stroke * 3
|
||||
thickness = max(width * 0.6, 8.0)
|
||||
|
||||
return {
|
||||
"width": round(width, 1),
|
||||
"height": round(height, 1),
|
||||
"thickness": round(thickness, 1),
|
||||
}
|
||||
|
||||
def _generate_lifter_components(self, size: Dict) -> List[Dict]:
|
||||
"""生成斜顶组件清单"""
|
||||
return [
|
||||
{"name": "lifter_body", "material": "P20", "hardness": "HRC 28-32"},
|
||||
{"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"},
|
||||
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
|
||||
{"name": "wear_bushing", "material": "bronze", "hardness": "HB 80-100"},
|
||||
]
|
||||
|
||||
def _generate_lifter_notes(self, angle: float) -> List[str]:
|
||||
"""生成斜顶加工注意事项"""
|
||||
notes = []
|
||||
if angle > 12:
|
||||
notes.append("斜顶角度偏大,需校核脱模力")
|
||||
notes.append("斜顶导滑孔需精确加工")
|
||||
notes.append("斜顶头部需做耐磨处理")
|
||||
notes.append("需设置限位防止斜顶脱出")
|
||||
return notes
|
||||
|
||||
|
||||
class SideActionDesigner:
|
||||
"""侧向分型机构综合设计器"""
|
||||
|
||||
def __init__(self):
|
||||
self.undercut_detector = UndercutDetector()
|
||||
self.slider_designer = SliderMechanismDesigner()
|
||||
self.lifter_designer = LifterMechanismDesigner()
|
||||
|
||||
def analyze_and_design(self, shape: Any, parting_direction: List[float],
|
||||
mold_size: Dict, parting_surface: Any = None) -> Dict[str, Any]:
|
||||
"""
|
||||
综合分析倒扣并设计侧向分型机构
|
||||
|
||||
Returns:
|
||||
{
|
||||
"undercut_analysis": Dict,
|
||||
"slider_mechanisms": List[Dict],
|
||||
"lifter_mechanisms": List[Dict],
|
||||
"summary": Dict,
|
||||
"recommendations": List[str]
|
||||
}
|
||||
"""
|
||||
logger.info("开始侧向分型机构分析...")
|
||||
|
||||
undercut_result = self.undercut_detector.detect_undercuts(
|
||||
shape, parting_direction, parting_surface
|
||||
)
|
||||
|
||||
slider_mechanisms = []
|
||||
for region in undercut_result["slider_regions"]:
|
||||
slider = self.slider_designer.design_slider(
|
||||
region, mold_size, parting_direction
|
||||
)
|
||||
slider_mechanisms.append(slider)
|
||||
|
||||
lifter_mechanisms = []
|
||||
for region in undercut_result["lifter_regions"]:
|
||||
lifter = self.lifter_designer.design_lifter(
|
||||
region, mold_size, parting_direction
|
||||
)
|
||||
lifter_mechanisms.append(lifter)
|
||||
|
||||
total_mechanisms = len(slider_mechanisms) + len(lifter_mechanisms)
|
||||
|
||||
summary = {
|
||||
"total_undercut_faces": len(undercut_result["undercut_faces"]),
|
||||
"total_slider_count": len(slider_mechanisms),
|
||||
"total_lifter_count": len(lifter_mechanisms),
|
||||
"total_mechanism_count": total_mechanisms,
|
||||
"complexity": undercut_result["complexity"],
|
||||
"has_hydraulic": any(s.get("actuation") == "hydraulic" for s in slider_mechanisms),
|
||||
}
|
||||
|
||||
recommendations = self._generate_overall_recommendations(summary, undercut_result)
|
||||
|
||||
result = {
|
||||
"undercut_analysis": undercut_result,
|
||||
"slider_mechanisms": slider_mechanisms,
|
||||
"lifter_mechanisms": lifter_mechanisms,
|
||||
"summary": summary,
|
||||
"recommendations": recommendations,
|
||||
}
|
||||
|
||||
logger.info(f"侧向分型机构设计完成: {len(slider_mechanisms)} 个滑块, "
|
||||
f"{len(lifter_mechanisms)} 个斜顶")
|
||||
|
||||
return result
|
||||
|
||||
def _generate_overall_recommendations(self, summary: Dict,
|
||||
undercut: Dict) -> List[str]:
|
||||
"""生成总体建议"""
|
||||
recs = []
|
||||
|
||||
if summary["total_mechanism_count"] == 0:
|
||||
recs.append("无倒扣区域,模具结构简单,无需侧向分型机构")
|
||||
return recs
|
||||
|
||||
if summary["total_slider_count"] > 0:
|
||||
recs.append(f"需要 {summary['total_slider_count']} 个滑块机构处理外侧倒扣")
|
||||
|
||||
if summary["total_lifter_count"] > 0:
|
||||
recs.append(f"需要 {summary['total_lifter_count']} 个斜顶机构处理内侧倒扣")
|
||||
|
||||
if summary["has_hydraulic"]:
|
||||
recs.append("大行程抽芯需使用液压系统,需配置液压站")
|
||||
|
||||
if summary["complexity"] == "very_complex":
|
||||
recs.append("侧向分型机构复杂,建议评估是否可通过产品修改简化")
|
||||
recs.append("考虑使用二次分型或旋转脱模替代方案")
|
||||
|
||||
if summary["total_mechanism_count"] > 3:
|
||||
recs.append("侧向机构较多,建议优化模具结构减少机构数量")
|
||||
|
||||
recs.append("所有侧向机构需做运动仿真验证干涉")
|
||||
|
||||
return recs
|
||||
+32
-27
@@ -1,5 +1,5 @@
|
||||
# models/database.py
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint, Numeric
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
@@ -10,6 +10,7 @@ Base = declarative_base()
|
||||
class User(Base):
|
||||
"""用户表"""
|
||||
__tablename__ = "users"
|
||||
__excluded_fields__ = {'hashed_password'}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
@@ -40,6 +41,10 @@ class User(Base):
|
||||
return True
|
||||
return False
|
||||
|
||||
def safe_dict(self):
|
||||
return {k: v for k, v in self.__dict__.items()
|
||||
if not k.startswith('_') and k not in self.__excluded_fields__}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(id={self.id}, username='{self.username}')>"
|
||||
|
||||
@@ -357,7 +362,7 @@ class FeatureDetection(Base):
|
||||
stp_file_id = Column(Integer, ForeignKey("stp_files.id"), nullable=False, index=True)
|
||||
|
||||
# 特征信息
|
||||
feature_type = Column(String(50), nullable=False, index=True) # thin_wall, thick_wall, rib, boss, draft_angle
|
||||
feature_type = Column(String(50), nullable=False, index=True) # thin_wall, thick_wall, wall_non_uniform, rib, boss, draft_angle, high_curvature, fillet
|
||||
confidence = Column(Float, nullable=False) # 0.0 - 1.0
|
||||
|
||||
# 位置和尺寸
|
||||
@@ -481,8 +486,8 @@ class Product(Base):
|
||||
category = Column(String(100), nullable=True)
|
||||
unit = Column(String(20), default="件")
|
||||
item_type = Column(String(20), default="finished", index=True)
|
||||
cost_price = Column(Float, default=0)
|
||||
sale_price = Column(Float, default=0)
|
||||
cost_price = Column(Numeric(12, 2), default=0)
|
||||
sale_price = Column(Numeric(12, 2), default=0)
|
||||
min_stock = Column(Integer, default=0)
|
||||
max_stock = Column(Integer, default=1000)
|
||||
is_active = Column(Boolean, default=True)
|
||||
@@ -516,8 +521,8 @@ class ProductMaterial(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
finished_product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
|
||||
material_product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
|
||||
quantity = Column(Float, nullable=False)
|
||||
loss_rate = Column(Float, default=0)
|
||||
quantity = Column(Numeric(12, 4), nullable=False)
|
||||
loss_rate = Column(Numeric(5, 4), default=0)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
@@ -542,7 +547,7 @@ class MaterialPriceHistory(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
|
||||
price = Column(Float, nullable=False)
|
||||
price = Column(Numeric(12, 2), nullable=False)
|
||||
effective_date = Column(DateTime, default=func.now(), index=True)
|
||||
supplier_id = Column(Integer, ForeignKey("suppliers.id"), nullable=True, index=True)
|
||||
remark = Column(Text, nullable=True)
|
||||
@@ -615,7 +620,7 @@ class Customer(Base):
|
||||
bank_name = Column(String(100), nullable=True)
|
||||
bank_account = Column(String(50), nullable=True)
|
||||
tax_number = Column(String(50), nullable=True)
|
||||
credit_limit = Column(Float, default=0)
|
||||
credit_limit = Column(Numeric(12, 2), default=0)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
@@ -653,8 +658,8 @@ class Inventory(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
|
||||
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True)
|
||||
quantity = Column(Float, default=0)
|
||||
locked_quantity = Column(Float, default=0)
|
||||
quantity = Column(Numeric(12, 4), default=0)
|
||||
locked_quantity = Column(Numeric(12, 4), default=0)
|
||||
batch_number = Column(String(50), nullable=True)
|
||||
location = Column(String(100), nullable=True)
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
@@ -678,14 +683,14 @@ class StockMovement(Base):
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
|
||||
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False)
|
||||
movement_type = Column(String(20), nullable=False)
|
||||
quantity = Column(Float, nullable=False)
|
||||
before_quantity = Column(Float, default=0)
|
||||
after_quantity = Column(Float, default=0)
|
||||
quantity = Column(Numeric(12, 4), nullable=False)
|
||||
before_quantity = Column(Numeric(12, 4), default=0)
|
||||
after_quantity = Column(Numeric(12, 4), default=0)
|
||||
reference_type = Column(String(50), nullable=True)
|
||||
reference_id = Column(Integer, nullable=True)
|
||||
reference_no = Column(String(50), nullable=True)
|
||||
unit_price = Column(Float, nullable=True)
|
||||
total_amount = Column(Float, nullable=True)
|
||||
unit_price = Column(Numeric(12, 2), nullable=True)
|
||||
total_amount = Column(Numeric(12, 2), nullable=True)
|
||||
remark = Column(Text, nullable=True)
|
||||
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime, default=func.now(), index=True)
|
||||
@@ -706,8 +711,8 @@ class PurchaseOrder(Base):
|
||||
order_date = Column(DateTime, default=func.now())
|
||||
expected_date = Column(Date, nullable=True)
|
||||
status = Column(String(20), default="draft")
|
||||
total_amount = Column(Float, default=0)
|
||||
paid_amount = Column(Float, default=0)
|
||||
total_amount = Column(Numeric(12, 2), default=0)
|
||||
paid_amount = Column(Numeric(12, 2), default=0)
|
||||
remark = Column(Text, nullable=True)
|
||||
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
@@ -732,8 +737,8 @@ class PurchaseOrderItem(Base):
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
quantity = Column(Integer, nullable=False)
|
||||
received_quantity = Column(Integer, default=0)
|
||||
unit_price = Column(Float, nullable=False)
|
||||
amount = Column(Float, nullable=False)
|
||||
unit_price = Column(Numeric(12, 2), nullable=False)
|
||||
amount = Column(Numeric(12, 2), nullable=False)
|
||||
remark = Column(Text, nullable=True)
|
||||
|
||||
order = relationship("PurchaseOrder", back_populates="items")
|
||||
@@ -757,10 +762,10 @@ class SalesOrder(Base):
|
||||
status = Column(String(20), default="draft")
|
||||
production_status = Column(String(20), default="not_started", index=True)
|
||||
production_no = Column(String(50), nullable=True, index=True)
|
||||
planned_material_cost = Column(Float, default=0)
|
||||
actual_material_cost = Column(Float, default=0)
|
||||
total_amount = Column(Float, default=0)
|
||||
received_amount = Column(Float, default=0)
|
||||
planned_material_cost = Column(Numeric(12, 2), default=0)
|
||||
actual_material_cost = Column(Numeric(12, 2), default=0)
|
||||
total_amount = Column(Numeric(12, 2), default=0)
|
||||
received_amount = Column(Numeric(12, 2), default=0)
|
||||
remark = Column(Text, nullable=True)
|
||||
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
@@ -781,7 +786,7 @@ class FinanceTransaction(Base):
|
||||
txn_type = Column(String(20), nullable=False, index=True)
|
||||
partner_type = Column(String(20), nullable=False, index=True)
|
||||
partner_id = Column(Integer, nullable=False, index=True)
|
||||
amount = Column(Float, nullable=False)
|
||||
amount = Column(Numeric(12, 2), nullable=False)
|
||||
txn_date = Column(DateTime, default=func.now(), index=True)
|
||||
method = Column(String(30), default="bank")
|
||||
account_name = Column(String(100), nullable=True)
|
||||
@@ -803,7 +808,7 @@ class FinanceAllocation(Base):
|
||||
transaction_id = Column(Integer, ForeignKey("finance_transactions.id"), nullable=False, index=True)
|
||||
order_type = Column(String(20), nullable=False, index=True)
|
||||
order_id = Column(Integer, nullable=False, index=True)
|
||||
allocated_amount = Column(Float, nullable=False)
|
||||
allocated_amount = Column(Numeric(12, 2), nullable=False)
|
||||
created_at = Column(DateTime, default=func.now(), index=True)
|
||||
|
||||
transaction = relationship("FinanceTransaction", back_populates="allocations")
|
||||
@@ -852,8 +857,8 @@ class SalesOrderItem(Base):
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
quantity = Column(Integer, nullable=False)
|
||||
delivered_quantity = Column(Integer, default=0)
|
||||
unit_price = Column(Float, nullable=False)
|
||||
amount = Column(Float, nullable=False)
|
||||
unit_price = Column(Numeric(12, 2), nullable=False)
|
||||
amount = Column(Numeric(12, 2), nullable=False)
|
||||
remark = Column(Text, nullable=True)
|
||||
|
||||
order = relationship("SalesOrder", back_populates="items")
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/air-datepicker@3.5.3/air-datepicker.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260317-01">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260419-01">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
+28
-1
@@ -2466,8 +2466,35 @@ select.form-input {
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.export-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-left: auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.export-buttons .btn-sm {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-secondary, #6b7280);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md, 6px);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-tertiary, #4b5563);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
|
||||
+47
-1
@@ -1261,7 +1261,39 @@ const ResultView = {
|
||||
return material === 'aluminum_foam';
|
||||
};
|
||||
|
||||
return { state, formatFileSize, formatDateTime, formatNumber, getPriorityText, isFoamMaterial };
|
||||
const exportCAD = async (format) => {
|
||||
try {
|
||||
const taskId = route.params.taskId;
|
||||
const result = await apiRequest('/api/export-mold', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
task_id: taskId,
|
||||
formats: [format],
|
||||
components: ['cavity', 'core', 'parting_surface']
|
||||
})
|
||||
});
|
||||
|
||||
if (result.status === 'success' && result.data.files) {
|
||||
for (const file of result.data.files) {
|
||||
const downloadUrl = `/api/export-download/${file.filepath.replace(/\\/g, '/').split('/').slice(-2).join('/')}`;
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = file.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
addNotification(`已导出 ${result.data.files.length} 个 ${format.toUpperCase()} 文件`, 'success');
|
||||
} else if (result.data.errors && result.data.errors.length > 0) {
|
||||
addNotification(`导出失败: ${result.data.errors[0]}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
addNotification(`导出失败: ${e.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
return { state, formatFileSize, formatDateTime, formatNumber, getPriorityText, isFoamMaterial, exportCAD };
|
||||
},
|
||||
template: `
|
||||
<div class="page-container">
|
||||
@@ -1284,6 +1316,20 @@ const ResultView = {
|
||||
<span :class="['badge', state.task.status === 'completed' ? 'badge-success' : 'badge-error']">
|
||||
{{ state.task.status }}
|
||||
</span>
|
||||
<div v-if="state.task.status === 'completed'" class="export-buttons">
|
||||
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
|
||||
导出 STEP
|
||||
</button>
|
||||
<button class="btn-sm btn-secondary" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
|
||||
导出 STL
|
||||
</button>
|
||||
<button class="btn-sm btn-secondary" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
|
||||
导出 IGES
|
||||
</button>
|
||||
<button class="btn-sm btn-secondary" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
|
||||
导出 BRep
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-grid">
|
||||
|
||||
Reference in New Issue
Block a user