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:
2026-03-06 23:20:55 +01:00
parent f15b035b88
commit 382a18fd02
18 changed files with 1222 additions and 355 deletions
+82 -1
View File
@@ -98,6 +98,16 @@ denoising_prefilter_arg = argv[22] if len(argv) > 22 else ""
denoising_quality_arg = argv[23] if len(argv) > 23 else ""
denoising_use_gpu_arg = argv[24] if len(argv) > 24 else ""
# Named argument: --mesh-attributes <json>
_mesh_attrs: dict = {}
_sys_argv = sys.argv
if "--mesh-attributes" in _sys_argv:
_idx = _sys_argv.index("--mesh-attributes")
try:
_mesh_attrs = _json.loads(_sys_argv[_idx + 1])
except Exception:
pass
# Validate template path: if provided it MUST exist on disk.
# Fail loudly rather than silently rendering with factory settings.
if template_path and not os.path.isfile(template_path):
@@ -203,6 +213,69 @@ def _apply_rotation(parts, rx, ry, rz):
print(f"[blender_render] applied rotation ({rx}°, {ry}°, {rz}°) to {len(parts)} parts")
def _mark_sharp_and_seams(obj, smooth_angle_deg: float, sharp_edge_midpoints=None):
"""Mark sharp edges and UV seams based on angle threshold and optional midpoints."""
import math
import bpy
# Ensure we're working with the right object
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
# Set auto-smooth angle
if hasattr(obj.data, 'auto_smooth_angle'):
obj.data.auto_smooth_angle = math.radians(smooth_angle_deg)
# Enter edit mode to mark edges
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
# Select edges above threshold angle and mark sharp
bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(smooth_angle_deg))
bpy.ops.mesh.mark_sharp()
# Mark same edges as UV seams
bpy.ops.mesh.mark_seam(clear=False)
# If we have OCC-derived midpoints, try to mark additional edges
if sharp_edge_midpoints and len(sharp_edge_midpoints) > 0:
try:
import bmesh
bpy.ops.object.mode_set(mode='OBJECT')
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.edges.ensure_lookup_table()
bm.verts.ensure_lookup_table()
# Build KD-tree for edge midpoints
import mathutils
kd = mathutils.kdtree.KDTree(len(bm.edges))
for i, edge in enumerate(bm.edges):
midpt = (edge.verts[0].co + edge.verts[1].co) / 2
kd.insert(midpt, i)
kd.balance()
# For each OCC sharp midpoint, find nearest Blender edge
tol = 0.5 # 0.5 mm tolerance (coordinates in mm before scale)
for mp in sharp_edge_midpoints[:200]:
vec = mathutils.Vector(mp)
co, idx, dist = kd.find(vec)
if dist < tol:
bm.edges[idx].seam = True
try:
bm.edges[idx].smooth = False
except Exception:
pass
bm.to_mesh(obj.data)
bm.free()
except Exception:
pass # Non-fatal
# Return to object mode
bpy.ops.object.mode_set(mode='OBJECT')
def _import_stl(stl_file):
"""Import STL into Blender, using per-part STLs if available.
@@ -394,9 +467,13 @@ if use_template:
col.objects.unlink(part)
target_col.objects.link(part)
# Apply smooth shading
# Apply smooth shading and mark sharp edges / UV seams
for part in parts:
_apply_smooth(part, smooth_angle)
_mark_sharp_and_seams(
part, smooth_angle,
sharp_edge_midpoints=_mesh_attrs.get('sharp_edge_midpoints'),
)
# Material assignment: library materials if available, otherwise palette
if material_library_path and material_map:
@@ -469,6 +546,10 @@ else:
for i, part in enumerate(parts):
_apply_smooth(part, smooth_angle)
_mark_sharp_and_seams(
part, smooth_angle,
sharp_edge_midpoints=_mesh_attrs.get('sharp_edge_midpoints'),
)
_assign_palette_material(part, i)
# Apply material library on top of palette colours (same logic as Mode B).
+73 -1
View File
@@ -145,6 +145,70 @@ def _apply_mesh_attributes(objects: list, mesh_attributes: dict) -> None:
obj.data.auto_smooth_angle = threshold_rad
def _mark_sharp_and_seams(obj, smooth_angle_deg: float, sharp_edge_midpoints=None):
"""Mark sharp edges and UV seams based on angle threshold and optional midpoints."""
import math
import bpy
# Ensure we're working with the right object
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
# Set auto-smooth angle
if hasattr(obj.data, 'auto_smooth_angle'):
obj.data.auto_smooth_angle = math.radians(smooth_angle_deg)
# Enter edit mode to mark edges
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
# Select edges above threshold angle and mark sharp
bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(smooth_angle_deg))
bpy.ops.mesh.mark_sharp()
# Mark same edges as UV seams
bpy.ops.mesh.mark_seam(clear=False)
# If we have OCC-derived midpoints, try to mark additional edges
if sharp_edge_midpoints and len(sharp_edge_midpoints) > 0:
try:
import bmesh
bpy.ops.object.mode_set(mode='OBJECT')
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.edges.ensure_lookup_table()
bm.verts.ensure_lookup_table()
# Build KD-tree for edge midpoints
import mathutils
kd = mathutils.kdtree.KDTree(len(bm.edges))
for i, edge in enumerate(bm.edges):
midpt = (edge.verts[0].co + edge.verts[1].co) / 2
kd.insert(midpt, i)
kd.balance()
# For each OCC sharp midpoint, find nearest Blender edge
tol = 0.5 # 0.5 mm tolerance (coordinates in mm before scale)
for mp in sharp_edge_midpoints[:200]:
vec = mathutils.Vector(mp)
co, idx, dist = kd.find(vec)
if dist < tol:
bm.edges[idx].seam = True
# Mark sharp via custom attribute
try:
bm.edges[idx].smooth = False
except Exception:
pass
bm.to_mesh(obj.data)
bm.free()
except Exception:
pass # Non-fatal
# Return to object mode
bpy.ops.object.mode_set(mode='OBJECT')
def _import_stl(stl_file):
"""Import STL into Blender, using per-part STLs if available.
@@ -411,9 +475,13 @@ def main():
col.objects.unlink(part)
target_col.objects.link(part)
# Apply smooth shading
# Apply smooth shading and mark sharp edges / UV seams
for part in parts:
_apply_smooth(part, SMOOTH_ANGLE)
_mark_sharp_and_seams(
part, SMOOTH_ANGLE,
sharp_edge_midpoints=_mesh_attrs.get('sharp_edge_midpoints'),
)
# Material assignment: library materials if available, otherwise palette
if material_library_path and material_map:
@@ -504,6 +572,10 @@ def main():
for i, part in enumerate(parts):
_apply_smooth(part, SMOOTH_ANGLE)
_mark_sharp_and_seams(
part, SMOOTH_ANGLE,
sharp_edge_midpoints=_mesh_attrs.get('sharp_edge_midpoints'),
)
# Material assignment: library materials if available, else part_colors/palette
if material_library_path and material_map: