krishnax12/watchy-eink-smartwatch

Unfinished prototype (not production-ready): 4 native DRC errors, 2 display-flex intersections, and missing antenna radiator copper remain

Version
1.2.7
License
MIT
Stars
1

scripts/audit-cad-placement.mjs

import fs from "node:fs/promises";
import path from "node:path";
import { convertCircuitJsonTo3D } from "circuit-json-to-gltf";

// Match circuit-json-to-gltf's transformMesh order/signs, not Three's default Euler.
function transformPoint(point, box) {
	const rotation = box.rotation ?? { x: 0, y: 0, z: 0 };
	const x1 = point.x * Math.cos(rotation.y) - point.z * Math.sin(rotation.y);
	const z1 = point.x * Math.sin(rotation.y) + point.z * Math.cos(rotation.y);
	const y2 = point.y * Math.cos(rotation.x) + z1 * Math.sin(rotation.x);
	const z2 = -point.y * Math.sin(rotation.x) + z1 * Math.cos(rotation.x);
	return [
		x1 * Math.cos(rotation.z) + y2 * Math.sin(rotation.z) + box.center.x,
		z2 + box.center.z,
		-x1 * Math.sin(rotation.z) + y2 * Math.cos(rotation.z) + box.center.y,
	];
}

const circuit = JSON.parse(
	await fs.readFile(
		process.env.WATCHY_CIRCUIT_PATH ?? "dist/index/circuit.json",
		"utf8",
	),
);
// Limit the audit to supplied STEP models, not generated passive approximations.
const input = circuit.filter(
	(element) => element.type !== "cad_component" || element.model_step_url,
);
const assetDirectory = path.resolve(process.env.WATCHY_ASSET_DIRECTORY ?? ".");
const board = circuit.find((element) => element.type === "pcb_board");
if (!board) throw new Error("Missing PCB for assembly screening");
const boardHalfThickness = board.thickness / 2;
{
	const scene = await convertCircuitJsonTo3D(input, {
		projectBaseUrl: `file://${assetDirectory}/`,
		renderBoardTextures: false,
	});
	const models = scene.boxes
		.filter((box) => box.meshUrl)
		.map((box) => {
			if (!box.mesh) throw new Error(`Missing model for ${box.label}`);
			const min = [Infinity, Infinity, Infinity];
			const max = [-Infinity, -Infinity, -Infinity];
			for (const triangle of box.mesh.triangles) {
				for (const point of triangle.vertices) {
					// Scene y is PCB z. Restore PCB coordinates for readable reports.
					const coordinates = transformPoint(point, box);
					coordinates.forEach((coordinate, axis) => {
						min[axis] = Math.min(min[axis], coordinate);
						max[axis] = Math.max(max[axis], coordinate);
					});
				}
			}
			return {
				name: box.label,
				file: box.meshUrl,
				min,
				max,
				intersectsBoardSlab:
					min[2] < boardHalfThickness - 0.01 &&
					max[2] > -boardHalfThickness + 0.01,
			};
		});
	const potentialIntersections = [];
	for (let i = 0; i < models.length; i++) {
		for (let j = i + 1; j < models.length; j++) {
			const a = models[i];
			const b = models[j];
			const overlap = [0, 1, 2].map(
				(axis) =>
					Math.min(a.max[axis], b.max[axis]) -
					Math.max(a.min[axis], b.min[axis]),
			);
			if (overlap.every((value) => value > 0.02))
				potentialIntersections.push({ a: a.name, b: b.name, overlap });
		}
	}
	const report = {
		scope:
			"Supplied STEP models, installed circuit-json-to-gltf transforms. AABB screening, not exact solid collision certification. Board slab flags can include intentional connector tabs/flex. Generated passive models excluded.",
		models,
		potentialIntersections,
	};
	await fs.writeFile(
		"reports/cad-placement-audit.json",
		JSON.stringify(report, null, 2),
	);
	console.log(JSON.stringify(report, null, 2));
}