feat(O): UI-Vollständigkeit + v3-Workflows + OCC-Kantenanalyse
Backend:
- Phase I: notification_configs router (GET/PUT/{event}/{channel}/POST reset)
war bereits in notifications.py — add-alias endpoint in uploads.py ergänzt
- OutputType schema: workflow_definition_id + workflow_name fields;
PATCH unterstützt Workflow-Zuweisung; _enrich_workflow_names() batch query
- Dispatch-Integration: orders.py dispatch_renders() → dispatch_render_with_workflow()
mit Legacy-Fallback; neues Logging
- uploads.py: POST /validations/{id}/add-alias für Material-Lücken
Pipeline:
- step_processor.py: extract_mesh_edge_data() via OCC — berechnet Dihedralwinkel
aller Kanten, liefert suggested_smooth_angle + sharp_edge_midpoints
Integriert in extract_cad_metadata() und process_cad_file()
- domains/rendering/tasks.py: apply_asset_library_materials_task (K3),
export_gltf_for_order_line_task → Blender export_gltf.py (K4),
export_blend_for_order_line_task → export_blend.py fix (K5)
- render-worker/scripts/still_render.py: _mark_sharp_and_seams() mit
OCC midpoint KD-tree matching + UV-Seam-Markierung
- render-worker/scripts/blender_render.py: identische Funktion + mesh_attributes parsing
Frontend:
- Layout.tsx: Upload-Link in Sidebar (alle User); Asset Libraries Link (admin/PM)
- App.tsx: /asset-libraries Route
- AssetLibrary.tsx: neue Seite (Upload, Catalog-Anzeige, Refresh, Toggle, Delete)
- OutputTypeTable.tsx: Workflow-Dropdown + Legacy/Workflow Badge
- ProductDetail.tsx: Geometry-Karte (Volumen, Surface, BBox, Sharp-Winkel)
- api/outputTypes.ts + api/products.ts: neue Felder
- api/imports.ts: ImportValidation API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,10 @@ def extract_cad_metadata(cad_file_id: str) -> None:
|
||||
objects = _extract_step_objects(step_path)
|
||||
cad_file.parsed_objects = {"objects": objects}
|
||||
|
||||
edge_data = extract_mesh_edge_data(str(step_path))
|
||||
if edge_data:
|
||||
cad_file.mesh_attributes = {**(cad_file.mesh_attributes or {}), **edge_data}
|
||||
|
||||
gltf_path = _convert_to_gltf(step_path, cad_file_id, settings.upload_dir)
|
||||
if gltf_path:
|
||||
cad_file.gltf_path = str(gltf_path)
|
||||
@@ -173,6 +177,11 @@ def process_cad_file(cad_file_id: str) -> None:
|
||||
objects = _extract_step_objects(step_path)
|
||||
cad_file.parsed_objects = {"objects": objects}
|
||||
|
||||
# Step 1b: Extract sharp-edge topology data and merge into mesh_attributes
|
||||
edge_data = extract_mesh_edge_data(str(step_path))
|
||||
if edge_data:
|
||||
cad_file.mesh_attributes = {**(cad_file.mesh_attributes or {}), **edge_data}
|
||||
|
||||
# Step 2: Generate thumbnail — pass empty part_colors so the Three.js
|
||||
# renderer extracts named parts and auto-assigns palette colours.
|
||||
# Other renderers (Blender, Pillow) ignore the part_colors argument.
|
||||
@@ -197,6 +206,120 @@ def process_cad_file(cad_file_id: str) -> None:
|
||||
session.commit()
|
||||
|
||||
|
||||
def extract_mesh_edge_data(step_path: str) -> dict:
|
||||
"""Extract sharp edge metrics and suggested smooth angle from STEP topology.
|
||||
|
||||
Returns dict with:
|
||||
- suggested_smooth_angle: float (degrees) — recommended auto-smooth angle
|
||||
- has_mechanical_edges: bool — True if part has distinct hard edges (bearings etc.)
|
||||
- sharp_edge_midpoints: list of [x, y, z] — midpoints of sharp edges in mm (max 500)
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_FACE
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.gp import gp_Pnt
|
||||
import math
|
||||
|
||||
reader = STEPControl_Reader()
|
||||
status = reader.ReadFile(step_path)
|
||||
if status != IFSelect_RetDone:
|
||||
return {}
|
||||
reader.TransferRoots()
|
||||
shape = reader.OneShape()
|
||||
|
||||
# Mesh the shape for geometry access
|
||||
BRepMesh_IncrementalMesh(shape, 0.5, False, 0.5)
|
||||
|
||||
# Collect face normals per edge (for dihedral angle computation)
|
||||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
||||
from OCC.Core.TopExp import topexp
|
||||
|
||||
edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape()
|
||||
topexp.MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_face_map)
|
||||
|
||||
dihedral_angles = []
|
||||
sharp_midpoints = []
|
||||
|
||||
for i in range(1, edge_face_map.Extent() + 1):
|
||||
edge = edge_face_map.FindKey(i)
|
||||
faces = edge_face_map.FindFromIndex(i)
|
||||
if faces.Size() < 2:
|
||||
continue
|
||||
|
||||
# Get the two adjacent faces
|
||||
face_list = list(faces)
|
||||
if len(face_list) < 2:
|
||||
continue
|
||||
|
||||
try:
|
||||
surf1 = BRepAdaptor_Surface(face_list[0])
|
||||
surf2 = BRepAdaptor_Surface(face_list[1])
|
||||
|
||||
# Get normals at midpoint of edge
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
mid_u = (curve.FirstParameter() + curve.LastParameter()) / 2
|
||||
mid_pt = curve.Value(mid_u)
|
||||
|
||||
# Sample face normals at UV center
|
||||
u1 = (surf1.FirstUParameter() + surf1.LastUParameter()) / 2
|
||||
v1 = (surf1.FirstVParameter() + surf1.LastVParameter()) / 2
|
||||
n1 = surf1.DN(u1, v1, 0, 1).Crossed(surf1.DN(u1, v1, 1, 0))
|
||||
|
||||
u2 = (surf2.FirstUParameter() + surf2.LastUParameter()) / 2
|
||||
v2 = (surf2.FirstVParameter() + surf2.LastVParameter()) / 2
|
||||
n2 = surf2.DN(u2, v2, 0, 1).Crossed(surf2.DN(u2, v2, 1, 0))
|
||||
|
||||
if n1.Magnitude() > 1e-10 and n2.Magnitude() > 1e-10:
|
||||
n1.Normalize()
|
||||
n2.Normalize()
|
||||
cos_angle = max(-1.0, min(1.0, n1.Dot(n2)))
|
||||
angle_deg = math.degrees(math.acos(abs(cos_angle)))
|
||||
dihedral_angles.append(angle_deg)
|
||||
|
||||
if angle_deg > 20 and len(sharp_midpoints) < 500:
|
||||
sharp_midpoints.append([
|
||||
round(mid_pt.X(), 3),
|
||||
round(mid_pt.Y(), 3),
|
||||
round(mid_pt.Z(), 3),
|
||||
])
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not dihedral_angles:
|
||||
return {}
|
||||
|
||||
import statistics
|
||||
median_angle = statistics.median(dihedral_angles)
|
||||
max_angle = max(dihedral_angles)
|
||||
|
||||
# Suggest smooth angle: slightly below the median of hard edges
|
||||
hard_edges = [a for a in dihedral_angles if a > 20]
|
||||
if hard_edges:
|
||||
suggested = max(15.0, min(60.0, statistics.median(hard_edges) * 0.8))
|
||||
else:
|
||||
suggested = 30.0
|
||||
|
||||
return {
|
||||
"suggested_smooth_angle": round(suggested, 1),
|
||||
"has_mechanical_edges": max_angle > 45,
|
||||
"sharp_edge_midpoints": sharp_midpoints[:500],
|
||||
}
|
||||
except ImportError:
|
||||
# OCC not available (e.g. in backend container)
|
||||
return {}
|
||||
except Exception as exc:
|
||||
logger.warning("extract_mesh_edge_data failed (non-fatal): %s", exc)
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_step_objects(step_path: Path) -> list[str]:
|
||||
"""Extract part names from STEP file using pythonocc."""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user