Skip to content
RPRichard PruittTech & Cyber
Navigation
repo://poppyup/viewer/geometry.js

viewer/geometry.js

7.1 KB · 129 lines · javascript · Download from the repository page

viewer/geometry.js
// 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]; }}