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:
@@ -0,0 +1,230 @@
|
||||
import { useState } from 'react'
|
||||
import { X, Cpu, ChevronDown, ChevronUp, Zap } from 'lucide-react'
|
||||
import type { RenderLog } from '../../api/orders'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
title: string
|
||||
renderLog: RenderLog | null | undefined
|
||||
renderStartedAt?: string | null
|
||||
renderCompletedAt?: string | null
|
||||
}
|
||||
|
||||
function formatBytes(n?: number | null): string {
|
||||
if (n == null) return '—'
|
||||
if (n < 1024) return `${n} B`
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||||
return `${(n / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatDuration(s?: number | null): string {
|
||||
if (s == null) return '—'
|
||||
if (s < 60) return `${s.toFixed(1)}s`
|
||||
const m = Math.floor(s / 60)
|
||||
const rem = (s % 60).toFixed(0)
|
||||
return `${m}m ${rem}s`
|
||||
}
|
||||
|
||||
function SectionHeader({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide mb-2">
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex justify-between gap-4 text-sm py-0.5">
|
||||
<span className="text-content-muted shrink-0">{label}</span>
|
||||
<span className="text-content font-medium text-right">{value ?? '—'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BoolPill({ value, trueLabel = 'Yes', falseLabel = 'No' }: { value: boolean | undefined; trueLabel?: string; falseLabel?: string }) {
|
||||
if (value == null) return <span className="text-content-muted">—</span>
|
||||
return (
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${value ? 'bg-status-success-bg text-status-success-text' : 'bg-surface-muted text-content-muted'}`}>
|
||||
{value ? trueLabel : falseLabel}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RenderInfoModal({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
renderLog,
|
||||
renderStartedAt,
|
||||
renderCompletedAt,
|
||||
}: Props) {
|
||||
const [logExpanded, setLogExpanded] = useState(false)
|
||||
|
||||
if (!open) return null
|
||||
|
||||
const rl = renderLog
|
||||
|
||||
const isAnimation = rl?.type === 'turntable'
|
||||
const hasTemplate = !!rl?.template
|
||||
const hasTimestamps = !!(renderStartedAt || renderCompletedAt)
|
||||
const hasLog = (rl?.log_lines?.length ?? 0) > 0
|
||||
const hasError = !!rl?.error
|
||||
|
||||
const engineLabel = rl?.engine_used || rl?.engine || '—'
|
||||
const device = rl?.device_used
|
||||
const isGpu = device?.toLowerCase().includes('gpu')
|
||||
const isCpu = device?.toLowerCase().includes('cpu')
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="relative w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-xl shadow-2xl"
|
||||
style={{ backgroundColor: 'var(--color-bg-surface)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border-light sticky top-0"
|
||||
style={{ backgroundColor: 'var(--color-bg-surface)' }}>
|
||||
<h2 className="font-semibold text-content">{title} — Render Info</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-content-muted hover:text-content transition-colors p-1 rounded"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-5 space-y-5">
|
||||
{/* Error */}
|
||||
{hasError && (
|
||||
<div className="rounded-md p-3 text-sm" style={{ backgroundColor: 'var(--color-status-error-bg)', color: 'var(--color-status-error-text)' }}>
|
||||
<p className="font-semibold mb-1">Render Error</p>
|
||||
<pre className="whitespace-pre-wrap text-xs font-mono break-all">{rl!.error}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Render Settings */}
|
||||
{rl && (
|
||||
<div className="rounded-md p-4" style={{ backgroundColor: 'var(--color-bg-surface-alt)' }}>
|
||||
<SectionHeader>Render Settings</SectionHeader>
|
||||
<div className="space-y-0.5">
|
||||
{rl.renderer && <Row label="Renderer" value={rl.renderer} />}
|
||||
<Row label="Engine" value={engineLabel} />
|
||||
{device && (
|
||||
<Row
|
||||
label="Device"
|
||||
value={
|
||||
<span className={`inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
isGpu
|
||||
? 'bg-status-success-bg text-status-success-text'
|
||||
: isCpu
|
||||
? 'bg-status-warning-bg text-status-warning-text'
|
||||
: 'bg-surface-muted text-content-muted'
|
||||
}`}>
|
||||
{isGpu ? <Zap size={10} /> : <Cpu size={10} />}
|
||||
{device}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{rl.samples != null && <Row label="Samples" value={rl.samples} />}
|
||||
{rl.compute_type && <Row label="Compute Type" value={rl.compute_type} />}
|
||||
{rl.gpu_fallback != null && (
|
||||
<Row label="GPU Fallback" value={<BoolPill value={rl.gpu_fallback} trueLabel="Yes (CPU used)" falseLabel="No" />} />
|
||||
)}
|
||||
{rl.format && <Row label="Format" value={rl.format.toUpperCase()} />}
|
||||
{rl.parts_count != null && <Row label="Parts" value={rl.parts_count} />}
|
||||
{rl.stl_quality && <Row label="STL Quality" value={rl.stl_quality} />}
|
||||
{rl.smooth_angle != null && <Row label="Smooth Angle" value={`${rl.smooth_angle}°`} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timing */}
|
||||
{rl && (rl.total_duration_s != null || rl.stl_duration_s != null || rl.render_duration_s != null) && (
|
||||
<div className="rounded-md p-4" style={{ backgroundColor: 'var(--color-bg-surface-alt)' }}>
|
||||
<SectionHeader>Timing</SectionHeader>
|
||||
<div className="space-y-0.5">
|
||||
<Row label="Total" value={formatDuration(rl.total_duration_s)} />
|
||||
{rl.stl_duration_s != null && <Row label="STL Conversion" value={formatDuration(rl.stl_duration_s)} />}
|
||||
{rl.render_duration_s != null && <Row label="Render" value={formatDuration(rl.render_duration_s)} />}
|
||||
{isAnimation && rl.ffmpeg_duration_s != null && <Row label="FFmpeg" value={formatDuration(rl.ffmpeg_duration_s)} />}
|
||||
{isAnimation && rl.frame_count != null && <Row label="Frames" value={rl.frame_count} />}
|
||||
{isAnimation && rl.fps != null && <Row label="FPS" value={rl.fps} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Files */}
|
||||
{rl && (rl.output_size_bytes != null || rl.stl_size_bytes != null) && (
|
||||
<div className="rounded-md p-4" style={{ backgroundColor: 'var(--color-bg-surface-alt)' }}>
|
||||
<SectionHeader>Files</SectionHeader>
|
||||
<div className="space-y-0.5">
|
||||
{rl.output_size_bytes != null && <Row label="Output File" value={formatBytes(rl.output_size_bytes)} />}
|
||||
{rl.stl_size_bytes != null && <Row label="STL Cache" value={formatBytes(rl.stl_size_bytes)} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template */}
|
||||
{hasTemplate && rl && (
|
||||
<div className="rounded-md p-4" style={{ backgroundColor: 'var(--color-bg-surface-alt)' }}>
|
||||
<SectionHeader>Template</SectionHeader>
|
||||
<div className="space-y-0.5">
|
||||
<Row label="Path" value={<span className="font-mono text-xs break-all">{rl.template}</span>} />
|
||||
{rl.lighting_only != null && <Row label="Lighting Only" value={<BoolPill value={rl.lighting_only} />} />}
|
||||
{rl.shadow_catcher != null && <Row label="Shadow Catcher" value={<BoolPill value={rl.shadow_catcher} />} />}
|
||||
{rl.material_replace != null && <Row label="Material Replace" value={<BoolPill value={rl.material_replace} />} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
{hasTimestamps && (
|
||||
<div className="rounded-md p-4" style={{ backgroundColor: 'var(--color-bg-surface-alt)' }}>
|
||||
<SectionHeader>Timestamps</SectionHeader>
|
||||
<div className="space-y-0.5">
|
||||
{renderStartedAt && <Row label="Started" value={new Date(renderStartedAt).toLocaleString()} />}
|
||||
{renderCompletedAt && <Row label="Completed" value={new Date(renderCompletedAt).toLocaleString()} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Blender Log */}
|
||||
{hasLog && rl && (
|
||||
<div className="rounded-md p-4" style={{ backgroundColor: 'var(--color-bg-surface-alt)' }}>
|
||||
<button
|
||||
onClick={() => setLogExpanded((v) => !v)}
|
||||
className="flex items-center gap-2 w-full text-left"
|
||||
>
|
||||
<SectionHeader>Blender Log</SectionHeader>
|
||||
<span className="ml-auto text-content-muted">
|
||||
{logExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</span>
|
||||
</button>
|
||||
{logExpanded && (
|
||||
<pre className="mt-2 text-xs font-mono whitespace-pre-wrap break-all max-h-64 overflow-y-auto text-content-secondary leading-relaxed">
|
||||
{rl.log_lines!.join('\n')}
|
||||
</pre>
|
||||
)}
|
||||
{!logExpanded && (
|
||||
<p className="text-xs text-content-muted mt-1">{rl.log_lines!.length} lines — click to expand</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!rl && (
|
||||
<p className="text-sm text-content-muted text-center py-4">No render metadata available.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user