import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { projectXyzacVisual, XYZAC_MACHINE_GROUPS } from "./xyzac-visual-adapter.ts"; import * as THREE from "three"; import { buildXyzacMachine, buildXyzbcMachine } from "./xyzac-machine-model.ts"; const source = await readFile(new URL("./XyzacMachineView.tsx", import.meta.url), "utf8") + await readFile(new URL("./xyzac-machine-model.ts", import.meta.url), "utf8"); const adapter = await readFile(new URL("./xyzac-visual-adapter.ts", import.meta.url), "utf8"); const snapshot = { schema: "axis-panel-snapshot.v1", simulation_tick: 42, state_hash: "fnv1a64:xyzac-authoritative", kinematics: { schema: "wasm-xyzac-kinematics.v1", status: "verified", active_type: 1, native_type: "xyzac-trt-kins", coordinates: "XYZAC", joint_count: 5, linear_units: "mm", angular_units: "deg", pose: { x: 12, y: -5, z: 20, a: 30, b: 0, c: 45, u: 0, v: 0, w: 0 }, joints: [12, -5, 20, 30, 45], offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 25 }, generation: 2, switch_count: 2, state_hash: "fnv1a64:kinematics", wasm_state_hash: "fnv1a64:wasm-kinematics", }, visual: { active_segment_id: 7, active_progress: 0.5, pose: { x: 12, y: -5, z: 20, a: 30, c: 45 }, joints: [12, -5, 20, 30, 45], toolpath: [{ id: 7, active: true, start: { x: 0, y: 0, z: 0 }, end: { x: 12, y: -5, z: 20 }, motion_type: 1 }], machine_model: { schema: "linuxcnc-vismach-model.v1", id: "xyzac-trt", source: "linuxcnc-wasm-worker", source_path: "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py", source_hash: "fnv1a64:model", coordinate_system: "XYZAC", joints: ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c"], components: ["base", "column"] } }, }; const visual = projectXyzacVisual(snapshot); assert.equal(visual.schema, "xyzac-visual-state.v1"); assert.equal(visual.tick, 42); assert.equal(visual.stateHash, snapshot.state_hash); assert.deepEqual(visual.joints, [12, -5, 20, 30, 45]); assert.deepEqual(visual.pose, snapshot.kinematics.pose); assert.deepEqual(visual.offsets, snapshot.kinematics.offsets); assert.equal(visual.activeSegmentId, 7); assert.equal(visual.activeProgress, 0.5); assert.equal(visual.source, "linuxcnc-wasm-worker"); assert.equal(visual.machineModel?.id, "xyzac-trt"); assert.equal(visual.machineModel?.source_hash, "fnv1a64:model"); assert.deepEqual(XYZAC_MACHINE_GROUPS, ["xyzac-machine", "table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c", "tool-offset", "xyzac-toolpath", "xyzac-live-toolpath"]); for (const token of ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c", "tool-offset", "tableX.position.x = -tableXJoint * MM", "saddle.position.z = -saddleYJoint * MM", "zslide.position.y = (200 - spindleZJoint) * MM", "tilt.rotation.x = -tiltAJoint * DEG", "rotate.rotation.y = -rotateCJoint * DEG", "tool.position.y = -state.offsets.tool_offset * MM", "assembly.joints.rotate.add(pathGroup)", "assembly.joints.rotate.add(liveGroup)", "dataset.xyzacToolpathFrame", "dataset.xyzacToolpathAttached", "data-xyzac-tick", "data-xyzac-state-hash", "data-xyzac-active-segment", "data-xyzac-joints", "data-xyzac-offsets"]) assert.ok(source.includes(token), token); for (const forbidden of [/accept\s*\(/, /postMessage\s*\(/, /_sim_/, /_malloc/, /navigator\.storage/, /HAL_SET/, /KINEMATICS_SELECT/]) assert.equal(forbidden.test(source), false, String(forbidden)); assert.ok(adapter.includes("source: \"linuxcnc-wasm-worker\"")); assert.equal(source.includes("axis-preview-hud xyzac-machine-hud"), false, "machine preview HUD must stay empty"); const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-7, `${actual} != ${expected}`); const model = buildXyzacMachine(); const xyzbcModel = buildXyzbcMachine(); assert.equal(xyzbcModel.machine.name, "xyzbc-machine"); assert.ok(xyzbcModel.machine.getObjectByName("tilt-b")); assert.equal(xyzbcModel.machine.getObjectByName("tilt-a"), undefined); xyzbcModel.update({ joints: [0, 0, 0, 30, 45], offsets: { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 } }); close(xyzbcModel.joints.tilt.rotation.z, -30 * Math.PI / 180); assert.equal(xyzbcModel.joints.tilt.rotation.y, Math.PI / 2); xyzbcModel.dispose(); const original = structuredClone(visual); const zero = { joints: [0, 0, 0, 0, 0], offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } }; const position = (node) => node.getWorldPosition(new THREE.Vector3()); model.update(zero); close(position(model.toolTip).y, 2); close(position(model.workOrigin).y, 2); for (const length of [0, 15, 30, -5]) { model.update({ ...zero, offsets: { ...zero.offsets, tool_offset: length } }); close(position(model.toolTip).y, (200 - length) * .01); const cutter = model.machine.getObjectByName("four-flute-end-mill"); const cutterBounds = new THREE.Box3().setFromObject(cutter); close(cutterBounds.min.y, position(model.toolTip).y); close(cutterBounds.max.x - cutterBounds.min.x, .06); close(position(model.machine.getObjectByName("collet-nut")).y, 2.095); } model.update({ ...zero, joints: [12, -5, 20, 0, 0] }); close(position(model.workOrigin).x, -.12); close(position(model.workOrigin).y, 1.8); close(position(model.workOrigin).z, .05); model.update({ ...zero, joints: [0, 0, 0, 90, 45] }); close(position(model.workOrigin).y, 1.9); close(position(model.workOrigin).z, .3); assert.equal(model.joints.rotate.parent.name, "c-rotary-base"); assert.equal(model.machine.getObjectByName("c-drive-housing").parent, model.joints.rotate.parent); model.update(visual); assert.deepEqual(visual, original, "rendering must not change the WASM snapshot"); const geometrySet = new Set(); model.machine.traverse((node) => { if (node.isMesh) geometrySet.add(node.geometry); }); for (let tick = 0; tick < 100; tick++) model.update(visual); const afterGeometrySet = new Set(); model.machine.traverse((node) => { if (node.isMesh) afterGeometrySet.add(node.geometry); }); assert.deepEqual(afterGeometrySet, geometrySet, "motion must reuse model geometry"); const disposed = new Set(); geometrySet.forEach((geometry) => geometry.addEventListener("dispose", () => disposed.add(geometry))); model.dispose(); assert.equal(disposed.size, geometrySet.size); console.log(JSON.stringify({ schema: "xyzac-threejs-rtcp-10.v1", status: "verified", tick: visual.tick, state_hash: visual.stateHash, groups: XYZAC_MACHINE_GROUPS, active_segment: visual.activeSegmentId, direction: { table_x: -1, saddle_y: -1, spindle_z: -1, tilt_a: "x+", rotate_c: "z+", tool_offset: "z-" }, clear_visual_nonmutating: true, source: visual.source }));