AnasSarkiz/magnetic-shutter-remote-r8
A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.
- Version
- 0.4.0-prototype
- License
- unset
- Stars
- 0
scripts/export-product-pcb.ts
import { indexPcbModel } from "./index-pcb-model";
import { partitionPcbModels } from "./partition-pcb-models";
import { dimensions } from "../product/dimensions";
import { pcbModelInAssemblyCoordinates } from "./normalize-pcb-model";
import { pathToFileURL } from "node:url";
import { resolve } from "node:path";
import { any_circuit_element } from "circuit-json";
import { convertCircuitJsonToGltf } from "circuit-json-to-gltf";
// Presentation fragments from one qualified circuit; never rewrite circuit.json.
// Keep native-upload assets under2.4MB while preserving one board and all fitted supplier models.
const circuit = any_circuit_element
.array()
.parse(await Bun.file("dist/index/circuit.json").json());
if (circuit.some((element) => element.type.endsWith("_error")))
throw new Error("Cannot refresh product models from a failed circuit build");
if (!circuit.some((element) => element.type === "pcb_trace"))
throw new Error("Cannot refresh product models from an unrouted board");
const models = circuit.filter((element) => element.type === "cad_component");
const fitted = circuit.filter((element) => element.type === "source_component");
if (
models.length !== fitted.length ||
new Set(models.map((model) => model.source_component_id)).size !==
fitted.length
)
throw new Error("Every fitted reference must have exactly one CAD model");
const directObjModel = models.find((model) =>
circuit.some(
(element) =>
element.type === "source_component" &&
element.name === "L3" &&
element.source_component_id === model.source_component_id,
),
);
if (
!directObjModel?.model_obj_url ||
!directObjModel.model_origin_position ||
!directObjModel.rotation
)
throw new Error("L3 requires its exact native supplier OBJ and origin");
if (directObjModel.rotation.x !== 0 || directObjModel.rotation.y !== 0)
throw new Error("Native OBJ assembly currently requires planar L3 rotation");
const objPath = directObjModel.model_obj_url.replace(/^\.\//, "");
const objBytes = await Bun.file(objPath).arrayBuffer();
if (objBytes.byteLength > 2_400_000)
throw new Error("Native supplier OBJ exceeds upload bound");
await Bun.write(
"product/native-obj-model.json",
JSON.stringify(
{
reference: "L3",
cadComponentId: directObjModel.cad_component_id,
path: objPath,
sha256: new Bun.CryptoHasher("sha256").update(objBytes).digest("hex"),
bytes: objBytes.byteLength,
position: directObjModel.position,
rotation: directObjModel.rotation,
modelOriginPosition: directObjModel.model_origin_position,
modelUnitToMmScale: directObjModel.model_unit_to_mm_scale_factor ?? 1,
},
null,
2,
),
);
const fragmentCount = 8;
const maxModelsPerFragment = 9;
const maxFragmentBytes = 2_400_000;
const exportOptions = {
format: "glb",
boardTextureResolution: 512,
projectBaseUrl: pathToFileURL(`${resolve(".")}/`).href,
} as const;
const boardGlb = await convertCircuitJsonToGltf(
circuit.filter((element) => element.type !== "cad_component"),
exportOptions,
);
if (!(boardGlb instanceof ArrayBuffer))
throw new Error("Expected native board GLB");
const measuredModels = [];
for (const model of models) {
const componentGlb = await convertCircuitJsonToGltf(
circuit.filter((element) =>
element.type === "cad_component"
? element.cad_component_id === model.cad_component_id
: element.type === "source_component" ||
element.type === "pcb_component",
),
exportOptions,
);
if (!(componentGlb instanceof ArrayBuffer))
throw new Error("Expected native component GLB");
if (model.cad_component_id === directObjModel.cad_component_id) {
await Bun.write(
".codex/runtime/pcb-model-proofs/L3.measurement.glb",
pcbModelInAssemblyCoordinates(componentGlb, {
rotationDegrees: dimensions.remote.ccwRotationDegrees,
}),
);
continue;
}
measuredModels.push({
cadComponentId: model.cad_component_id,
bytes: (await indexPcbModel(componentGlb)).byteLength,
});
}
await Bun.write(
"product/pcb-fragment-probes.json",
JSON.stringify(
{
sourceCircuitSha256: new Bun.CryptoHasher("sha256")
.update(await Bun.file("dist/index/circuit.json").arrayBuffer())
.digest("hex"),
boardBytes: boardGlb.byteLength,
measuredModels,
scope:
"Native individual mesh measurements for packaging, not accepted assembly assets",
},
null,
2,
),
);
const partitions = partitionPcbModels({
models: measuredModels,
boardBytes: (await indexPcbModel(boardGlb)).byteLength,
fragmentCount,
maxModelsPerFragment,
maxFragmentBytes,
});
await Bun.write(
"product/pcb-fragment-partition.json",
JSON.stringify(
{
sourceCircuitSha256: new Bun.CryptoHasher("sha256")
.update(await Bun.file("dist/index/circuit.json").arrayBuffer())
.digest("hex"),
boardBytes: (await indexPcbModel(boardGlb)).byteLength,
measuredModels,
partitions,
maxFragmentBytes,
maxModelsPerFragment,
},
null,
2,
),
);
for (const [index, filename] of [
"r8-pcb.glb",
"r8-parts-2.glb",
"r8-parts-3.glb",
"r8-parts-4.glb",
"r8-parts-5.glb",
"r8-parts-6.glb",
"r8-parts-7.glb",
"r8-parts-8.glb",
].entries()) {
const selected = new Set(partitions[index]);
const fragment = circuit.filter((element) =>
element.type === "cad_component"
? selected.has(element.cad_component_id)
: index === 0 ||
element.type === "source_component" ||
element.type === "pcb_component",
);
const glb = await convertCircuitJsonToGltf(fragment, {
format: "glb",
boardTextureResolution: 512,
projectBaseUrl: pathToFileURL(`${resolve(".")}/`).href,
});
if (!(glb instanceof ArrayBuffer))
throw new Error("Expected native binary GLB");
const indexedGlb = await indexPcbModel(glb);
await Bun.write(
`.codex/runtime/pcb-model-proofs/${filename}.native.glb`,
glb,
);
await Bun.write(
`.codex/runtime/pcb-model-proofs/${filename}.indexed.glb`,
indexedGlb,
);
const normalized = pcbModelInAssemblyCoordinates(indexedGlb, {
rotationDegrees: dimensions.remote.ccwRotationDegrees,
});
if (normalized.length > maxFragmentBytes)
throw new Error(
`Native asset exceeds upload bound: ${filename}, ${normalized.length} bytes`,
);
await Bun.write(`product/models/${filename}`, normalized);
console.log(
`Native ${filename}: ${normalized.length} bytes,${selected.size} real fitted models`,
);
}