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:
2026-03-11 14:40:36 +01:00
parent 202b06a026
commit ca62319688
70 changed files with 6551 additions and 1130 deletions
+63 -56
View File
@@ -13,6 +13,8 @@ import { listOutputTypes } from '../api/outputTypes'
import type { OutputType } from '../api/outputTypes'
import api from '../api/client'
import StepDropzone from '../components/upload/StepDropzone'
import StepIndicator from '../components/shared/StepIndicator'
import HelpTooltip from '../components/HelpTooltip'
function StatCard({ icon, value, label, description, color }: {
icon: React.ReactNode
@@ -238,6 +240,8 @@ export default function UploadPage() {
</p>
</div>
<StepIndicator step={step} total={4} labels={['Upload', 'Review', 'Configure', 'STEP Files']} />
{/* ── Step 1: Excel drop zone ─────────────────────────────────────── */}
{step === 1 && (
<div
@@ -379,9 +383,12 @@ export default function UploadPage() {
</th>
<th className="px-4 py-2 font-medium text-content-secondary">PIM-ID</th>
<th className="px-4 py-2 font-medium text-content-secondary">Series</th>
<th className="px-4 py-2 font-medium text-content-secondary"
title="Gew\u00e4hltes Produkt \u2014 the specific material/coating variant from the Excel"
>Gew. Produkt</th>
<th className="px-4 py-2 font-medium text-content-secondary">
<span className="flex items-center gap-1">
Gew. Produkt
<HelpTooltip help={{ title: 'Gew. Produkt', body: 'Gewähltes Produkt the specific product variant selected in the Excel file.' }} />
</span>
</th>
<th className="px-4 py-2 font-medium text-content-secondary">Category</th>
<th className="px-4 py-2 font-medium text-content-secondary">Status</th>
<th className="px-4 py-2 font-medium text-content-secondary text-center" title="Whether a STEP/CAD file is already linked to this product">STEP</th>
@@ -477,59 +484,6 @@ export default function UploadPage() {
</div>
)}
{/* ── Step 4: Upload STEP Files ────────────────────────────────────── */}
{step === 4 && createdOrder && (
<div className="space-y-4">
<div className="card p-4">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<FileBox size={18} className="text-content-secondary" />
<h2 className="font-semibold text-content">
Upload STEP Files — {createdOrder.order_number}
</h2>
</div>
</div>
<p className="text-sm text-content-secondary">
Drop one or more <strong>.stp / .step</strong> files below.
Each file is matched to an order item by filename stem (case-insensitive).
You can also skip this and upload STEP files later from the order detail page.
</p>
</div>
<div className="card p-6">
<StepDropzone
orderId={createdOrder.id}
onMatchComplete={() => qc.invalidateQueries({ queryKey: ['order', createdOrder.id] })}
/>
</div>
<div className="flex justify-end gap-3">
<button
className="btn-secondary"
onClick={() => navigate(`/orders/${createdOrder.id}`)}
>
Skip &mdash; Go to Order
</button>
<button
className="btn-primary"
onClick={() => navigate(`/orders/${createdOrder.id}`)}
>
<ArrowRight size={16} />
Done &mdash; Go to Order
</button>
</div>
</div>
)}
{/* ── Validation Dialog ────────────────────────────────────────────── */}
{showValidationDialog && (
<ValidationDialog
validation={validationData}
onClose={() => setShowValidationDialog(false)}
onSaveAlias={(alias, suggestion) => saveAlias.mutate({ alias, materialName: suggestion })}
/>
)}
{/* ── Step 3: Output Type Selection ───────────────────────────────── */}
{step === 3 && previewResult && (
<div className="space-y-4">
@@ -684,6 +638,59 @@ export default function UploadPage() {
</div>
</div>
)}
{/* ── Validation Dialog ────────────────────────────────────────────── */}
{showValidationDialog && (
<ValidationDialog
validation={validationData}
onClose={() => setShowValidationDialog(false)}
onSaveAlias={(alias, suggestion) => saveAlias.mutate({ alias, materialName: suggestion })}
/>
)}
{/* ── Step 4: Upload STEP Files ────────────────────────────────────── */}
{step === 4 && createdOrder && (
<div className="space-y-4">
<div className="card p-4">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<FileBox size={18} className="text-content-secondary" />
<h2 className="font-semibold text-content">
Upload STEP Files — {createdOrder.order_number}
</h2>
</div>
</div>
<p className="text-sm text-content-secondary">
Drop one or more <strong>.stp / .step</strong> files below.
Each file is matched to an order item by filename stem (case-insensitive).
You can also skip this and upload STEP files later from the order detail page.
</p>
</div>
<div className="card p-6">
<StepDropzone
orderId={createdOrder.id}
onMatchComplete={() => qc.invalidateQueries({ queryKey: ['order', createdOrder.id] })}
/>
</div>
<div className="flex justify-end gap-3">
<button
className="btn-secondary"
onClick={() => navigate(`/orders/${createdOrder.id}`)}
>
Skip &mdash; Go to Order
</button>
<button
className="btn-primary"
onClick={() => navigate(`/orders/${createdOrder.id}`)}
>
<ArrowRight size={16} />
Done &mdash; Go to Order
</button>
</div>
</div>
)}
</div>
)
}