viewer/geometry.js
7.1 KB · 129 lines · javascript · Download from the repository page
// Builds renderer objects from the component spec. Keeps metadata out of rendering logic:// each component becomes a THREE.Group (userData.componentId) holding one Mesh per geometry spec.import * as THREE from 'three'; // Document frame → renderer frame (mm, device centred at origin, Y up, front toward +Z).export const X0 = 95, Y0 = 48, Z0 = 19;export const toScene = (x, y, z) => new THREE.Vector3(x - X0, Y0 - y, Z0 - z); const MAT = {};function mat(key, params) { if (!MAT[key]) MAT[key] = new THREE.MeshStandardMaterial({ side: THREE.DoubleSide, ...params }); return MAT[key];}// Restrained palette keyed by material family; chosen to separate families at a glance.export function materialFor(comp) { const m = (comp.material || '').toLowerCase(), id = comp.id, kind = comp.kind; if (kind === 'reference') return mat('ref', { color: 0x8fa3b8, transparent: true, opacity: 0.28, roughness: 0.9, depthWrite: false }); if (id.startsWith('cell-')) return mat('cell', { color: 0x6faa7b, roughness: 0.55, metalness: 0.15 }); if (id === 'oled-lens') return mat('lens', { color: 0x0b0f14, transparent: true, opacity: 0.85, roughness: 0.15, metalness: 0.1 }); if (id === 'oled-module') return mat('oled', { color: 0x11161c, roughness: 0.5 }); if (id.startsWith('wifi-window') || id === 'ble-window') return mat('win', { color: 0x7fa7d8, transparent: true, opacity: 0.55, roughness: 0.6 }); if (id === 'led-light-guide') return mat('led', { color: 0xffb347, emissive: 0x552200, roughness: 0.4 }); if (id.startsWith('wifi-ant')) return mat('ant', { color: 0x9dc1ea, roughness: 0.7 }); if (id.startsWith('pad-')) return mat('pad', { color: 0xe58bb0, roughness: 0.95 }); if (id.includes('gasket')) return mat('gasket', { color: 0xc8443a, roughness: 0.95 }); if (id.startsWith('rib-') || id.startsWith('boss-') || m.includes('az91d')) return mat('mg', { color: 0xb9bdc3, roughness: 0.6, metalness: 0.35 }); if (m.includes('6061')) return mat('al', { color: 0x3b3e44, roughness: 0.42, metalness: 0.65 }); if (m.includes('pom')) return mat('pom', { color: 0xe9e6dd, roughness: 0.8 }); if (m.includes('stainless') || m.includes('zn-ni') || m.includes('insert')) return mat('steel', { color: 0x9ea4aa, roughness: 0.35, metalness: 0.85 }); if (m.includes('ndfeb')) return mat('mag', { color: 0x74797f, roughness: 0.4, metalness: 0.7 }); if (m.includes('tpu')) return mat('tpu', { color: 0x1a1b1e, roughness: 0.95 }); if (m.includes('pc') && !m.includes('pcb')) return mat('pc', { color: 0x4b515a, roughness: 0.78 }); if (id === 'som') return mat('som', { color: 0x2f5fa8, roughness: 0.6 }); if (m.includes('fr-4') || m.includes('megtron') || m.includes('flex') || id.endsWith('-board')) return mat('pcb', { color: 0x226b3c, roughness: 0.7 }); if (id.startsWith('m2-')) return mat('m2', { color: 0x2f6f4f, roughness: 0.65 }); if (id.startsWith('usb-c') || id === 'rj45' || id.startsWith('conn-') || id === 'mezzanine' || id === 'm2-nvme-socket') return mat('conn', { color: 0x8d949b, roughness: 0.5, metalness: 0.5 }); if (id.startsWith('card-')) return mat('card', { color: 0x37474f, roughness: 0.6 }); return mat('ic', { color: 0x2a2d31, roughness: 0.55, metalness: 0.2 });} function roundedRectPath(x0, y0, x1, y1, r) { r = Math.min(r, (x1 - x0) / 2, (y1 - y0) / 2); const p = new THREE.Path(); p.moveTo(x0 + r, y0); p.lineTo(x1 - r, y0); p.absarc(x1 - r, y0 + r, r, -Math.PI / 2, 0, false); p.lineTo(x1, y1 - r); p.absarc(x1 - r, y1 - r, r, 0, Math.PI / 2, false); p.lineTo(x0 + r, y1); p.absarc(x0 + r, y1 - r, r, Math.PI / 2, Math.PI, false); p.lineTo(x0, y0 + r); p.absarc(x0 + r, y0 + r, r, Math.PI, Math.PI * 1.5, false); return p;}function shapeFromPath(path) { const s = new THREE.Shape(); s.curves = path.curves; s.autoClose = true; return s; } // Align a geometry's bounding-box minimum with the target renderer-frame box minimum.function alignToDocBox(geom, spec) { geom.computeBoundingBox(); const bb = geom.boundingBox; const target = new THREE.Vector3(spec.x[0] - X0, Y0 - spec.y[1], Z0 - spec.z[1]); geom.translate(target.x - bb.min.x, target.y - bb.min.y, target.z - bb.min.z); return geom;} export function geometryFromSpec(spec) { switch (spec.type) { case 'box': { const g = new THREE.BoxGeometry(spec.x[1] - spec.x[0], spec.y[1] - spec.y[0], spec.z[1] - spec.z[0]); const c = toScene((spec.x[0] + spec.x[1]) / 2, (spec.y[0] + spec.y[1]) / 2, (spec.z[0] + spec.z[1]) / 2); g.translate(c.x, c.y, c.z); return g; } case 'cyl': { const g = new THREE.CylinderGeometry(spec.r, spec.r, spec.len, 28); if (spec.axis === 'x') g.rotateZ(Math.PI / 2); else if (spec.axis === 'z') g.rotateX(Math.PI / 2); const c = toScene(spec.c[0], spec.c[1], spec.c[2]); g.translate(c.x, c.y, c.z); return g; } case 'plate': case 'ring': { // 2D outline in (u,v): u = doc X; v = -doc Y for normal 'z', v = doc Z for normal 'y'. const vOf = spec.normal === 'z' ? (y) => -y : (z) => z; const yr = spec.normal === 'z' ? spec.y : spec.z; const outer = roundedRectPath(spec.x[0], Math.min(vOf(yr[0]), vOf(yr[1])), spec.x[1], Math.max(vOf(yr[0]), vOf(yr[1])), spec.r); const shape = shapeFromPath(outer); if (spec.type === 'ring') { const t = spec.t; shape.holes.push(roundedRectPath(spec.x[0] + t, Math.min(vOf(yr[0]), vOf(yr[1])) + t, spec.x[1] - t, Math.max(vOf(yr[0]), vOf(yr[1])) - t, Math.max(0.2, spec.r - t))); } else { for (const h of spec.holes || []) { const hv = [vOf(h.y[0]), vOf(h.y[1])]; shape.holes.push(roundedRectPath(h.x[0], Math.min(...hv), h.x[1], Math.max(...hv), h.r || 0.5)); } } const depth = spec.normal === 'z' ? spec.z[1] - spec.z[0] : spec.y[1] - spec.y[0]; const g = new THREE.ExtrudeGeometry(shape, { depth, bevelEnabled: false, curveSegments: 8 }); if (spec.normal === 'y') g.rotateX(-Math.PI / 2); return alignToDocBox(g, spec); } default: throw new Error('unknown geometry type ' + spec.type); }} /** Build the whole scene graph. Returns { root, nodes: Map<id, Group>, meshes: Mesh[] }. */export function buildScene(components) { const root = new THREE.Group(); root.name = 'device'; const nodes = new Map(), meshes = []; for (const c of components) { const g = new THREE.Group(); g.name = c.id; g.userData.componentId = c.id; nodes.set(c.id, g); } for (const c of components) { const g = nodes.get(c.id); (c.parent ? nodes.get(c.parent) : root).add(g); const material = materialFor(c); for (const spec of c.geometry || []) { const mesh = new THREE.Mesh(geometryFromSpec(spec), material); mesh.userData.componentId = c.id; mesh.name = c.id; g.add(mesh); meshes.push(mesh); } // Immutable assembled transform (identity, since specs are authored in world/doc coordinates). g.userData.assembledPosition = g.position.clone(); } return { root, nodes, meshes };} export function disposeScene(root) { root.traverse((o) => { if (o.isMesh) { o.geometry.dispose(); } }); for (const k of Object.keys(MAT)) { MAT[k].dispose(); delete MAT[k]; }}