clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

main
marbles / tools / measure_ports.ts
measure_ports.ts 6.6K
1/**2 * Measure helix / hole / cross pieces from the packed marble-kit.glb.3 * For each piece: bbox, and per bbox-face channel-opening clusters4 * (lateral center/extent + y range) so port poses and floor heights can be5 * read off. Also dumps low-y vertex bands to see support/foot geometry.6 */7import { NodeIO } from "@gltf-transform/core";8import { KHRONOS_EXTENSIONS } from "@gltf-transform/extensions";910const GLB = "/Users/dami/Developer/marbles/assets/marble-kit.glb";11const PIECES = Deno.args.length ? Deno.args : [12 "helix-quarter-left",13 "helix-quarter-right",14 "helix-half-left",15 "helix-half-right",16 "helix-left",17 "helix-right",18 "helix-large-quarter-left",19 "helix-large-quarter-right",20 "helix-large-half-left",21 "helix-large-half-right",22 "helix-large-left",23 "helix-large-right",24 "straight-hole",25 "end-hole-rounded",26 "end-hole-square",27 "cross",28];2930const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);31const doc = await io.read(GLB);32const root = doc.getRoot();3334type V3 = [number, number, number];3536function mat4mul(m: number[], v: V3): V3 {37 return [38 m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12],39 m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13],40 m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14],41 ];42}4344/** Collect world-space (piece-local: relative to the named root node) verts. */45function collectVerts(pieceName: string): V3[] {46 const node = root.listNodes().find((n) => n.getName() === pieceName);47 if (!node) return [];48 const verts: V3[] = [];49 const rootInv = invert(node.getWorldMatrix() as unknown as number[]);50 const walk = (n: typeof node) => {51 const mesh = n.getMesh();52 if (mesh) {53 const world = n.getWorldMatrix() as unknown as number[];54 for (const prim of mesh.listPrimitives()) {55 const pos = prim.getAttribute("POSITION");56 if (!pos) continue;57 const arr = pos.getArray()!;58 for (let i = 0; i < arr.length; i += 3) {59 const w = mat4mul(world, [arr[i], arr[i + 1], arr[i + 2]]);60 verts.push(mat4mul(rootInv, w));61 }62 }63 }64 for (const c of n.listChildren()) walk(c);65 };66 walk(node);67 return verts;68}6970function invert(m: number[]): number[] {71 // General 4x4 inverse (column-major).72 const inv = new Array(16).fill(0);73 inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] -74 m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] -75 m[13] * m[7] * m[10];76 inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] +77 m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] +78 m[12] * m[7] * m[10];79 inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] -80 m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] -81 m[12] * m[7] * m[9];82 inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] +83 m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] +84 m[12] * m[6] * m[9];85 inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] +86 m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] +87 m[13] * m[3] * m[10];88 inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] -89 m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] -90 m[12] * m[3] * m[10];91 inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] +92 m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] +93 m[12] * m[3] * m[9];94 inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] -95 m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] -96 m[12] * m[2] * m[9];97 inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] -98 m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] -99 m[13] * m[3] * m[6];100 inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] +101 m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] +102 m[12] * m[3] * m[6];103 inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] -104 m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] -105 m[12] * m[3] * m[5];106 inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] +107 m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] +108 m[12] * m[2] * m[5];109 inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] +110 m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] +111 m[9] * m[3] * m[6];112 inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] -113 m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] -114 m[8] * m[3] * m[6];115 inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] +116 m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] +117 m[8] * m[3] * m[5];118 inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] -119 m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] -120 m[8] * m[2] * m[5];121 const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];122 return inv.map((v) => v / det);123}124125function fmt(v: number): string {126 return (Math.abs(v) < 5e-4 ? 0 : v).toFixed(3);127}128129/** 1-D clustering with gap threshold. */130function clusters(values: { lat: number; y: number }[], gap = 0.15) {131 const sorted = values.slice().sort((a, b) => a.lat - b.lat);132 const out: { lat: number; y: number }[][] = [];133 let cur: { lat: number; y: number }[] = [];134 for (const v of sorted) {135 if (cur.length && v.lat - cur[cur.length - 1].lat > gap) {136 out.push(cur);137 cur = [];138 }139 cur.push(v);140 }141 if (cur.length) out.push(cur);142 return out;143}144145for (const name of PIECES) {146 const verts = collectVerts(name);147 if (verts.length === 0) {148 console.log(`\n=== ${name}: NOT FOUND`);149 continue;150 }151 const min: V3 = [Infinity, Infinity, Infinity];152 const max: V3 = [-Infinity, -Infinity, -Infinity];153 for (const v of verts) {154 for (let a = 0; a < 3; a++) {155 min[a] = Math.min(min[a], v[a]);156 max[a] = Math.max(max[a], v[a]);157 }158 }159 console.log(160 `\n=== ${name} verts=${verts.length} bbox x[${fmt(min[0])},${161 fmt(max[0])162 }] y[${fmt(min[1])},${fmt(max[1])}] z[${fmt(min[2])},${fmt(max[2])}]`,163 );164165 const faces: [string, number, number, number][] = [166 ["x-", 0, min[0], 2],167 ["x+", 0, max[0], 2],168 ["z-", 2, min[2], 0],169 ["z+", 2, max[2], 0],170 ];171 for (const [label, axis, plane, latAxis] of faces) {172 const hits = verts.filter((v) => Math.abs(v[axis] - plane) < 2e-3)173 .map((v) => ({ lat: v[latAxis], y: v[1] }));174 if (hits.length === 0) continue;175 for (const c of clusters(hits)) {176 const lats = c.map((h) => h.lat);177 const ys = c.map((h) => h.y);178 console.log(179 ` face ${label}@${fmt(plane)}: lat[${fmt(Math.min(...lats))},${180 fmt(Math.max(...lats))181 }] center=${fmt((Math.min(...lats) + Math.max(...lats)) / 2)} y[${182 fmt(Math.min(...ys))183 },${fmt(Math.max(...ys))}] n=${c.length}`,184 );185 }186 }187}