Complete XYZAC LinuxCNC WASM parity and smooth 3D preview
This commit is contained in:
115
web/react/src/XyzacMachineView.tsx
Normal file
115
web/react/src/XyzacMachineView.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { projectXyzacVisual, type XyzacVisualSource, type XyzacVisualState } from "./xyzac-visual-adapter";
|
||||
|
||||
type Point = Record<string, number>;
|
||||
type Segment = { id?: number; active?: boolean; motion_type?: number; turn?: number; center?: Point; normal?: Point; samples?: Point[]; start?: Point; end?: Point };
|
||||
type Props = { visual: XyzacVisualState; toolpath?: Segment[]; view?: "z" | "z2" | "x" | "y" | "y2" | "p"; zoom?: number; rotateMode?: boolean; showPath?: boolean; showLive?: boolean; showLimits?: boolean; showVelocity?: boolean; showOffsets?: boolean; showExtents?: boolean };
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
const MM = 0.01;
|
||||
|
||||
function material(color: number) { return new THREE.MeshStandardMaterial({ color, roughness: .78, metalness: .12 }); }
|
||||
function sourcePosition(x: number, y: number, z: number) { return new THREE.Vector3(x * MM, z * MM, y * MM); }
|
||||
function sourceBox(width: number, depth: number, height: number, color: number) { return new THREE.Mesh(new THREE.BoxGeometry(width * MM, height * MM, depth * MM), material(color)); }
|
||||
function sourceCylinderZ(radius: number, height: number, color: number) { return new THREE.Mesh(new THREE.CylinderGeometry(radius * MM, radius * MM, height * MM, 32), material(color)); }
|
||||
function translate(name: string, object: THREE.Object3D, x: number, y: number, z: number) { object.name = name; object.position.copy(sourcePosition(x, y, z)); return object; }
|
||||
|
||||
export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotateMode = false, showPath = true, showLive = true, showLimits = false, showVelocity = false, showOffsets = false, showExtents = false }: Props) {
|
||||
const host = useRef<HTMLDivElement>(null);
|
||||
const visualRef = useRef(visual); visualRef.current = visual;
|
||||
const toolpathRef = useRef(toolpath); toolpathRef.current = toolpath;
|
||||
const viewRef = useRef(view); viewRef.current = view;
|
||||
const zoomRef = useRef(zoom); zoomRef.current = zoom;
|
||||
const rotateRef = useRef(rotateMode); rotateRef.current = rotateMode;
|
||||
const layersRef = useRef({ showPath, showLive, showLimits, showVelocity, showOffsets, showExtents });
|
||||
layersRef.current = { showPath, showLive, showLimits, showVelocity, showOffsets, showExtents };
|
||||
|
||||
useEffect(() => {
|
||||
const element = host.current;
|
||||
if (!element) return;
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x081017);
|
||||
scene.add(new THREE.HemisphereLight(0xe6f2ff, 0x202830, 1.65));
|
||||
const key = new THREE.DirectionalLight(0xffffff, 2.1); key.position.set(5, 8, 10); scene.add(key);
|
||||
const camera = new THREE.PerspectiveCamera(38, 1, .1, 100);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, preserveDrawingBuffer: true });
|
||||
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, 2));
|
||||
renderer.domElement.dataset.engine = "three.js r185 / xyzac-trt-vismach";
|
||||
element.appendChild(renderer.domElement);
|
||||
|
||||
const machine = new THREE.Group(); machine.name = "xyzac-machine"; scene.add(machine);
|
||||
machine.add(translate("base", sourceBox(240, 260, 150, 0x43616b), 0, 30, -175));
|
||||
machine.add(translate("column", sourceBox(100, 100, 510, 0x3b7a72), 0, 150, 5));
|
||||
const head = new THREE.Group(); head.name = "head"; head.position.copy(sourcePosition(0, 0, 200)); machine.add(head);
|
||||
head.add(translate("head-body", sourceBox(60, 60, 135, 0x277c83), 0, 0, 87.5));
|
||||
head.add(translate("motor", sourceCylinderZ(30, 65, 0x277c83), 0, 200, 165));
|
||||
const spindle = new THREE.Group(); spindle.name = "spindle"; spindle.position.copy(sourcePosition(0, 0, 20)); head.add(spindle);
|
||||
spindle.add(translate("spindle-housing", sourceCylinderZ(20, 135, 0x31888d), 0, 0, 67.5));
|
||||
spindle.add(translate("spindle-nose", sourceCylinderZ(10, 20, 0x4da0a0), 0, 0, 10));
|
||||
const tool = new THREE.Group(); tool.name = "tool-offset"; spindle.add(tool);
|
||||
tool.add(translate("tool-holder", sourceCylinderZ(6, 70, 0x9da7a8), 0, 0, 35));
|
||||
tool.add(translate("tool", sourceCylinderZ(3, 70, 0xd85f4a), 0, 0, 105));
|
||||
|
||||
const zslide = new THREE.Group(); zslide.name = "spindle-z"; machine.add(zslide);
|
||||
const saddle = new THREE.Group(); saddle.name = "saddle-y"; zslide.add(saddle);
|
||||
saddle.add(translate("saddle-body", sourceBox(150, 106, 32, 0x8b9697), 0, 0, -89));
|
||||
saddle.add(translate("z-slide", sourceBox(100, 220, 75, 0xb8bdbe), 0, 10, -142.5));
|
||||
const tableX = new THREE.Group(); tableX.name = "table-x"; saddle.add(tableX);
|
||||
tableX.add(translate("table-bed", sourceBox(300, 100, 17, 0x686f72), 0, 0, -60.5));
|
||||
tableX.add(translate("ways", sourceBox(300, 80, 6, 0x50575a), 0, 0, -72));
|
||||
const tilt = new THREE.Group(); tilt.name = "tilt-a"; tableX.add(tilt);
|
||||
const yOffset = 20; const zOffset = 10;
|
||||
tilt.position.copy(sourcePosition(0, yOffset, zOffset));
|
||||
tilt.rotation.x = 0;
|
||||
const rotate = new THREE.Group(); rotate.name = "rotate-c"; tilt.add(rotate);
|
||||
rotate.position.copy(sourcePosition(0, -yOffset, -zOffset));
|
||||
rotate.add(translate("trunnion", sourceBox(145, 80, 35, 0xb17540), 0, 0, -30));
|
||||
rotate.add(translate("table", sourceCylinderZ(50, 18, 0xb17540), 0, 0, -9));
|
||||
rotate.rotation.y = 0;
|
||||
const yoke = new THREE.Group(); yoke.name = "yoke"; tilt.add(yoke);
|
||||
yoke.add(translate("yoke-plate", sourceBox(130, 80, 10, 0xb17540), 0, 0, -30));
|
||||
const brackets = new THREE.Group(); brackets.name = "brackets"; tableX.add(brackets);
|
||||
brackets.position.copy(sourcePosition(0, yOffset, zOffset));
|
||||
brackets.add(translate("bracket-left", sourceBox(10, 80, 50, 0x9b6b3b), -72, 0, -25));
|
||||
brackets.add(translate("bracket-right", sourceBox(10, 80, 50, 0x9b6b3b), 72, 0, -25));
|
||||
const pathGroup = new THREE.Group(); pathGroup.name = "xyzac-toolpath"; scene.add(pathGroup);
|
||||
const liveGroup = new THREE.Group(); liveGroup.name = "xyzac-live-toolpath"; scene.add(liveGroup);
|
||||
const overlayGroup = new THREE.Group(); overlayGroup.name = "xyzac-overlays"; scene.add(overlayGroup);
|
||||
const resize = () => { const width = Math.max(element.clientWidth, 280); const height = Math.max(element.clientHeight, 260); camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height, false); };
|
||||
const observer = new ResizeObserver(resize); observer.observe(element); resize();
|
||||
|
||||
const toView = (point: Point) => new THREE.Vector3(Number(point.x ?? 0) * MM, Number(point.z ?? 0) * MM, Number(point.y ?? 0) * MM);
|
||||
let frame = 0;
|
||||
const render = () => {
|
||||
const state = visualRef.current; const [tableXJoint, saddleYJoint, spindleZJoint, tiltAJoint, rotateCJoint] = state.joints;
|
||||
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;
|
||||
pathGroup.clear(); liveGroup.clear();
|
||||
const completed = new THREE.LineBasicMaterial({ color: 0xdce7e8 }); const active = new THREE.LineBasicMaterial({ color: 0xff9a55 }); const rapid = new THREE.LineBasicMaterial({ color: 0x5c9d9a });
|
||||
for (const raw of (toolpathRef.current && toolpathRef.current.length > 0 ? toolpathRef.current : state.toolpath) as Segment[]) {
|
||||
const start = raw.start ?? {}; const end = raw.end ?? {};
|
||||
const points = Array.isArray(raw.samples) && raw.samples.length > 1 ? raw.samples.map(toView) : [toView(start), toView(end)];
|
||||
const group = raw.active || raw.id === state.activeSegmentId ? liveGroup : pathGroup; const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), raw.active || raw.id === state.activeSegmentId ? active : raw.motion_type === 0 ? rapid : completed); group.add(line);
|
||||
}
|
||||
const layer = layersRef.current;
|
||||
pathGroup.visible = layer.showPath; liveGroup.visible = layer.showLive;
|
||||
overlayGroup.clear();
|
||||
if (layer.showLimits) { const limits = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(4, 2, 3)), new THREE.LineBasicMaterial({ color: 0xdd7566 })); limits.position.set(0, .8, 0); overlayGroup.add(limits); }
|
||||
if (layer.showOffsets) { const offset = new THREE.AxesHelper(1.8); offset.name = "xyzac-offset-axes"; offset.position.set(state.offsets.x_rot_point * MM, state.offsets.z_offset * MM, state.offsets.y_offset * MM); overlayGroup.add(offset); }
|
||||
overlayGroup.visible = layer.showLimits || layer.showOffsets || layer.showExtents || layer.showVelocity;
|
||||
const distance = 9 / Math.max(.5, zoomRef.current); const selected = viewRef.current;
|
||||
if (selected === "z") camera.position.set(0, distance, .01); else if (selected === "z2") camera.position.set(distance * .3, distance * .9, distance * .3); else if (selected === "x") camera.position.set(distance, 2, .01); else if (selected === "y") camera.position.set(.01, 2, distance); else if (selected === "y2") camera.position.set(.01, 2, -distance); else camera.position.set(6 / Math.max(.5, zoomRef.current), 5 / Math.max(.5, zoomRef.current), 7 / Math.max(.5, zoomRef.current));
|
||||
camera.lookAt(0, 0, 0); scene.rotation.y = rotateRef.current ? (performance.now() / 5000) % (Math.PI * 2) : 0; renderer.render(scene, camera); frame = requestAnimationFrame(render);
|
||||
};
|
||||
render();
|
||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); renderer.dispose(); element.replaceChildren(); };
|
||||
}, []);
|
||||
|
||||
const pose = visual.pose;
|
||||
return <div className="three-viewer xyzac-machine-view" ref={host} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} data-xyzac-joints={JSON.stringify(visual.joints)} data-xyzac-offsets={JSON.stringify(visual.offsets)} aria-label="XYZAC TRT Vismach machine"><pre className="axis-preview-hud" aria-label="XYZAC authoritative visual state">{`XYZAC TRT\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\nA ${Number(pose.a).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ ${visual.joints.map((value) => Number(value).toFixed(2)).join(" ")}\nTick ${visual.tick}\nHash ${visual.stateHash.slice(0, 18)}`}</pre></div>;
|
||||
}
|
||||
|
||||
export function XyzacMachineViewFromSnapshot({ snapshot, toolpath, ...props }: Omit<Props, "visual"> & { snapshot: XyzacVisualSource | null | undefined }) {
|
||||
return <XyzacMachineView visual={projectXyzacVisual(snapshot)} toolpath={toolpath} {...props} />;
|
||||
}
|
||||
Reference in New Issue
Block a user