57 Commits

Author SHA1 Message Date
Hartmut 3e810c74a3 chore: snapshot workflow migration progress 2026-04-12 11:49:04 +02:00
Hartmut b795f0e6d6 refactor: rebrand project to HartOMat 2026-04-06 12:45:47 +02:00
Hartmut 02669c395c fix: cinematic camera accounts for output aspect ratio
For 16:9 (1280x720), vertical FOV is narrower than horizontal —
products could be clipped top/bottom. Now applies an aspect ratio
correction factor to camera distance: wider formats push camera
further back proportionally.

Still render (_blender_camera.py) already handled this via
min(fov_h, fov_v); cinematic now has equivalent correction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:15:10 +01:00
Hartmut 81eb1f9eae fix: cinematic camera distances — no clipping, full product always visible
Redesigned all 3 segments to keep distance > 3× bsphere_radius:
- Segment 1: 3.5× → 3.0× (establishing orbit, 50mm)
- Segment 2: 3.0× → 3.2× (low angle sweep, 50→65mm)
- Segment 3: 3.2× → 4.0× (crane up, 50→40mm)

Removed: detail closeup (was 1.5-1.8×, caused object clipping),
telephoto 85mm (caused extreme close framing), DOF (not needed at safe distance)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:21:48 +01:00
Hartmut 3a815a85c5 fix: Blender 5.0 Action.fcurves API change — safe LINEAR keyframe setting
Blender 5.0 renamed Action.fcurves. Now tries fcurves first, falls
back to channels, and wraps in try/except so the render proceeds
even if LINEAR interpolation can't be set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:05:58 +01:00
Hartmut 458c6cd813 refactor: cinematic render — linear keyframes, 3 segments, 250 frames, white bg
Changes per user feedback:
- Keyframe interpolation: BEZIER → LINEAR (all fcurves set to LINEAR)
- Removed segment 4 (closeup) — now 3 segments only
- Frame count: 480 → 250 (10 seconds at 25fps)
- FPS: 24 → 25
- Easing removed — pure linear interpolation between segment params
- White background by default (World node Color = white)
- Transparent bg still available as override

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:57:42 +01:00
Hartmut 75ad397c09 fix: unbuffered stdout for live cinematic frame progress
Two fixes for frame progress not appearing in frontend:
1. Added flush=True to all print() calls in cinematic_render.py
2. Set PYTHONUNBUFFERED=1 in subprocess environment

Without these, Python buffers stdout inside Blender, so all frame
progress lines arrive in a batch after the process exits instead
of streaming line-by-line during rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:53:26 +01:00
Hartmut f22b963be9 feat: cinematic highlight render — 20s procedural camera animation
New render type: 4-segment cinematic camera animation (480 frames @ 24fps)
for professional product highlight videos.

Camera sequence:
1. Establishing (5s): slow 45° orbit + push-in, 50mm lens
2. Detail sweep (5s): low-angle close arc, 85mm telephoto, shallow DOF
3. Crane up (5s): rising 30°→60°, 35mm wide reveal, pull-back
4. Hero close (5s): push-in to beauty angle, 65mm, smooth ease-out

Technical:
- cinematic_render.py: procedural camera from bounding sphere, cubic easing,
  per-frame keyframes (location, rotation, focal length, DOF)
- render_cinematic_to_file(): service function (same pattern as turntable)
- Pipeline routing: render_settings.cinematic flag → cinematic path
- Depth of field enabled (f-stop scales with product size)
- use_persistent_data for BVH caching between frames
- Same material/template/USD pipeline as turntable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:25:56 +01:00
Hartmut ffe3eebfca perf: render pipeline optimizations — sample scaling, USD logging, persistent BVH
Task 1: Resolution-aware sample count
- Auto-scale samples for resolutions <= 1024: max(32, samples * max_dim / 2048)
- 512x512 thumbnails: 256 → 64 samples (75% GPU savings)
- Thumbnail tasks capped at 64 samples via context manager
- 2048x2048 HQ renders unchanged

Task 2: USD path preference audit + logging
- Verified USD master path is correctly preferred over GLB tessellation
- Added clear emit() messages: "Using USD master" vs "No USD master — GLB path"
- Dynamic render log label: "USD → Blender" vs "STEP → GLB → Blender"

Task 3: Persistent BVH for turntable animations
- Added scene.render.use_persistent_data = True before frame loop
- BVH acceleration structure cached between frames (not rebuilt per frame)
- Applies to both camera orbit and object rotation modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:03:31 +01:00
Hartmut 7e57dba085 fix: add --material-override support to turntable_render.py
Both camera-orbit and object-rotation modes now parse and apply
the --material-override CLI arg, overriding USD primvar lookups
and material_map before assignment — same pattern as still renders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:21:53 +01:00
Hartmut c054236d22 fix: material override pipeline — pass --material-override CLI arg to Blender scripts
The initial implementation only overrode the material_map dict in the task,
but the Blender USD primvar path bypassed it. Now:
- Added --material-override named CLI arg parsed in _blender_args.py
- Both Mode A (factory) and Mode B (template) in _blender_scene_setup.py
  override usd_material_lookup and material_map when set
