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/normalize-pcb-model.ts

import { mat3, mat4, quat, vec3 } from "gl-matrix";
import { z } from "zod";

/** Lossless assembly interchange: native (-PCB X,PCB Z,PCB Y) to product XYZ.
 * Use a proper rotation and the documented z+ normal in both browser and CLI.
 * Flatten scene transforms and give each mesh instance
 * its own record; all accessors, triangles, materials and binary bytes survive.
 */
export function pcbModelInAssemblyCoordinates(
	glb: ArrayBuffer,
	{ rotationDegrees }: { rotationDegrees: number },
): Uint8Array {
	const source = new Uint8Array(glb),
		view = new DataView(glb);
	if (view.getUint32(0, true) !== 0x46546c67 || view.getUint32(4, true) !== 2)
		throw new Error("Expected glTF 2 GLB");
	const jsonLength = view.getUint32(12, true);
	const triple = z.tuple([z.number(), z.number(), z.number()]);
	const document = z
		.object({
			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(),
					})
					.passthrough(),
			),
			meshes: z.array(z.record(z.unknown())),
			scenes: z.array(z.object({ nodes: z.array(z.number()) }).passthrough()),
		})
		.passthrough()
		.parse(
			JSON.parse(new TextDecoder().decode(source.slice(20, 20 + jsonLength))),
		);
	const nodes: typeof document.nodes = [],
		meshes: typeof document.meshes = [];
	const loaderFromExport = mat4.multiply(
		mat4.create(),
		mat4.fromZRotation(mat4.create(), (rotationDegrees * Math.PI) / 180),
		mat4.fromValues(-1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1),
	);
	function visit(index: number, parent: mat4) {
		const node = document.nodes[index];
		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 translation = vec3.create(),
				scale = vec3.create();
			mat4.getTranslation(translation, world);
			mat4.getScaling(scale, world);
			if (mat4.determinant(world) < 0) scale[0] *= -1;
			if (Array.from(scale).some((n) => Math.abs(n) < 1e-10))
				throw new Error("Degenerate model transform");
			const basis = mat3.fromMat4(mat3.create(), world);
			for (let axis = 0; axis < 3; axis++)
				for (let row = 0; row < 3; row++) basis[axis * 3 + row] /= scale[axis];
			const rotation = quat.normalize(
				quat.create(),
				quat.fromMat3(quat.create(), basis),
			);
			const rebuilt = mat4.fromRotationTranslationScale(
				mat4.create(),
				rotation,
				translation,
				scale,
			);
			if (Array.from(world).some((n, i) => Math.abs(n - rebuilt[i]) > 1e-5))
				throw new Error("Unsupported sheared model transform");
			const mesh = meshes.length;
			meshes.push({ ...document.meshes[node.mesh] });
			nodes.push({
				name: node.name,
				mesh,
				translation: triple.parse(Array.from(translation)),
				rotation: z
					.tuple([z.number(), z.number(), z.number(), z.number()])
					.parse(Array.from(rotation)),
				scale: triple.parse(Array.from(scale)),
			});
		}
		for (const child of node.children ?? []) visit(child, world);
	}
	const scenes = document.scenes.map((scene) => {
		const start = nodes.length;
		for (const root of scene.nodes) visit(root, loaderFromExport);
		return {
			...scene,
			nodes: Array.from({ length: nodes.length - start }, (_, i) => start + i),
		};
	});
	const encoded = new TextEncoder().encode(
		JSON.stringify({ ...document, nodes, meshes, scenes }),
	);
	const paddedLength = Math.ceil(encoded.length / 4) * 4;
	const tail = source.slice(20 + jsonLength),
		output = new Uint8Array(20 + paddedLength + tail.length),
		header = new DataView(output.buffer);
	header.setUint32(0, 0x46546c67, true);
	header.setUint32(4, 2, true);
	header.setUint32(8, output.length, true);
	header.setUint32(12, paddedLength, true);
	header.setUint32(16, 0x4e4f534a, true);
	output.fill(32, 20, 20 + paddedLength);
	output.set(encoded, 20);
	output.set(tail, 20 + paddedLength);
	if (
		!output
			.slice(20 + paddedLength)
			.every((byte, index) => byte === tail[index])
	)
		throw new Error("Binary geometry changed");
	return output;
}