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/review-product-model.ts
import { dimensions } from "../product/dimensions";
import { mat4, vec3 } from "gl-matrix";
import { any_circuit_element } from "circuit-json";
import { z } from "zod";
const triple = z.tuple([z.number(), z.number(), z.number()]);
const documentSchema = z.object({
scene: z.number().optional(),
scenes: z.array(z.object({ nodes: z.array(z.number()) })),
nodes: z.array(
z.object({
name: z.string().optional(),
mesh: z.number().optional(),
children: z.array(z.number()).optional(),
translation: triple.optional(),
rotation: z
.tuple([z.number(), z.number(), z.number(), z.number()])
.optional(),
scale: triple.optional(),
matrix: z.array(z.number()).length(16).optional(),
}),
),
meshes: z.array(
z.object({
primitives: z.array(
z.object({
attributes: z.object({ POSITION: z.number() }),
indices: z.number().optional(),
}),
),
}),
),
accessors: z.array(
z.object({
count: z.number(),
min: triple.optional(),
max: triple.optional(),
}),
),
});
const nativeAssets: {
filename: string;
sha256: string;
sizeMm: { x: number; y: number; z: number };
}[] = [];
const meshRows: {
name: string;
vertices: number;
triangles: number;
boundsMm: number[][];
anchorMm: number[];
}[] = [];
const overallMin = [Infinity, Infinity, Infinity],
overallMax = [-Infinity, -Infinity, -Infinity];
function visit(
nodeIndex: number,
context: { parent: mat4; doc: z.infer<typeof documentSchema> },
) {
const { parent, doc } = context;
const node = doc.nodes[nodeIndex];
const local = node.matrix
? mat4.clone(node.matrix)
: mat4.fromRotationTranslationScale(
mat4.create(),
node.rotation ?? [0, 0, 0, 1],
node.translation ?? [0, 0, 0],
node.scale ?? [1, 1, 1],
);
const world = mat4.multiply(mat4.create(), parent, local);
if (node.mesh !== undefined) {
const min = [Infinity, Infinity, Infinity],
max = [-Infinity, -Infinity, -Infinity];
let vertices = 0;
let triangles = 0;
for (const primitive of doc.meshes[node.mesh].primitives) {
const accessor = doc.accessors[primitive.attributes.POSITION];
if (!accessor.min || !accessor.max || accessor.count <= 0)
throw new Error(`Empty model ${node.name}`);
if (
accessor.min.every(
(coordinate, index) => coordinate === accessor.max?.[index],
)
)
throw new Error(`Zero geometry: ${node.name}`);
for (const x of [accessor.min[0], accessor.max[0]])
for (const y of [accessor.min[1], accessor.max[1]])
for (const z of [accessor.min[2], accessor.max[2]]) {
const point = vec3.transformMat4(vec3.create(), [x, y, z], world);
for (let axis = 0; axis < 3; axis++) {
min[axis] = Math.min(min[axis], point[axis]);
max[axis] = Math.max(max[axis], point[axis]);
}
}
vertices += accessor.count;
triangles +=
(primitive.indices === undefined
? accessor.count
: doc.accessors[primitive.indices].count) / 3;
}
for (let axis = 0; axis < 3; axis++) {
overallMin[axis] = Math.min(overallMin[axis], min[axis]);
overallMax[axis] = Math.max(overallMax[axis], max[axis]);
}
meshRows.push({
name: node.name ?? `Node${nodeIndex}`,
vertices,
triangles,
boundsMm: [min, max],
anchorMm: Array.from(vec3.transformMat4(vec3.create(), [0, 0, 0], world)),
});
}
for (const child of node.children ?? []) visit(child, { parent: world, doc });
}
for (const 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",
"L3.measurement.glb",
]) {
const bytes = await Bun.file(
filename === "L3.measurement.glb"
? ".codex/runtime/pcb-model-proofs/L3.measurement.glb"
: `product/models/${filename}`,
).arrayBuffer(),
view = new DataView(bytes);
const doc = documentSchema.parse(
JSON.parse(
new TextDecoder().decode(
new Uint8Array(bytes, 20, view.getUint32(12, true)),
),
),
);
const start = meshRows.length;
for (const root of doc.scenes[doc.scene ?? 0].nodes)
visit(root, {
parent: mat4.fromZRotation(
mat4.create(),
(-dimensions.remote.ccwRotationDegrees * Math.PI) / 180,
),
doc,
});
const fragment = meshRows.slice(start);
const min = [0, 1, 2].map((axis) =>
Math.min(...fragment.map((row) => row.boundsMm[0][axis])),
);
const max = [0, 1, 2].map((axis) =>
Math.max(...fragment.map((row) => row.boundsMm[1][axis])),
);
if (filename !== "L3.measurement.glb")
nativeAssets.push({
filename,
sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"),
sizeMm: { x: max[1] - min[1], y: max[0] - min[0], z: max[2] - min[2] },
});
}
const source = any_circuit_element
.array()
.parse(await Bun.file("dist/index/circuit.json").json());
const fitted = source.filter((row) => row.type === "source_component");
for (const component of fitted) {
const cad = source.find(
(row) =>
row.type === "cad_component" &&
row.source_component_id === component.source_component_id,
);
const model = meshRows.find((row) => row.name === component.name);
if (cad?.type !== "cad_component" || !model)
throw new Error(`Missing fitted model: ${component.name}`);
if (
vec3.distance(model.anchorMm, [
cad.position.x,
cad.position.y,
cad.position.z,
]) > 0.00001
)
throw new Error(`Wrong model pose: ${component.name}`);
}
if (meshRows.length !== fitted.length + 1)
throw new Error("Expected one board and every fitted supplier model");
const size = overallMax.map(
(coordinate, axis) => coordinate - overallMin[axis],
);
const report = {
sourceCircuitSha256: new Bun.CryptoHasher("sha256")
.update(await Bun.file("dist/index/circuit.json").arrayBuffer())
.digest("hex"),
nativeAssets,
assetRotationDegrees: dimensions.remote.ccwRotationDegrees,
modelCoordinateFrame: "product XYZ, board normal z+, millimeters",
fittedModels: fitted.length,
emptyModels: 0,
allFittedCadAnchorsMatch: true,
boundsMm: [overallMin, overallMax],
sizeMm: { x: size[0], y: size[1], z: size[2] },
meshes: meshRows,
};
await Bun.write(
"product/pcb-model-review.json",
JSON.stringify(report, null, 2),
);
console.log(JSON.stringify({ ...report, meshes: undefined }, null, 2));