import { useEffect, useRef } from "react"; import * as THREE from "three"; type ViewMode = "z" | "z2" | "x" | "y" | "y2" | "p"; type Point = Record; 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(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
{`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`}
; }