Files
linuxcncWebWASM/web/react/src/ThreeMachineView.tsx

135 lines
8.6 KiB
TypeScript

import { useEffect, useRef } from "react";
import * as THREE from "three";
type ViewMode = "z" | "z2" | "x" | "y" | "y2" | "p";
type Point = Record<string, number>;
type Segment = { id?: number; source_line?: number | null; active?: boolean; rapid?: boolean; motion_type?: number; turn?: number; center?: Point; normal?: Point; start?: Point; end?: Point };
type Props = { axes: number[]; view?: ViewMode; zoom?: number; rotateMode?: boolean; segments?: Segment[]; showPath?: boolean; showLive?: boolean; showLimits?: boolean; showVelocity?: boolean; showOffsets?: boolean; showExtents?: boolean };
export function ThreeMachineView({ axes, view = "p", zoom = 1, rotateMode = false, segments = [], showPath = true, showLive = true, showLimits = false, showVelocity = false, showOffsets = false, showExtents = false }: Props) {
const host = useRef<HTMLDivElement>(null);
const viewRef = useRef(view);
const zoomRef = useRef(zoom);
const rotateRef = useRef(rotateMode);
const segmentsRef = useRef(segments);
const layersRef = useRef({ showPath, showLive, showLimits, showVelocity, showOffsets, showExtents });
viewRef.current = view;
zoomRef.current = zoom;
rotateRef.current = rotateMode;
segmentsRef.current = segments;
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(0x080e13);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(6, 5, 7);
camera.lookAt(0, 1, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, 2));
element.appendChild(renderer.domElement);
// AXIS preview is a backplot of the source program. The Vismach solid
// model belongs to the machine view and must not be drawn in this pane.
const axesHelper = new THREE.AxesHelper(2.2);
axesHelper.name = "axis-coordinate-axes";
scene.add(axesHelper);
const pathGroup = new THREE.Group(); pathGroup.name = "axis-toolpath"; scene.add(pathGroup);
const livePathGroup = new THREE.Group(); livePathGroup.name = "axis-live-toolpath"; scene.add(livePathGroup);
const overlayGroup = new THREE.Group(); overlayGroup.name = "axis-overlays"; scene.add(overlayGroup);
const resize = () => {
const width = Math.max(element.clientWidth, 320);
const height = Math.max(element.clientHeight, 280);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
};
const observer = new ResizeObserver(resize);
observer.observe(element);
resize();
let frame = 0;
const render = () => {
const bounds = segmentsRef.current.reduce((box, segment) => {
for (const point of [segment.start ?? {}, segment.end ?? {}]) {
for (const axis of ["x", "y", "z"] as const) {
const value = Number(point[axis] ?? 0);
if (Number.isFinite(value)) { box.min[axis] = Math.min(box.min[axis], value); box.max[axis] = Math.max(box.max[axis], value); }
}
}
return box;
}, { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } });
const span = Math.max(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y, bounds.max.z - bounds.min.z, 1);
const scale = Math.min(0.12, 4.2 / span);
const center = { x: (bounds.min.x + bounds.max.x) / 2, y: (bounds.min.y + bounds.max.y) / 2, z: (bounds.min.z + bounds.max.z) / 2 };
// Keep the live coordinates available for the backplot marker without
// introducing a solid tool or machine mesh into the source preview.
pathGroup.visible = layersRef.current.showPath;
livePathGroup.visible = layersRef.current.showLive;
pathGroup.clear();
livePathGroup.clear();
const completedMaterial = new THREE.LineBasicMaterial({ color: 0xf0f0f0 });
const liveMaterial = new THREE.LineBasicMaterial({ color: 0xff6666 });
const traverseMaterial = new THREE.LineBasicMaterial({ color: 0x4d8080 });
const toView = (point: Point) => new THREE.Vector3(
(Number(point.x ?? 0) - center.x) * scale,
(Number(point.z ?? 0) - center.z) * scale,
(Number(point.y ?? 0) - center.y) * scale,
);
const arcPoints = (segment: Segment, start: Point, end: Point, centerPoint: Point, normal: Point) => {
const startVector = new THREE.Vector3(Number(start.x ?? 0) - Number(centerPoint.x ?? 0), Number(start.y ?? 0) - Number(centerPoint.y ?? 0), Number(start.z ?? 0) - Number(centerPoint.z ?? 0));
const endVector = new THREE.Vector3(Number(end.x ?? 0) - Number(centerPoint.x ?? 0), Number(end.y ?? 0) - Number(centerPoint.y ?? 0), Number(end.z ?? 0) - Number(centerPoint.z ?? 0));
const axis = new THREE.Vector3(Number(normal.x ?? 0), Number(normal.y ?? 0), Number(normal.z ?? 0));
if (!Number.isFinite(axis.length()) || axis.length() === 0 || startVector.length() === 0) return [toView(start), toView(end)];
axis.normalize();
let angle = Math.atan2(axis.dot(startVector.clone().cross(endVector)), startVector.dot(endVector));
const direction = Number(segment.turn ?? 1) < 0 ? -1 : 1;
if (Math.abs(angle) < 1e-8 && startVector.distanceTo(endVector) < 1e-7) angle = direction * Math.PI * 2;
else if (angle * direction < 0) angle += direction * Math.PI * 2;
const quaternion = new THREE.Quaternion();
const points: THREE.Vector3[] = [];
for (let index = 0; index <= 32; index += 1) {
const rotated = startVector.clone().applyQuaternion(quaternion.setFromAxisAngle(axis, angle * index / 32));
points.push(toView({ x: Number(centerPoint.x ?? 0) + rotated.x, y: Number(centerPoint.y ?? 0) + rotated.y, z: Number(centerPoint.z ?? 0) + rotated.z }));
}
return points;
};
for (const segment of segmentsRef.current) {
const start = segment.start ?? {}; const end = segment.end ?? {};
const points = segment.center && segment.normal && segment.motion_type === 2
? arcPoints(segment, start, end, segment.center, segment.normal)
: [toView(start), toView(end)];
const target = segment.active ? livePathGroup : pathGroup;
const material = segment.active ? liveMaterial : segment.motion_type === 0 || segment.rapid ? traverseMaterial : completedMaterial;
target.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), material));
}
if (layersRef.current.showLimits) {
const limits = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(5, 2, 3.5)), new THREE.LineBasicMaterial({ color: 0xd87d6c }));
limits.position.y = 0.9; overlayGroup.clear(); overlayGroup.add(limits);
} else overlayGroup.clear();
overlayGroup.visible = layersRef.current.showLimits || layersRef.current.showOffsets || layersRef.current.showExtents || layersRef.current.showVelocity;
const distance = 10 / Math.max(0.5, zoomRef.current);
const selectedView = viewRef.current;
if (selectedView === "z") camera.position.set(0, distance, 0.01);
else if (selectedView === "z2") camera.position.set(distance * 0.3, distance * 0.9, distance * 0.3);
else if (selectedView === "x") camera.position.set(distance, 2, 0.01);
else if (selectedView === "y") camera.position.set(0.01, 2, distance);
else if (selectedView === "y2") camera.position.set(0.01, 2, -distance);
else camera.position.set(6 / Math.max(0.5, zoomRef.current), 5 / Math.max(0.5, zoomRef.current), 7 / Math.max(0.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 [x = 0, y = 0, z = 0] = axes;
const format = (value: number) => Number(value || 0).toFixed(3);
return <div className="three-viewer" ref={host} aria-label="AXIS source program backplot">
<pre className="axis-preview-hud" aria-label="AXIS preview coordinates">{`X: ${format(x)} DTG X: ${format(-x)}\nY: ${format(y)} DTG Y: ${format(-y)}\nZ: ${format(z)} DTG Z: ${format(-z)}\nG54 X: ${format(x)} G92 X: 0.000\nG54 Y: ${format(y)} G92 Y: 0.000\nG54 Z: ${format(z)} G92 Z: 0.000\nG54 R: 0.000\nTLO X: 0.000\nTLO Y: 0.000\nTLO Z: 0.000\nVel: 0.000`}</pre>
</div>;
}