- Passed through full chain: task → step_processor → render_blender → CLI → Blender
- Tested: 175-part bearing rendered with single Steel-Bare material (1/1 materials)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:19:21 +01:00
Hartmut b583b0d7a2 feat: per-position camera settings, material alias dialog, product delete, media browser links
- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:16:37 +01:00
Hartmut 0020376702 fix: GLB tessellation destroyed by BRepBuilderAPI_Transform + MergeFaces
Root cause 1: BRepBuilderAPI_Transform(shape, trsf, copy=True) destroys
all Poly_Triangulation data. The mm→m scaling was applied before export,
wiping the tessellation from BRepMesh_IncrementalMesh.

Fix: Remove BRepBuilderAPI_Transform entirely — RWGltf_CafWriter already
handles mm→m conversion and Z-up→Y-up rotation internally.

Root cause 2: RWGltf_CafWriter with MergeFaces=False (the default) fails
to find per-face tessellation from the XCAF component hierarchy, producing
degenerate meshes (~2 vertices per face instead of thousands).

Fix: SetMergeFaces(True) to compose face triangulations into proper
per-shape mesh buffers. Vertex count goes from 1,212 to 46,573.

Also bumps cache key version to v2 to invalidate broken cached GLBs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:23:41 +01:00
Hartmut 253f11a945 feat: surface-evaluated normals, GMSH tessellation, draw call batching
USD exporter:
- Compute normals from B-Rep surface via BRepLProp_SLProps at each vertex
  UV parameter — eliminates faceting on curved surfaces (same as Stepper)
- Add GMSH Frontal-Delaunay tessellation engine (opt-in via --tessellation_engine gmsh)
  with per-solid strategy matching export_step_to_gltf.py
- Use vertex normal interpolation instead of faceVarying (6x smaller normals)
- Default engine remains OCC (GMSH has coordinate-space bug with instanced parts)

Frontend:
- Fix faceted shading in InlineCadViewer: only call computeVertexNormals()
  when geometry lacks normals, preserving smooth GLB normals from pipeline
- Add useGeometryMerge hook for draw call batching (merge by material)
- Fix unused import in cadUtils, optional props in ThreeDViewer

Backend:
- Move dataclass import to top of step_processor.py (PEP 8)
- Unified single-read STEP metadata extraction with fallback

Render worker:
- Fix USD import seam/sharp restoration: read primvars via pxr directly
  (Blender's USD importer doesn't expose custom Int2Array primvars)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:14:23 +01:00
Hartmut 6c5873d51f feat: performance optimizations + part-materials validation
- @timed_step decorator with wall-clock + RSS tracking (pipeline_logger)
- Blender timing laps for sharp edges and material assignment
- MeshRegistry pattern: eliminate 13 scene.traverse() calls across viewers
- Lazy material cloning (clone-on-first-write in both viewers)
- _pipeline_session context manager: 7 create_engine() → 2 in render_thumbnail
- KD-tree spatial pre-filter for sharp edge marking (bbox-based pruning)
- Batch material library append: N bpy.ops.wm.append → single bpy.data.libraries.load
- GMSH single-session batching: compound all solids into one tessellation call
- Validate part-materials save endpoints against parsed_objects (prevents bogus keys)
- ROADMAP updated with completion status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:53:14 +01:00
Hartmut ec667dd56a refactor: remove dead export_gltf.py, cleanup rendering tasks, improve tessellation UI
- Remove export_gltf.py (Blender-based GLB export replaced by OCC direct)
- Remove unused export_gltf_for_order_line_task
- Add Ultra tessellation preset to Admin settings
- Improve tessellation preset descriptions and styling
- Minor cleanup across media, rendering, and workflow modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:37:35 +01:00
Hartmut d843162e5f feat(PBR): extract Blender PBR properties and apply in 3D viewer
Extract Base Color, Metallic, Roughness, Transmission, IOR from Blender
asset library materials via catalog_assets.py. Store in catalog JSON and
serve via /api/asset-libraries/pbr-map endpoint. Frontend viewers apply
PBR properties to Three.js MeshStandardMaterial using hex color strings
(avoiding Three.js ColorManagement sRGB/linear issues).

Key fixes:
- RLS bypass for material alias lookup in pbr-map endpoint
- pbrMap empty guard prevents premature grey fallback in viewers
- Cache-Control: no-cache on pbr-map requests to avoid stale data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:37:23 +01:00
Hartmut 577dd1ca7e refactor(P11+P12): codebase hygiene — CLAUDE.md rewrite, type safety, dead code removal
- Rewrite CLAUDE.md to match current 8-service architecture (was 11, 5 deleted)
- Remove all as-any casts in OrderDetail.tsx (9 casts → 0)
- Add cad_parsed_objects/cad_part_materials to OrderItem interface
- Rename require_admin → require_global_admin across 6 router files (22 calls)
- Remove EXPORT_GLB_PRODUCTION enum + generate_gltf_production_task (dead code)
- Remove worker-thumbnail from ALLOWED_SERVICES, replace Flamenco link
- Delete obsolete PLAN.md (1455 lines) and PLAN_REFACTOR.md (1174 lines)
- Fix digit-only USD prim names with p_ prefix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 07:22:04 +01:00
Hartmut 3dcfa7c0bd fix(USD): preserve full XCAF hierarchy with local transforms
Rewrite _traverse_xcaf → _author_xcaf_to_usd that recursively authors
USD prims mirroring the XCAF assembly tree:
- Assembly nodes become UsdGeom.Xform prims with local transforms from
  each component label's Location (not composed with parents)
