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`,
	);
}