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>
This commit is contained in:
+69
-40
@@ -1,59 +1,88 @@
|
||||
# Review Report: Optimized Material Substitution Algorithm
|
||||
Datum: 2026-03-07
|
||||
# Review Report: CAD Viewer Material Assignment Fix + Feature Parity
|
||||
Datum: 2026-03-10
|
||||
|
||||
## Ergebnis: ⚠️ Kleinigkeiten
|
||||
## Ergebnis: ✅ Freigabe
|
||||
|
||||
---
|
||||
|
||||
## Gefundene Probleme
|
||||
|
||||
### [products.py:510] Prefix-Matching ohne Mindestlänge-Guard
|
||||
### [InlineCadViewer.tsx + ThreeDViewer.tsx] Misleading comment on isolateMode reset effect
|
||||
**Schwere**: Gering (Kommentar)
|
||||
|
||||
**Schwere**: Gering
|
||||
|
||||
Der Prefix-Fallback in `build_materials_from_excel` prüft `cad_norm.startswith(excel_norm)` ohne Mindestlänge für `excel_norm`:
|
||||
|
||||
```python
|
||||
if excel_norm and cad_norm and (
|
||||
cad_norm.startswith(excel_norm) or excel_norm.startswith(cad_norm)
|
||||
):
|
||||
In both files the comment reads:
|
||||
```tsx
|
||||
// Reset isolateMode and hideAssigned when no part is pinned
|
||||
useEffect(() => {
|
||||
if (!pinnedPart) setIsolateMode('none') // ← only resets isolateMode, not hideAssigned!
|
||||
}, [pinnedPart])
|
||||
```
|
||||
The comment says "and hideAssigned" but the effect only calls `setIsolateMode('none')`. The behavior is actually correct — `hideAssigned` should NOT be reset when unpinning (it's a persistent view toggle). Only the comment is wrong.
|
||||
|
||||
Wenn ein Excel-Eintrag nach Normalisierung sehr kurz wird (z.B. `"f"` aus `f-12345678.prt`), trifft der Präfix-Check auf fast alle CAD-Namen die mit `f_` beginnen. Schaeffler-Teilenamen sind zwar praktisch immer lang genug, aber das Risiko eines Fehlmatches bei atypischen Einträgen existiert.
|
||||
|
||||
**Empfehlung**: Guard hinzufügen: `len(excel_norm) >= 5 and len(cad_norm) >= 5`.
|
||||
|
||||
---
|
||||
|
||||
### [export_gltf.py:122] Prefix-Fallback iteriert über unsortierte dict-Keys
|
||||
|
||||
**Schwere**: Gering
|
||||
|
||||
```python
|
||||
for key, val in mat_map_lower.items():
|
||||
if lower_base.startswith(key) or key.startswith(lower_base):
|
||||
mat_name = val
|
||||
break
|
||||
```
|
||||
|
||||
`mat_map_lower` hat keine garantierte Sortierung nach Schlüssellänge. Wenn ein kurzer Key (`"ring"`) und ein langer Key (`"ring_inner_seal"`) beide die Präfix-Bedingung erfüllen, gewinnt der erste in dict-Reihenfolge — nicht zwangsläufig der spezifischste Match.
|
||||
|
||||
**Empfehlung**: Keys nach Länge absteigend sortieren: `sorted(mat_map_lower.items(), key=lambda x: len(x[0]), reverse=True)` — längster Match gewinnt = spezifischster Match.
|
||||
**Empfehlung**: Change to `// Reset isolateMode when no part is pinned`.
|
||||
|
||||
---
|
||||
|
||||
## Positiv aufgefallen
|
||||
|
||||
- **Task 1 korrekt und robust**: `while prev != base_name` Loop für nested Suffixe terminiert sicher und deckt `_AF0_AF1`-Fälle ab.
|
||||
- **`_re.IGNORECASE` korrekt gesetzt** in `export_gltf.py` — deckt `_AF0` wie auch `_af0`.
|
||||
- **`_normalize_part_token_name` Reihenfolge stimmt**: `_af\d+` wird VOR Hyphen→Underscore-Konvertierung gestrippt, Regex funktioniert zuverlässig.
|
||||
- **Hash-Suffix-Stripping `\d{4,}`**: Mindestlänge 4 verhindert False-Positives bei legitimen kurzen Nummern in Teilenamen.
|
||||
- **`print()` in Blender-Script korrekt**: Blender-Scripts laufen als Subprocess, stdout wird vom Caller geloggt — `logging` wäre hier falsch.
|
||||
- **Kein DB-Schema geändert**: Keine Migration nötig, korrekt erkannt und ausgelassen.
|
||||
- **Tuple-Erweiterung auf 4 Elemente**: `excel_entries` korrekt auf `(tokens, raw, material, excel_norm)` erweitert, keine alten Stellen übersehen.
|
||||
### Bug fix: MaterialPanel invisible in ThreeDViewer — root cause correctly identified
|
||||
The diagnosis was precise: the outer `<div onClick={() => setPinnedPart(null)}>` was receiving the
|
||||
native DOM bubble from every canvas click, calling `setPinnedPart(null)` in the same React batch as
|
||||
`setPinnedPart(name)` from the THREE.js event handler — final state always `null`.
|
||||
|
||||
The two-part fix is clean and idiomatic:
|
||||
- `onClick={(e) => e.stopPropagation()}` on the viewport div absorbs DOM clicks
|
||||
- `onPointerMissed={() => setPinnedPart(null)}` on the R3F Canvas handles the "click empty space"
|
||||
case via the THREE.js raycaster (fires only when no mesh is hit) — this is exactly the right
|
||||
R3F API for this use case
|
||||
|
||||
### cadUtils.ts — normalization regex extension
|
||||
`/_AF\d+(_ASM)?$/i` is minimal and correct. It handles:
|
||||
- `_AF0`, `_AF1` (existing, unchanged)
|
||||
- `_AF0_ASM`, `_AF1_ASM` (new — assembly-node suffix)
|
||||
- Case-insensitive flag is defensive and correct
|
||||
- The loop-until-stable pattern handles nested suffixes as before
|
||||
- `_ASM` alone (without `_AF\d+`) is NOT stripped — correct, it's part of base names like
|
||||
`GE360-HF_000_P_ASM_ASM`
|
||||
|
||||
### Combined visibility useEffect — correct design
|
||||
Merging `hideAssigned` + `isolateMode` into a single traversal effect avoids
|
||||
ordering ambiguity between two independent effects competing on the same `mesh.visible` and
|
||||
`mat.opacity`. The priority order (hideAssigned first, then isolateMode) is explicit and logical.
|
||||
The pinned part (`isSelected`) is always protected from hiding regardless of mode. ✓
|
||||
|
||||
### Effect separation is clean
|
||||
- Color-apply effect: only touches `mat.color` → deps `[modelReady, partMaterials]`
|
||||
- Unassigned glow effect: only touches `mat.emissive` → deps `[modelReady, showUnassigned, partMaterials]`
|
||||
- Combined visibility effect: only touches `mesh.visible` / `mat.opacity` / `mat.transparent` → deps `[modelReady, pinnedPart, isolateMode, hideAssigned, partMaterials]`
|
||||
|
||||
No effect touches another effect's properties — no race conditions.
|
||||
|
||||
### GPU hint and DPR cap
|
||||
`gl={{ powerPreference: 'high-performance' }}` + `dpr={[1, 1.5]}` on both Canvas elements.
|
||||
`preserveDrawingBuffer: true` correctly kept only in ThreeDViewer (required for screenshot capture).
|
||||
|
||||
### "Hide assigned" toolbar button correctly conditional
|
||||
`{assignedCount > 0 && (...)}` in InlineCadViewer and
|
||||
`{modelReady && Object.keys(partMaterials).length > 0 && (...)}` in ThreeDViewer — button only
|
||||
appears when there is something to hide.
|
||||
|
||||
### Debug log is dev-only
|
||||
`if (!import.meta.env.DEV || ...)` guard ensures the console output and traversal overhead
|
||||
never reach production. The output logs both matched and unmatched keys, which is exactly what's
|
||||
needed to diagnose remaining name mismatches after the normalization fix.
|
||||
|
||||
### Feature parity achieved
|
||||
ThreeDViewer and InlineCadViewer now have matching material-assignment features:
|
||||
- ✓ `showUnassigned` highlight toggle with count badge
|
||||
- ✓ `hideAssigned` toggle (new, both viewers)
|
||||
- ✓ `isolateMode` (ghost / hide) via MaterialPanel (both viewers)
|
||||
- ✓ `onPointerMissed` closes panel on empty-space click in ThreeDViewer
|
||||
|
||||
---
|
||||
|
||||
## Empfehlung
|
||||
|
||||
Zwei geringe Probleme (Mindestlänge-Guard + Sortierung nach Key-Länge). Beide sind je eine Zeile Fix und verhindern Fehlmatches bei atypischen Eingaben. Können direkt inline gepatcht werden, kein erneutes Review nötig.
|
||||
**Freigabe.** The one Gering comment issue can be fixed inline.
|
||||
|
||||
Review abgeschlossen. Ergebnis: ✅
|
||||
|
||||
Reference in New Issue
Block a user