app
Adom TSCI - Tscircuit Board Viewer
Public Made by Adomby adom
Interactive tscircuit preview in a Hydrogen webview — first-class 3D / PCB / Schematic tabs, Components & Nets HUDs, an auto-glow x-ray view, Inspect, Measure, a Walkthrough Demo, and a one-click `exp
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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
/**
* 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)`)