Compare commits

..

5 Commits

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