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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
/**
* Rewrite a GLB's embedded images as `data:` URIs, and drop the bufferViews
* they vacated.
*
* Why not external files: Babylon's glTF loader still routes a relative image
* URI through XHR + createObjectURL in a null-origin (sandboxed) document, and
* the wiki's CSP is `img-src data: https:` with no `blob:`. A data: URI is
* used directly by the loader, so it is the only embedded-texture form the
* sandbox accepts.
*
* Usage: node glb-datauri-textures.mjs <in.glb> <out.glb>
*/
import fs from "fs"
const [inPath, outPath] = process.argv.slice(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 drop = new Set()
let n = 0
for (const [i, img] of (json.images || []).entries()) {
if (img.bufferView === undefined) continue
const bv = json.bufferViews[img.bufferView]
const bytes = bin.slice(bv.byteOffset || 0, (bv.byteOffset || 0) + bv.byteLength)
img.uri = `data:${img.mimeType || "image/png"};base64,${bytes.toString("base64")}`
drop.add(img.bufferView)
delete img.bufferView
delete img.mimeType
console.log(` image ${i}: ${(bytes.length / 1e6).toFixed(2)} MB inlined as data: URI`)
n++
}
if (!n) { console.log(" no embedded images"); process.exit(0) }
// Rebuild BIN without the vacated views, remapping every surviving index.
const keep = json.bufferViews.map((_, i) => i).filter(i => !drop.has(i))
const remap = new Map(keep.map((old, idx) => [old, idx]))
const parts = []
let off = 0
const newViews = keep.map(i => {
const bv = json.bufferViews[i]
const bytes = bin.slice(bv.byteOffset || 0, (bv.byteOffset || 0) + bv.byteLength)
const pad = (4 - (off % 4)) % 4
if (pad) { parts.push(Buffer.alloc(pad)); off += pad }
const v = { ...bv, byteOffset: off }
parts.push(bytes); off += bytes.length
return v
})
json.bufferViews = newViews
for (const a of json.accessors || [])
if (a.bufferView !== undefined) a.bufferView = remap.get(a.bufferView)
for (const m of json.meshes || [])
for (const pr of m.primitives || [])
if (pr.extensions?.KHR_draco_mesh_compression?.bufferView !== undefined)
pr.extensions.KHR_draco_mesh_compression.bufferView =
remap.get(pr.extensions.KHR_draco_mesh_compression.bufferView)
const newBin = Buffer.concat(parts)
json.buffers = [{ byteLength: newBin.length }]
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([newBin, Buffer.alloc((4 - (newBin.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(` ${(buf.length / 1e6).toFixed(2)} MB -> ${(fs.statSync(outPath).size / 1e6).toFixed(2)} MB`)