- Leaf shapes get definition-space vertices (face_loc only, no instance
  placement) — the USD scene graph composes transforms hierarchically
- Coordinate swap (X,-Z,Y) now authored once as a root Xform on
  /Root/Assembly instead of per-vertex transformation
- Sharp/seam edges extracted per-part from definition shape (not global)

This fixes misplaced geometry for sub-assembly parts (e.g. KOMP-EIN
roller cages with -45° Z rotation) that were previously lost by the
flat traversal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:45:02 +01:00
Hartmut 078420c5f1 fix(USD): use canonical SCHAEFFLER material names for USD material bindings
Material prims now use resolved canonical names (e.g.
SCHAEFFLER_010101_Steel_Bare) instead of source object names. When
importing USD in Blender, materials show the correct SCHAEFFLER names
directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:19:58 +01:00
Hartmut b72d8498b9 fix(USD): correct coordinate transform (Z-up stage) and store canonical_material in DB
- export_step_to_usd.py: change stage from Y-up to Z-up, keep (X,-Z,Y)
  transform — matches GLB orientation exactly (verified: bounding box match)
- export_glb.py: include canonical_material in resolved_material_assignments
  DB field (was being dropped during manifest parsing)
- import_usd.py: use pxr customData read (not string primvars — Blender
  ignores those, confirmed by testing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:17:45 +01:00
Hartmut cc3071297b feat(M5-M7): embed canonical material names in USD via customData + pxr direct read
- export_step_to_usd.py: accept --material_map CLI arg, write
  schaeffler:canonicalMaterialName as customData on each Mesh prim,
  fix geometry transform (strip shape Location before face exploration,
  apply both face_loc and shape_loc sequentially)
- import_usd.py: after Blender USD import, use pxr to read customData
  directly from the USD file — builds {part_key: material_name} lookup
  (Blender ignores STRING primvars and customData, but pxr reads both)
- _blender_materials.py: add apply_material_library_direct() for exact
  dict-based material assignment without name-matching heuristics
- _blender_scene_setup.py: prefer direct USD lookup, fall back to
  name-matching for legacy USD files without material metadata
- export_glb.py (generate_usd_master_task): resolve material_map via
  material_service.resolve_material_map() and pass to subprocess;
  include material hash in cache key for invalidation
- ROADMAP.md: update P5 status, add M5-M7 milestones

Tested: 3/3 parts matched (ans_lfs120), 172/175 parts matched
(F-802007.TR4-D1-H122AG). Previous: 0/25 matched.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:04:26 +01:00
Hartmut 1321ef2bd4 refactor: rename thumbnail_rendering queue to asset_pipeline
The queue handles far more than thumbnails: OCC tessellation, USD master
generation, GLB production, order line renders, and workflow renders.
asset_pipeline better reflects its role as the render-worker's primary queue.

Updated all references in: task decorators, celery_app.py, beat_tasks.py,
docker-compose.yml worker command, worker.py MONITORED_QUEUES, admin.py,
CLAUDE.md, LEARNINGS.md, Dockerfile, helpTexts.ts, test files,
and all .claude/commands/*.md skill files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:28:38 +01:00
Hartmut 8e1cd41868 fix(critical): SQLAlchemy mapper crash + material matching for USD renders + kanban drag-to-reject
- beat_tasks.py: import app.models at module level so SQLAlchemy can
  resolve relationship("Template") and relationship("User") when domain
  models are imported in isolation inside task functions. Fixes all
  beat tasks (batch_render_notifications, recover_stuck_cad_files) that
  crashed every 60s with mapper initialization error.

- _blender_materials.py: build_mat_map_lower() now adds a slug-normalized
  key variant (re.sub([^a-z0-9]+, _, kl)) for each mat_map entry. OCC
  part names like 'F-802007_TR4-D1-H122AG' → slug 'f_802007_tr4_d1_h122ag'
  now matches USD-imported Blender objects. Existing prefix fallback
  (key.startswith(part_key)) catches AF-suffix variants.

- Orders.tsx: kanban drag-to-reject implemented. submitted/processing
  cards are draggable (cursor-grab). Rejected column highlights with
  red ring on drag-over. Drop opens reject reason modal via createPortal.
  Confirm calls rejectOrder() mutation + invalidates orders cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:21:46 +01:00
Hartmut 71584edce6 docs: learning erfasst - OCC face_loc behavior und GLB-Cache-Invalidierung nach Tessellierungs-Fixes
Dokumentiert drei neue Learnings aus der GE360-HF Wälzkörper-Positions-Untersuchung:
1. BRepMesh auf Compound: Triangulation in Definition-Space, Face-loc = Instance-Placement
2. IsSame() vs IsPartner() für Assembly-Instanz-Deduplizierung
3. Stale GLB-Cache maskiert Code-Fixes — nach Tessellierungs-Änderungen Cache invalidieren

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 18:08:48 +01:00
Hartmut de7f97be87 fix(render): fix double-transform bug in USD mesh extraction causing wrong part positions
In _extract_mesh(), BRep_Tool.Triangulation_s(face, face_loc) returns a face_loc
that already encodes the instance's full placement transform when a compound shape
is tessellated with BRepMesh_IncrementalMesh. Applying shape_trsf on top doubled
every rotation/translation, causing multiple roller elements to collapse to the same
wrong world position (e.g. Z(-75°)×2 ≡ Z(+105°)×2 mod 360° → identical positions).

Fix: use elif so shape_loc is only applied as a fallback when face_loc is identity.
Adds seam edge extraction (UV seam primvar) and improves _traverse_xcaf doc.

docs: learning erfasst - OCC face_loc double-transform in compound tessellation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 16:43:33 +01:00
Hartmut 409fb92899 feat(P2): USD Foundation — canonical part identity + material overrides
M1 — USD exporter:
- render-worker/scripts/export_step_to_usd.py (631 lines)
  Full XCAF traversal, one UsdGeom.Mesh per leaf part,
  schaeffler:partKey on every prim, index-space sharpEdgeVertexPairs
- render-worker/Dockerfile: usd-core>=24.11 installed (USD 0.26.3)

M2 — usd_master MediaAsset + pipeline auto-chain:
- migrations 060 (usd_master enum), 061 (3 JSONB columns),
  062 (rename tessellation settings keys)
- generate_usd_master_task: runs export_step_to_usd.py, upserts
  usd_master MediaAsset, writes resolved_material_assignments to CadFile
- Auto-chained from generate_gltf_geometry_task after every GLB export
- step_tasks.py shim re-exports generate_usd_master_task

M3 — scene-manifest API:
- part_key_service.py: build_scene_manifest(), generate_part_key(),
  four-layer material priority resolution with provenance
- SceneManifest / PartEntry Pydantic models in products/schemas.py
- GET /api/cad/{id}/scene-manifest endpoint (graceful fallback to
  parsed_objects when USD not yet generated)
- POST /api/cad/{id}/generate-usd-master endpoint
- frontend/src/api/sceneManifest.ts: fetchSceneManifest(),
  triggerUsdMasterGeneration()

M4 — manual-material-overrides API:
- GET/PUT /api/cad/{id}/manual-material-overrides endpoints
- CadFile.manual_material_overrides JSONB column (migration 061)
- getManualOverrides() / saveManualOverrides() in cad.ts

M5 — ThreeDViewer partKey integration:
- export_step_to_gltf.py injects partKeyMap into GLB extras
- ThreeDViewer: partKeyMap extraction, resolvePartKey(), effectiveMaterials
  merges legacy partMaterials + new manualOverrides (server-side persistence)
- MaterialPanel: dual-path save (partKey vs legacy), provenance badge,
  reconciliation panel for unmatched/unassigned parts

Also:
- Admin.tsx: generate-missing-usd-masters + canonical scenes bulk actions
- ProductDetail.tsx: usd_master row in asset table
- vite-env.d.ts: fix ImportMeta.env TypeScript error
- GPUProbeResult: add timestamp/devices/render_time_s fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 13:11:09 +01:00
Hartmut 47b5d42bb5 refactor(P1): M1 dead code removal + M3 blender_render.py split
M1 — dead code removed:
- Delete blender-renderer/ and threejs-renderer/ source files
- Remove PIL/Pillow fallback block from step_processor.py
  (_generate_thumbnail_placeholder, _finalise_image JPG path)
- Remove stl_quality param from render_blender.py, render_still_task,
  render_turntable_task (was always "low"; hardcode deflection values)
- render_turntable_task now reads scene_linear/angular_deflection from
  system_settings (consistent with export_glb.py pipeline)

M3 — blender_render.py split from 263 → 68 lines:
- _blender_args.py: parse_args() — all 25 positional + named args
- _blender_scene_setup.py: setup_scene() — MODE A/B including USD import
- _blender_render_config.py: configure_and_render() — engine + output

Post-review fixes:
- _db_engine.dispose() after settings read in render_turntable_task
- _finalise_image() fmt param removed (always PNG; PIL never installed)
- _blender_import.py committed together with new submodules to satisfy
  import_usd_file dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 12:54:40 +01:00
Hartmut 393e4b92a7 refactor(P1): complete pipeline cleanup — M1 dead code + M3 blender split
M1 dead code removal:
- admin.py: remove VALID_STL_QUALITIES + stl_quality (7 locations)
- frontend: remove stl_quality from 6 files (api/orders.ts, api/worker.ts,
  WorkerActivity.tsx, RenderInfoModal.tsx, helpTexts.ts, mocks/handlers.ts)
- blender_render.py: delete _mark_sharp_and_seams() — dead, never called (62 lines)
- step_processor.py: delete _render_via_service() + 2 elif renderer=="threejs" branches
- renderproblems_tmp/: remove 3 orphaned debug images

M3 blender_render.py decomposition (858 → 248 lines):
- _blender_gpu.py: activate_gpu(), configure_engine()
- _blender_import.py: import_glb(), apply_rotation()
- _blender_materials.py: FAILED_MATERIAL_NAME, assign_failed_material(),
  build_mat_map_lower(), apply_material_library()
- _blender_camera.py: setup_auto_camera(), setup_auto_lights()
- _blender_scene.py: ensure_collection(), apply_smooth_batch(),
  apply_sharp_edges_from_occ(), setup_shadow_catcher()
- Entry-point: sys.path.insert for submodule discovery; arg-parse + Mode A/B orchestration only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 22:19:59 +01:00
Hartmut d938c4db1b fix(materials): universal FailedMaterial sentinel for unmatched mesh objects
- export_gltf.py: replace single-material fallback (only fired when
  len(appended)==1) with a universal sentinel that appends
  SCHAEFFLER_059999_FailedMaterial unconditionally and assigns it to
  every mesh object not matched by name-based lookup.
  Also adds in-memory magenta fallback if library append fails.
  Removes 2 temporary [DEBUG] print lines from investigation.

- blender_render.py: add FailedMaterial assignment inside
  _apply_material_library() for unmatched parts (was log-only before).
  Includes copy-on-write guard (users > 1) matching existing pattern.

Also added alias 'Stahl; Durotect CMT' (semicolon) → Durotect-Blue
to cover STEP files using semicolon separator instead of comma.

Verified: 23/25 objects matched correctly, 2 ISO8734 dowel pins
(empty material) receive SCHAEFFLER_059999_FailedMaterial as sentinel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 21:49:37 +01:00
Hartmut 638b93bb1e fix(gmsh): fix mirror instances + reduce mesh size to ≤120% of OCC
Bug 1 — Missing parts (mirror/repeated instances):
- id(solid.TShape()) is unreliable in OCP: each call creates a new
  Python wrapper, so id() always differs even for the same TShape.
  Replaced with IsSame() for correct TShape-pointer deduplication.
- TopExp_Explorer(SOLID) misses free shells/faces in assemblies.
  Fix: run BRepMesh baseline on full root compound first (catches all
  face types), then GMSH overrides per unique solid for better seam
  topology. REVERSED solids keep BRepMesh to avoid inverted Jacobians.

Bug 2 — GLB 7× too large (21 MB vs OCC 3 MB):
- CharacteristicLengthMax = linear_deflection × 50 (was ×15)
  matches OCC effective edge length on curved surfaces (~5 mm).
- MinimumCirclePoints = min(12, ...) (was min(20, ...))
- Result: GMSH 91% of OCC file size (target ≤120% ✓)

Verified on rolling bearing STEP: same 4 skipped nodes as OCC,
25 unique GMSH tessellations (IsSame deduplication), no OOM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 21:12:03 +01:00
Hartmut dbc032ec74 feat(gmsh): GMSH Frontal-Delaunay tessellation for clean cylinder seams
- Per-solid iteration prevents OOM on multi-part assemblies (25-part bearing:
  2.3GB RAM when processing compound → ~100MB per solid with per-solid approach)
- Fix CharacteristicLengthMax multiplier 5× → 15× and cap MinimumCirclePoints
  at 20 (prevents 63-pts/circle on angular_deflection=0.1rad → 231MB → 21MB)
- Geometry task timeout 120s → 600s for large assemblies
- Production task: reuse _geometry.glb when GMSH enabled (no re-tessellation)
  and cache _production_geom.glb for OCC (mtime vs STEP check)
- Viewer now prefers production GLB when available (shows correct GMSH mesh)
- GMSH OpenMP multithreading (min(cpu_count,16)) for 4.4× speedup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:45:27 +01:00
Hartmut 9703aec497 perf(P3): enable GMSH OpenMP multithreading — 4.4x faster tessellation
GMSH defaults to single-threaded meshing. Setting General.NumThreads,
Mesh.MaxNumThreads1D and Mesh.MaxNumThreads2D to min(cpu_count, 16) enables
parallel Frontal-Delaunay surface meshing across all available cores.

Benchmark on 121-face assembly (32-core host, capped at 16 threads):
  Before: 12.7s total (9.8s in gmsh.model.mesh.generate)
  After:   2.8s total (1.1s in gmsh.model.mesh.generate)

Cap at 16 threads — benchmark showed 16 threads (1.1s) matches or beats auto
(1.6s), likely due to NUMA/coordination overhead above that threshold.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:34:40 +01:00
Hartmut af320bcdc8 feat(P3): add GMSH Frontal-Delaunay tessellation engine
Introduces GMSH as an alternative to OCC BRepMesh for STEP→GLB tessellation.
GMSH produces conforming meshes that eliminate fan triangles at cylinder seam
edges — a structural limitation of OCC BRepMesh that cannot be fixed via
deflection parameters.

Changes:
- render-worker/Dockerfile: install gmsh>=4.15.0 + libglu1-mesa + libxft2
- export_step_to_gltf.py: --tessellation_engine occ|gmsh CLI arg +
  _tessellate_with_gmsh() using BRep→GMSH→Poly_Triangulation write-back
- admin.py: tessellation_engine setting (SETTINGS_DEFAULTS, SettingsOut,
  SettingsUpdate, validation)
- export_glb.py: pass tessellation_engine to export_step_to_gltf.py CLI in
  both geometry and production GLB tasks
- Admin.tsx: radio button UI for OCC vs GMSH selection

Tested: 121 faces meshed, 0 BRepMesh fallback, 649K triangles on sample part.
Clean seam edges for UV unwrap — GMSH respects B-rep periodic face boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:17:26 +01:00
Hartmut ca62319688 feat: sharp edge pipeline V02, tessellation presets, media cache-bust, GMSH plan
Sharp Edge Pipeline V02:
- export_step_to_gltf.py: replace BRep_Tool.Polygon3D_s (returns None in XCAF) with
  GCPnts_UniformAbscissa curve sampling at 0.3mm step — extracts 17,129 segment pairs
- Inject sharp_edge_pairs + sharp_threshold_deg into GLB extras (scenes[0].extras)
  via binary GLB JSON-chunk patching (no extra dependency)
- export_gltf.py: read schaeffler_sharp_edge_pairs from Blender scene custom props,
  apply via KD-tree to mark edges sharp=True + seam=True (OCC mm Z-up → Blender transform)
- tools/restore_sharp_marks.py: dual-pass (dihedral angle + OCC pairs), updated coordinate
  transform (X, -Z, Y) * 0.001

Tessellation:
- Admin UI: Draft / Standard / Fine preset buttons with active-state highlighting
- Default angular deflection: preview 0.5→0.1 rad, production 0.2→0.05 rad
- export_glb.py: read updated defaults from system_settings

Media / Cache:
- media/service.py: get_download_url appends ?v={file_size_bytes} cache-buster
- media/router.py: Cache-Control: no-cache for all download/thumbnail endpoints

Render pipeline:
- still_render.py / turntable_render.py: shared GPU activation + camera improvements
- render_order_line.py: global render position support
- render_thumbnail.py: updated defaults

Frontend:
- InlineCadViewer: file_size_bytes-aware URL update triggers re-fetch on regeneration
- ThreeDViewer: material panel, part selection, PBR mode improvements
- Admin.tsx: tessellation preset cards, GMSH setting dropdown
- MediaBrowser, ProductDetail, OrderDetail, Orders: various UI improvements
- New: MaterialPanel, GlobalRenderPositionsPanel, StepIndicator components
- New: renderPositions.ts API client

Plans / Docs:
- plan.md: GMSH Frontal-Delaunay tessellation plan (6 tasks)
- LEARNINGS.md: OCC Polygon3D_s None issue + GCPnts fix
- .gitignore: add backend/core (core dump from root process)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 14:40:36 +01:00
Hartmut 202b06a026 feat(export_gltf): embed sharp angle in GLB extras + restore script
- Add export_extras=True to bpy.ops.export_scene.gltf() call
- Store schaeffler_sharp_angle_deg in scene custom props before export
  → value is embedded in scenes[0].extras in the GLB JSON chunk
  → survives import/export round-trip intact (verified: 30.0 restored)
- Add tools/restore_sharp_marks.py: companion Blender script that reads
  the angle from scene.get("schaeffler_sharp_angle_deg") and re-applies
  mark_sharp() + mark_seam() on all mesh objects after GLB import

GLB format cannot store per-edge sharp/seam flags natively; the visual
shading is correct via vertex splits. The extras + restore script give
users the ability to reconstruct Edit Mode markers without a second format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:38:47 +01:00
Hartmut e189934b12 fix(export_gltf): use edit-mode mark_sharp+mark_seam for proper GLB sharp edges
Replace shade_smooth_by_angle with explicit edit-mode operators:
- edges_select_sharp(angle) → mark_sharp() + mark_seam()
- Produces vertex splits at sharp edges (6027 split positions verified)
- Remove OCC custom_normal attribute before processing to prevent
  pre-baked normals overriding Blender's sharp edge processing
- Update comment: calc_normals_split() removed in Blender 5.0

Verified: production GLB has 812 extra vertices vs geometry GLB,
6027 positions with multiple normals = sharp edges correctly encoded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:20:54 +01:00
Hartmut 72123c5aa9 fix(export_gltf): clear OCC custom_normal attribute before sharp edge processing
The geometry GLB from export_step_to_gltf.py contains a 'custom_normal' attribute
(CORNER, INT16_2D) from OCC tessellation. If left in place, Blender's glTF exporter
re-exports these pre-baked normals unchanged — ignoring shade_smooth_by_angle
processing and our explicit sharp edge marks.

Fix: remove the 'custom_normal' attribute from all imported mesh objects immediately
after GLB import, before applying smooth shading. Also add orphans_purge() before
export to remove palette materials (mat_0/1/2/3) that become users=0 after library
material substitution.

Same custom_normal clearing applied to blender_render.py for thumbnail renders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 10:46:21 +01:00
Hartmut a1d140d30f fix(render): production GLB sharp edges + materials (25/25)
Sharp edges:
- OCC→Blender coordinate transform was wrong: Blender(X,Y,Z) = OCC(X×0.001, -Z×0.001, Y×0.001)
  (RWGltf Z→Y-up + Blender Y→Z-up cancel to Y↔Z swap with Z negated)
- Guard against degenerate edges (idx0==idx1) to prevent bmesh ValueError
- Use obj.matrix_world @ v.co for world-space KD-tree (assembly node transforms)

Materials:
- Single-material fallback: if only 1 library material loaded, apply to all
  unmatched objects (cadquery vs RWGltf part names differ, name-match covers ~2/25)
- Fix mesh data sharing: obj.data.copy() before materials.clear() to avoid
  clearing shared data blocks
- Use obj.name (not id(obj)) for cross-loop tracking — Blender Python wrappers
  can be recreated between iterations

Both fixes applied to export_gltf.py (production GLB) and blender_render.py (thumbnails).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 10:29:08 +01:00
Hartmut 8933d0be17 fix(render+roles): batch smooth shading + step timings + global_admin role support
Render pipeline:
- Replace per-object _apply_smooth() loop with _apply_smooth_batch(): selects
  all 175 parts, calls shade_smooth_by_angle() ONCE in C → reduces 16s to ~0.2s
- Remove 175 per-part "assigned material to part" log lines (replace with summary)
- Add TIMING_SUMMARY log line at end of every render showing all step durations
- _lap() helper records split times for: template_load, glb_import, rotation,
  smooth_shading, material_assign, pre_render_setup, gpu_render

Frontend role checks:
- Add global_admin + tenant_admin to User role type in auth store
- Add isAdmin() and isPrivileged() helper functions
- Fix Admin.tsx, Layout.tsx, Notifications.tsx, OrderDetail.tsx, ProductDetail.tsx,
  CostOverviewWidget.tsx — all were checking role === 'admin' but JWT now has
  role === 'global_admin' after migration 049 (admin → global_admin backfill)
- This caused Admin page to render completely empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 21:29:22 +01:00
Hartmut 34f89cc225 feat(gpu): GPU health check + RENDER_DEVICE_USED token + strict mode
- gpu_probe.py: Blender script that probes OPTIX/CUDA/HIP/ONEAPI and
  exits 1 on no GPU — used at startup + on-demand from Admin UI
- blender_render.py, still_render.py, turntable_render.py: emit
  RENDER_DEVICE_USED: engine=CYCLES device=GPU|CPU compute_type=...
  after GPU activation; exit 2 when CYCLES_DEVICE=gpu and CPU fallback
- render_blender.py: parse RENDER_DEVICE_USED token into render_log
  (device_used, compute_type, gpu_fallback); handle exit code 2 as
  explicit GPU strict-mode failure
- check_version.py: check_gpu() runs gpu_probe.py at container startup;
  CYCLES_DEVICE=gpu aborts startup if no GPU found
- docker-compose.yml: CYCLES_DEVICE=${CYCLES_DEVICE:-auto} env var
- gpu_tasks.py: probe_gpu Celery task on thumbnail_rendering queue;
  saves result to system_settings.gpu_probe_last_result; beat every 30min
- worker.py: POST /probe/gpu (trigger) + GET /probe/gpu/result (last result)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:57:36 +01:00
Hartmut 9f54bc3ab1 feat(phase4+5): role hierarchy, tenant config, fallback material, dead code removal
Phase 4.1 — Role Hierarchy:
  - UserRole enum: add global_admin (platform operator) + tenant_admin
    (per-tenant admin); keep legacy 'admin' for backward compat
  - Role sets: ADMIN_ROLES, TENANT_ADMIN_ROLES, PM_ROLES, RLS_BYPASS_ROLES
  - New auth guards: require_global_admin(), require_tenant_admin_or_above(),
    require_pm_or_above(), is_admin(), is_privileged()
  - Legacy require_admin / require_admin_or_pm now check both old+new roles
  - Migration 049: ADD VALUE global_admin + tenant_admin with AUTOCOMMIT
    workaround; backfills admin → global_admin
  - Seed: new admin users created with global_admin role

Phase 4.3 — RLS bypass updated for global_admin in get_db + set_tenant_context

Phase 4.4 — Tenant Feature Flags:
  - Migration 050: tenant_config JSONB on tenants table
  - Tenant model: tenant_config field + get_config() accessor
  - Defaults: max_concurrent_renders=3, fallback_material, invoice_prefix etc.

Phase 5.1 — Fallback Material:
  - blender_render.py: replace PALETTE_LINEAR/PALETTE_HEX/_assign_palette_material
    with _assign_failed_material() → SCHAEFFLER_059999_FailedMaterial (magenta)
  - Unmatched parts now logged explicitly before rendering

Phase 5.2 — Remove EEVEE fallback:
  - render_blender.py: EEVEE→Cycles silent retry removed; hard failure on EEVEE error

Phase 5.3 — Remove Blender version check:
  - render_blender.py: deleted MIN_BLENDER_VERSION = (5, 0, 1) constant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:42:10 +01:00
Hartmut 966c3aed57 feat(refactor/phase2): pipeline modularity + dead code removal
Phase 2.3 — Fix render cancellation (real Celery task ID):
  - orders.py cancel endpoints: read celery_task_id from render_job_doc
    instead of synthetic "render-{line_id}" which was a no-op
  - render_order_line_still_task: creates RenderJobDocument at task start,
    stores self.request.id as celery_task_id, writes step-level state
    (RESOLVE_STEP_PATH → BLENDER_STILL) back to order_lines.render_job_doc

Phase 3.1 — Remove Pillow overlay dead code:
  - blender_render.py: deleted 55-line Pillow post-processing block
    (lines 798-851, green bar + model name label)
  - transparent_bg=True is always passed; the else branch was unreachable
  - Removed mention from script docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:27:33 +01:00
Hartmut ee6eb34b4c feat: GPU rendering + material matching + perf improvements
- GPU: fix Cycles device activation order — set compute_device_type
  BEFORE engine init, re-set AFTER open_mainfile wipes preferences
- GPU: remove _mark_sharp_and_seams edit-mode loop (redundant with
  Blender 5.0 shade_smooth_by_angle), saves ~200s/render on 175 parts
- Material: fix _AFN suffix mismatch — build AF-stripped mat_map keys
  and add prefix fallback in _apply_material_library (blender_render.py)
- Material: production GLB now uses get_material_library_path() which
  checks active AssetLibrary instead of empty legacy system setting
- Admin: RenderTemplateTable multi-select output types (M2M frontend)
- Admin: MaterialLibraryPanel replaced with link to Asset Libraries
- UX: move Toaster to top-left to avoid dispatch button overlap
- SQLAlchemy: add .unique() to all RenderTemplate M2M collection queries
- Logging: flush=True on all Blender progress prints, stdout reconfigure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:05:03 +01:00
Hartmut 934728da77 fix(glb): 175/175 material substitution via _AFN suffix stripping
OCC's RWGltf_CafWriter appends _AF0/_AF1 assembly-instance suffixes to
mesh object names when a part appears multiple times in an assembly.
The material matching in export_gltf.py only stripped Blender's .001
suffix, leaving 24/175 GLB objects without materials.

Fix: strip _AFN suffixes via while loop (handles nested _AF0_AF1),
add prefix fallback (longest key wins) as last resort before no-match.

Also improve build_materials_from_excel Jaccard matching:
- Strip _AFN and numeric hash suffixes (-21227) before tokenizing
- Add prefix-based fallback (step 3) before position fallback (step 4)
- Raise threshold 0.3 → 0.35 for better precision
- Guard prefix matches to len >= 5 to prevent trivial false positives

Result: Material substitution: 175/175 mesh objects assigned (was 151/175)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:15:11 +01:00
Hartmut 95cfe0aa93 refactor: replace STL intermediary with OCC-native STEP→GLB pipeline
- export_step_to_gltf.py: STEP→GLB via RWGltf_CafWriter + BRepBuilderAPI_Transform
  (mm→m pre-scaling, XCAFDoc_ShapeTool.GetComponents_s static method)
- Blender scripts (blender_render.py, still_render.py, turntable_render.py,
  export_gltf.py, export_blend.py): import GLB instead of STL, remove _scale_mm_to_m
- step_tasks.py: add generate_gltf_production_task, remove generate_stl_cache,
  replace _bbox_from_stl with _bbox_from_glb (trimesh), auto-queue geometry GLB
  after thumbnail render
- render_blender.py: replace _stl_from_cache_or_convert with _glb_from_step,
  remove convert_step_to_stl and export_per_part_stls
- domains/rendering/tasks.py: update render_turntable_task, export_gltf/blend tasks
  to use GLB instead of STL
- cad.py: remove STL download/generate endpoints, add generate-gltf-production
- admin.py: generate-missing-stls → generate-missing-geometry-glbs
- Frontend: replace STL cache UI with GLB generate buttons, remove stl_cached field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 16:49:18 +01:00
Hartmut 3eba7b2d37 fix(glb): remove invalid export_colors param + fix viewer stale mesh
4 root causes fixed:

1. export_colors=False was removed in Blender 4.x — caused every Blender
   export to fail (exit 1) and always fall back to trimesh. Remove it.
   Blender now runs the full pipeline: materials + sharp edges.

2. GlbModel cloned ref never reset on url change — key={glbBlobUrl} forces
   React to remount GlbModel on each new blob URL, resetting the ref so
   fresh geometry is always loaded.

3. glbBlobUrl not cleared before re-fetch — setGlbBlobUrl(null) added at
   start of downloadUrl effect so spinner shows instead of stale mesh.

4. staleTime: 30_000 delayed picking up new MediaAsset after generation.
   Changed to staleTime: 0 so invalidation always triggers immediate refetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 15:46:42 +01:00
Hartmut e2eda92d82 fix(gltf): append materials (link=False) for proper PBR export to GLB
Linked materials are external references — Blender's GLTF exporter cannot
traverse their node trees to extract Principled BSDF PBR values (metallic,
roughness, base color, normal maps). Appended materials are local copies
that the exporter can fully traverse.

Changes:
- asset_library.py: add link=True parameter (default unchanged for renders)
- export_gltf.py: call apply_asset_library_materials with link=False
- export_gltf.py: add export_materials='EXPORT' + export_image_format='AUTO'
  to embed textures and ensure full PBR data in the GLB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 15:11:33 +01:00
Hartmut 5ee4b2e3b5 fix(export_gltf): update for Blender 5.0 API (wm.stl_import, shade_smooth_by_angle)
- bpy.ops.import_mesh.stl → bpy.ops.wm.stl_import (removed in Blender 4.0)
- mesh.use_auto_smooth = True → bpy.ops.object.shade_smooth_by_angle()
  (use_auto_smooth removed in Blender 4.1)
- Apply smooth shading to all objects unconditionally after scale, so
  GLB is smooth-shaded even when no sharp edge midpoints are present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 14:30:03 +01:00
Hartmut bfd58e3419 fix: media thumbnails, product dimensions, inline 3D viewer, GLB export
Bug A: Media Library thumbnails were gray because <img src> cannot send
JWT auth headers. Added useAuthBlob() hook (fetch + createObjectURL) in
MediaBrowser.tsx. Also fixed publish_asset Celery task to populate
product_id + cad_file_id on MediaAsset for thumbnail fallback resolution.

Bug B: Product dimensions now shown in Product Details card with Ruler
icon and "from CAD" label when cad_mesh_attributes.dimensions_mm exists.

Bug C: Replaced 128×128 CAD thumbnail with InlineCadViewer component.
Queries gltf_geometry MediaAssets, fetches GLB via auth fetch → blob URL
→ Three.js Canvas with OrbitControls. Falls back to thumbnail + "Load 3D
Model" button. Polling when GLB generation is in progress.

Bug D: trimesh was in [cad] optional extra but Dockerfile only installed
[dev]. Changed to pip install -e ".[dev,cad]" — trimesh now available in
backend container, GLB + Colors export works.

Also added bbox extraction (STL-first numpy parsing) in render_step_thumbnail
and admin "Re-extract CAD Metadata" bulk endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 13:27:46 +01:00