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>
This commit is contained in:
@@ -4,7 +4,7 @@ import { Canvas, useThree } from '@react-three/fiber'
|
||||
import { OrbitControls, useGLTF, Environment } from '@react-three/drei'
|
||||
import * as THREE from 'three'
|
||||
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
import { Loader2, Box, RefreshCw, Grid3X3, Layers, Sun, AlertCircle, EyeOff } from 'lucide-react'
|
||||
import { Loader2, Box, RefreshCw, Grid3X3, Layers, Sun, AlertCircle, EyeOff, Zap } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { listMediaAssets as getMediaAssets } from '../../api/media'
|
||||
import { generateGltfGeometry, getPartMaterials, type PartMaterialMap } from '../../api/cad'
|
||||
@@ -12,6 +12,7 @@ import { useAuthStore } from '../../store/auth'
|
||||
import MaterialPanel, { type IsolateMode } from './MaterialPanel'
|
||||
import { normalizeMeshName, resolvePartMaterial, remapToPartKeys, applyPBRToMaterial, previewColorForEntry, forEachMeshMaterial, type MeshRegistryEntry } from './cadUtils'
|
||||
import { fetchMaterialPBR, type MaterialPBRMap } from '../../api/assetLibraries'
|
||||
import { useGeometryMerge } from './useGeometryMerge'
|
||||
|
||||
type ViewMode = 'solid' | 'wireframe'
|
||||
type LightPreset = 'studio' | 'warehouse' | 'sunset' | 'park' | 'city'
|
||||
@@ -105,7 +106,10 @@ function GlbModelWithFit({
|
||||
if (obj.geometry) {
|
||||
let geo = obj.geometry.clone()
|
||||
if (!geo.index) geo = mergeVertices(geo)
|
||||
geo.computeVertexNormals()
|
||||
// Only compute normals if the geometry doesn't already have them.
|
||||
// GLBs from our pipeline include smooth normals — overwriting them
|
||||
// with computeVertexNormals() produces flat/faceted shading.
|
||||
if (!geo.attributes.normal) geo.computeVertexNormals()
|
||||
obj.geometry = geo
|
||||
}
|
||||
// Clone materials so emissive / color changes don't affect the shared GLTF cache
|
||||
@@ -189,6 +193,7 @@ export default function InlineCadViewer({
|
||||
const [showUnassigned, setShowUnassigned] = useState(false)
|
||||
const [hideAssigned, setHideAssigned] = useState(false)
|
||||
const [isolateMode, setIsolateMode] = useState<IsolateMode>('none')
|
||||
const [perfMode, setPerfMode] = useState(false)
|
||||
const [totalMeshCount, setTotalMeshCount] = useState(0)
|
||||
const [glbMeshNames, setGlbMeshNames] = useState<Set<string>>(new Set())
|
||||
const [partKeyMap, setPartKeyMap] = useState<Record<string, string>>({})
|
||||
@@ -381,6 +386,15 @@ export default function InlineCadViewer({
|
||||
onError: () => toast.error('Failed to queue GLB generation'),
|
||||
})
|
||||
|
||||
// Performance mode: merge geometries by material to reduce draw calls
|
||||
useGeometryMerge({
|
||||
meshRegistryRef,
|
||||
partMaterials,
|
||||
pbrMap,
|
||||
enabled: perfMode,
|
||||
sceneRef,
|
||||
})
|
||||
|
||||
// Hover highlight
|
||||
const handlePointerOver = useCallback((e: any) => {
|
||||
e.stopPropagation()
|
||||
@@ -469,6 +483,13 @@ export default function InlineCadViewer({
|
||||
</ToolbarBtn>
|
||||
))}
|
||||
|
||||
<div className="w-px h-4 bg-white/10 mx-0.5" />
|
||||
|
||||
{/* Performance mode */}
|
||||
<ToolbarBtn active={perfMode} onClick={() => setPerfMode(v => !v)} title="Performance mode — merges geometries, disables per-part hover">
|
||||
<Zap size={11} /> Perf
|
||||
</ToolbarBtn>
|
||||
|
||||
{/* Show unassigned + hide assigned toggles */}
|
||||
{modelReady && (
|
||||
<>
|
||||
@@ -542,9 +563,9 @@ export default function InlineCadViewer({
|
||||
setModelReady(true)
|
||||
setFitTrigger(t => t + 1)
|
||||
}}
|
||||
onPointerOver={handlePointerOver}
|
||||
onPointerOut={handlePointerOut}
|
||||
onClick={handleClick}
|
||||
onPointerOver={perfMode ? undefined : handlePointerOver}
|
||||
onPointerOut={perfMode ? undefined : handlePointerOut}
|
||||
onClick={perfMode ? undefined : handleClick}
|
||||
/>
|
||||
</Suspense>
|
||||
<OrbitControls ref={controlsRef} makeDefault />
|
||||
|
||||
Reference in New Issue
Block a user