ducki
Version:
Simple CLI for working with KiCad projects.
141 lines (115 loc) • 3.74 kB
text/typescript
// Single-shot ChatGPT; absurd this works out of the box. AI will eat us all.
// Prompt:
//
// I am making a script for kicad that uses the cli that will
// render a fly around the board. I want to give an initial
// rotation of the board, then from the board's perspective
// rotate it about its y-axis. Should I use quaternions and
// can you provide code that would create the series of x, y,
// z rotation values I need?
//
// Note: I had to further specify JavaScript (it generated Python)
// and a handful of types were added to make it TypeScript.
// ==============================
// Quaternion utilities
// Stored as [w, x, y, z]
// ==============================
type Vector3 = [ number, number, number ];
type Vector4 = [ number, number, number, number ];
export type XYZ = { x: number, y: number, z: number };
function quatMul(a: Vector4, b: Vector4): Vector4 {
const [aw, ax, ay, az] = a;
const [bw, bx, by, bz] = b;
return [
aw * bw - ax * bx - ay * by - az * bz,
aw * bx + ax * bw + ay * bz - az * by,
aw * by - ax * bz + ay * bw + az * bx,
aw * bz + ax * by - ay * bx + az * bw,
];
}
function quatNormalize(q: Vector4): Vector4 {
const [w, x, y, z] = q;
const n = Math.hypot(w, x, y, z);
return [w / n, x / n, y / n, z / n];
}
function quatFromAxisAngle(axis: Vector3, angleRad: number): Vector4 {
const [ax, ay, az] = axis;
const half = angleRad * 0.5;
const s = Math.sin(half);
return quatNormalize([
Math.cos(half),
ax * s,
ay * s,
az * s,
]);
}
// Intrinsic XYZ (local X, then Y, then Z)
function quatFromEulerXYZ(x: number, y: number, z: number) {
const qx = quatFromAxisAngle([1, 0, 0], x);
const qy = quatFromAxisAngle([0, 1, 0], y);
const qz = quatFromAxisAngle([0, 0, 1], z);
return quatNormalize(quatMul(quatMul(qx, qy), qz));
}
function quatToEulerXYZ(q: Vector4): Vector3 {
const [w, x, y, z] = q;
// Rotation matrix elements
const r00 = 1 - 2 * (y * y + z * z);
const r01 = 2 * (x * y - w * z);
const r02 = 2 * (x * z + w * y);
const r12 = 2 * (y * z - w * x);
const r22 = 1 - 2 * (x * x + y * y);
// Clamp for numerical safety
const clamped = Math.max(-1, Math.min(1, r02));
const yAngle = Math.asin(clamped);
const xAngle = Math.atan2(-r12, r22);
const zAngle = Math.atan2(-r01, r00);
return [xAngle, yAngle, zAngle];
}
// ==============================
// Main generator
// ==============================
export function generateSpinFrames(
rot: XYZ, // [x, y, z] degrees
totalSpinDeg: number, // degrees
frames: number, // number of frames
) {
const deg2rad = (d: number) => d * Math.PI / 180;
const rad2deg = (r: number) => r * 180 / Math.PI;
const [x0, y0, z0] = [ rot.x, rot.y, rot.z ].map(deg2rad);
const q0 = quatFromEulerXYZ(x0, y0, z0);
const result: Array<XYZ> = [];
for (let i = 0; i < frames; i++) {
const t = frames > 1 ? i / (frames - 1) : 0;
const spinRad = deg2rad(totalSpinDeg * t);
// Local Y rotation (post-multiply)
const qSpinY = quatFromAxisAngle([0, 1, 0], spinRad);
const q = quatNormalize(quatMul(q0, qSpinY));
const [xr, yr, zr] = quatToEulerXYZ(q);
result.push({
x: rad2deg(xr),
y: rad2deg(yr),
z: rad2deg(zr),
});
}
return result;
}
// ==============================
// Example usage
// ==============================
/*
const frames = generateLocalYSpinFrames({
initialEulerDeg: [20, 30, 0], // initial board orientation
totalSpinDeg: 360, // full local-Y spin
frames: 120,
});
// Print first few frames
for (let i = 0; i < 5; i++) {
const [x, y, z] = frames[i];
console.log(
i,
x.toFixed(2),
y.toFixed(2),
z.toFixed(2)
);
}
*/