main
John Lauer Add provenance, manifests, verification log, licenses and README fbc6889 8h ago
// Turn step2glb's single-mesh GLB of the KiCad SM04B-GHS-TB model into named, batched parts with physical materials.
// step2glb (service-step2glb) writes one mesh with two base-colour materials: mat_0 = housing, mat_1 = contacts and
// solder tabs (checked by bounds: mat_1 spans the four contacts at x = +-0.625, +-1.875 and the tabs at x = +-3.725).
// Output nodes: "housing" (PA9T, natural ivory: metallic 0, roughness 0.6) and "contacts" (tin plated phosphor bronze
// contacts and brass solder tabs: metallic 1, roughness 0.35). Geometry and the Z-up frame are unchanged.
// usage: node glb_parts.mjs in.glb out.glb [--simplify ratio:error] [--quantize]   (run with glTF Transform 4.1)
import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import { dedup, join, prune, weld, simplify, quantize } from '@gltf-transform/functions';
import { MeshoptSimplifier } from 'meshoptimizer';
const [, , src, out, ...opts] = process.argv;
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const doc = await io.read(src);
const root = doc.getRoot();
const scene = root.getDefaultScene() || root.listScenes()[0];
const SPEC = {
  mat_0: { part: 'housing', name: 'Adom.Housing (PA9T, natural ivory)', color: [0.80, 0.78, 0.66, 1], metal: 0, rough: 0.6 },
  mat_1: { part: 'contacts', name: 'Adom.Contacts (tin plated)', color: [0.72, 0.72, 0.70, 1], metal: 1, rough: 0.35 },
};
const srcNodes = root.listNodes().filter(n => n.getMesh());
if (srcNodes.length !== 1) throw new Error('expected one mesh node from step2glb, got ' + srcNodes.length);
const srcNode = srcNodes[0];
const meshes = {};
const srcPrims = srcNode.getMesh().listPrimitives().map(p => [p, p.getMaterial()?.getName()]);
for (const [p, matName] of srcPrims) {
  const s = SPEC[matName];
  if (!s) throw new Error('unexpected material ' + matName);
  if (!meshes[s.part]) {
    const m = p.getMaterial();
    m.setName(s.name).setBaseColorFactor(s.color).setMetallicFactor(s.metal).setRoughnessFactor(s.rough);
    meshes[s.part] = doc.createMesh(s.part);
  }
  meshes[s.part].addPrimitive(p);
}
const parent = doc.createNode('SM04B-GHS-TB(LF)(SN)').setTranslation(srcNode.getTranslation()).setRotation(srcNode.getRotation()).setScale(srcNode.getScale());
for (const [part, mesh] of Object.entries(meshes)) parent.addChild(doc.createNode(part).setMesh(mesh));
scene.addChild(parent);
srcNode.dispose();
const steps = [dedup(), join({ keepNamed: true }), prune()];
const si = opts.indexOf('--simplify');
if (si >= 0) {
  const [ratio, error] = opts[si + 1].split(':').map(Number);
  await MeshoptSimplifier.ready;
  steps.push(weld(), simplify({ simplifier: MeshoptSimplifier, ratio, error, lockBorder: false }), prune());
}
if (opts.includes('--quantize')) steps.push(quantize());
await doc.transform(...steps);
await io.write(out, doc);
let prims = 0, tris = 0;
for (const m of root.listMeshes()) for (const p of m.listPrimitives()) { prims++; const i = p.getIndices(); tris += (i ? i.getCount() : p.getAttribute('POSITION').getCount()) / 3; }
console.log(JSON.stringify({ out, prims, tris, nodes: root.listNodes().map(n => n.getName()) }));