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/lib/qualification-source-placements.ts
import ts from "typescript";
export interface AuthoredPlacement {
ref: string;
x_mm: number;
y_mm: number;
ccw_rotation_degrees: number;
rotation_explicit: boolean;
line: number;
}
function numericLiteral(expression: ts.Expression): number {
if (ts.isNumericLiteral(expression)) return Number(expression.text);
if (
ts.isPrefixUnaryExpression(expression) &&
expression.operator === ts.SyntaxKind.MinusToken &&
ts.isNumericLiteral(expression.operand)
)
return -Number(expression.operand.text);
throw new Error(`Audit requires a literal, found ${expression.getText()}`);
}
function lengthMillimetres(attribute: ts.JsxAttribute): number {
const initializer = attribute.initializer;
if (initializer && ts.isStringLiteral(initializer)) {
if (!/^-?\d+(\.\d+)?mm$/.test(initializer.text))
throw new Error(
`Audit requires explicit mm units: ${attribute.getText()}`,
);
return Number(initializer.text.slice(0, -2));
}
if (initializer && ts.isJsxExpression(initializer) && initializer.expression)
return numericLiteral(initializer.expression);
throw new Error(`Unsupported length: ${attribute.getText()}`);
}
export function readAuthoredPlacements(
sourceText: string,
): AuthoredPlacement[] {
const sourceFile = ts.createSourceFile(
"remote-circuit.tsx",
sourceText,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
);
const placements: AuthoredPlacement[] = [];
visitPlacements(sourceFile, { sourceFile, placements });
if (placements.length !== 37)
throw new Error(
`Expected 37 authored fitted placements, got ${placements.length}`,
);
return placements;
}
function visitPlacements(
node: ts.Node,
context: { sourceFile: ts.SourceFile; placements: AuthoredPlacement[] },
) {
if (ts.isJsxSelfClosingElement(node)) {
const attributes = node.attributes.properties.filter(ts.isJsxAttribute);
const name = attributes.find(
(attribute) => attribute.name.getText() === "name",
);
if (
name?.initializer &&
ts.isStringLiteral(name.initializer) &&
/^(J|U|R|C|Q|LED|SW)\d+$/.test(name.initializer.text)
) {
const x = attributes.find(
(attribute) => attribute.name.getText() === "pcbX",
);
const y = attributes.find(
(attribute) => attribute.name.getText() === "pcbY",
);
const rotation = attributes.find(
(attribute) => attribute.name.getText() === "pcbRotation",
);
if (!x || !y)
throw new Error(`Missing authored XY: ${name.initializer.text}`);
let ccwRotationDegrees = 0; // Documented pcbRotation default, not an inferred orientation.
if (rotation) {
if (
!rotation.initializer ||
!ts.isJsxExpression(rotation.initializer) ||
!rotation.initializer.expression
)
throw new Error(`Unsupported rotation: ${rotation.getText()}`);
ccwRotationDegrees = numericLiteral(rotation.initializer.expression);
}
context.placements.push({
ref: name.initializer.text,
x_mm: lengthMillimetres(x),
y_mm: lengthMillimetres(y),
ccw_rotation_degrees: ccwRotationDegrees,
rotation_explicit: Boolean(rotation),
line:
context.sourceFile.getLineAndCharacterOfPosition(node.getStart())
.line + 1,
});
}
}
ts.forEachChild(node, (child) => visitPlacements(child, context));
}