master
John Lauer export-wiki guide: rewrite for the current publish path (README.html, not an auto-rendered asset), add the three tscirc… ed21612 2mo ago
/**
 * Bake a Y-up -> Z-up rotation into a GLB by re-parenting every scene root
 * under one wrapper node.
 *
 * Why: glTF is Y-up by spec, and that is what tscircuit emits — the board's
 * thickness axis is Y (Box0 spans about +/-0.7 in Y). adom-tsci's own viewer
 * compensates at runtime (`zUp: true` + applyGlbZUpTransform), so the copy it
 * loads must stay Y-up. But the Adom wiki's native 3D Model viewer is Z-up and
 * consumes the raw file, so an unrotated board is displayed standing on its
 * edge like a wall.
 *
 * Rotating +90 degrees about X maps (x, y, z) -> (x, -z, y): the Y thickness
 * becomes Z, and the board lies flat in XY. Apply this to the copy the wiki
 * renders (board.glb), never to viewer/3d.glb.
 *
 * An extra spin about Z is often wanted too: the wiki's viewer looks from -Y,
 * so a board whose USB-C edge is at +X ends up facing away. Pass the degrees as
 * a third argument; it is composed as z_spin * x_90 (x first, then z).
 *
 * Usage: node glb-make-zup.mjs <in.glb> <out.glb> [zDegrees]
 */
import fs from "fs"

const [inPath, outPath, zDegRaw] = process.argv.slice(2)
const zDeg = Number(zDegRaw || 0)

/** Hamilton product, q1 applied AFTER q2. Both are glTF [x, y, z, w]. */
const qmul = (q1, q2) => [
  q1[3] * q2[0] + q1[0] * q2[3] + q1[1] * q2[2] - q1[2] * q2[1],
  q1[3] * q2[1] - q1[0] * q2[2] + q1[1] * q2[3] + q1[2] * q2[0],
  q1[3] * q2[2] + q1[0] * q2[1] - q1[1] * q2[0] + q1[2] * q2[3],
  q1[3] * q2[3] - q1[0] * q2[0] - q1[1] * q2[1] - q1[2] * q2[2],
]
const buf = fs.readFileSync(inPath)
if (buf.readUInt32LE(0) !== 0x46546c67) throw new Error("not a GLB")

const jsonLen = buf.readUInt32LE(12)
const json = JSON.parse(buf.slice(20, 20 + jsonLen).toString("utf8"))
let p = 20 + jsonLen
let bin = Buffer.alloc(0)
while (p < buf.length) {
  const len = buf.readUInt32LE(p), type = buf.readUInt32LE(p + 4)
  if (type === 0x004e4942) bin = buf.slice(p + 8, p + 8 + len)
  p += 8 + len + ((4 - (len % 4)) % 4)
}

const scene = json.scenes[json.scene ?? 0]
const roots = scene.nodes.slice()
if (roots.length === 1 && json.nodes[roots[0]].name === "ZUpRoot") {
  console.log("  already Z-up; nothing to do"); process.exit(0)
}
const S = Math.SQRT1_2                                   // sin(45deg) == cos(45deg)
const qx90 = [S, 0, 0, S]                                // Y-up -> Z-up
const zr = (zDeg * Math.PI) / 180 / 2
const qz = [0, 0, Math.sin(zr), Math.cos(zr)]
const q = qmul(qz, qx90).map(v => +v.toFixed(12))
json.nodes.push({ name: "ZUpRoot", rotation: q, children: roots })
scene.nodes = [json.nodes.length - 1]
console.log(`  wrapped ${roots.length} root node(s) under ZUpRoot (+90deg about X, then ${zDeg}deg about Z)`)

const jsonBuf = Buffer.from(JSON.stringify(json), "utf8")
const jsonChunk = Buffer.concat([jsonBuf, Buffer.alloc((4 - (jsonBuf.length % 4)) % 4, 0x20)])
const binChunk = Buffer.concat([bin, Buffer.alloc((4 - (bin.length % 4)) % 4, 0)])
const head = Buffer.alloc(12)
head.writeUInt32LE(0x46546c67, 0); head.writeUInt32LE(2, 4)
head.writeUInt32LE(12 + 8 + jsonChunk.length + 8 + binChunk.length, 8)
const jh = Buffer.alloc(8); jh.writeUInt32LE(jsonChunk.length, 0); jh.writeUInt32LE(0x4e4f534a, 4)
const bh = Buffer.alloc(8); bh.writeUInt32LE(binChunk.length, 0); bh.writeUInt32LE(0x004e4942, 4)
fs.writeFileSync(outPath, Buffer.concat([head, jh, jsonChunk, bh, binChunk]))
console.log(`  wrote ${outPath} (${(fs.statSync(outPath).size / 1e6).toFixed(2)} MB)`)