Match 3axis preview to AXIS backplot
This commit is contained in:
@@ -191,8 +191,8 @@ export function App() {
|
||||
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={openProgramFile}>Open G-code...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn}>Power on</MenuAction><MenuAction onClick={resetMachine}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
|
||||
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label="Power on / recover [F2]" asset="tool_power.gif" onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact 3axis tutorial case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => { setPlotCleared(true); setCommandStatus("Live plot cleared"); }} /></section>
|
||||
<section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_AXIS3_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | 3-axis AXIS</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled}>Load case</TextButton><span className="case-status" role="status">{caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></section>
|
||||
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><ThreeMachineView axes={axes} segments={plotCleared ? [] : (state.toolpath.length ? state.toolpath : trajectory.map((segment) => ({ ...segment, active: true })))} view={view} zoom={zoom} rotateMode={rotateMode} showGrid={layers.grid} showTool={layers.tool} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
||||
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["grid", "tool", "path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
|
||||
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><ThreeMachineView axes={axes} segments={plotCleared ? [] : (state.toolpath.length ? state.toolpath : trajectory.map((segment) => ({ ...segment, active: true })))} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
||||
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
|
||||
<ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} onOpenFile={openProgramFile} onLoad={() => { setPlotCleared(false); execute("PROGRAM_LOAD", { source: programText, path: "main.ngc" }); }} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled} canRun={gates.run.enabled} canStep={gates.step.enabled} currentLine={Number(state.program?.line ?? state.task?.current_line ?? 0) || null} diagnostic={state.error?.code ?? null} />
|
||||
<input ref={programFileInput} className="visually-hidden-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
|
||||
<footer className="axis-info-bar" data-axis-feature="AXIS-UI-07"><span className={`task-state ${state.status}`} data-axis-feature="AXIS-SHELL-01">{taskState}</span><span className="machine-state" data-axis-state-machine={machineState}>Machine {machineState}</span><span className="operation-state" data-axis-state-operation={operationState}>Operation {operationState}</span><span className="tool-state" data-axis-feature="AXIS-UI-07">Tool {toolNumber}</span><span className="position-state" data-axis-feature="AXIS-VIEW-17">{position}</span><span className="footer-status" role="status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer>
|
||||
|
||||
@@ -2,23 +2,22 @@ import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
|
||||
type ViewMode = "z" | "z2" | "x" | "y" | "y2" | "p";
|
||||
type Segment = { id?: number; source_line?: number | null; active?: boolean; rapid?: boolean; start?: Record<string, number>; end?: Record<string, number> };
|
||||
type Props = { axes: number[]; view?: ViewMode; zoom?: number; rotateMode?: boolean; segments?: Segment[]; showGrid?: boolean; showTool?: boolean; showPath?: boolean; showLive?: boolean; showLimits?: boolean; showVelocity?: boolean; showOffsets?: boolean; showExtents?: boolean };
|
||||
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 = [], showGrid = true, showTool = true, showPath = true, showLive = true, showLimits = false, showVelocity = false, showOffsets = false, showExtents = false }: Props) {
|
||||
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 axesRef = useRef(axes);
|
||||
const viewRef = useRef(view);
|
||||
const zoomRef = useRef(zoom);
|
||||
const rotateRef = useRef(rotateMode);
|
||||
const segmentsRef = useRef(segments);
|
||||
const layersRef = useRef({ showGrid, showTool, showPath, showLive, showLimits, showVelocity, showOffsets, showExtents });
|
||||
axesRef.current = axes;
|
||||
const layersRef = useRef({ showPath, showLive, showLimits, showVelocity, showOffsets, showExtents });
|
||||
viewRef.current = view;
|
||||
zoomRef.current = zoom;
|
||||
rotateRef.current = rotateMode;
|
||||
segmentsRef.current = segments;
|
||||
layersRef.current = { showGrid, showTool, showPath, showLive, showLimits, showVelocity, showOffsets, showExtents };
|
||||
layersRef.current = { showPath, showLive, showLimits, showVelocity, showOffsets, showExtents };
|
||||
|
||||
useEffect(() => {
|
||||
const element = host.current;
|
||||
@@ -32,43 +31,14 @@ export function ThreeMachineView({ axes, view = "p", zoom = 1, rotateMode = fals
|
||||
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, 2));
|
||||
element.appendChild(renderer.domElement);
|
||||
|
||||
const grid = new THREE.GridHelper(8, 16, 0x405765, 0x1e303b);
|
||||
grid.name = "axis-grid";
|
||||
scene.add(grid);
|
||||
scene.add(new THREE.AxesHelper(2.2));
|
||||
const machine = new THREE.Group();
|
||||
machine.name = "axis-3axis-tutorial-model";
|
||||
const frameMaterial = new THREE.MeshStandardMaterial({ color: 0x7d8b91, roughness: 0.82 });
|
||||
const xMaterial = new THREE.MeshStandardMaterial({ color: 0x3d6fc4, roughness: 0.72 });
|
||||
const yMaterial = new THREE.MeshStandardMaterial({ color: 0x3f9b69, roughness: 0.72 });
|
||||
const zMaterial = new THREE.MeshStandardMaterial({ color: 0xd3ba45, roughness: 0.7 });
|
||||
const toolMaterial = new THREE.MeshStandardMaterial({ color: 0xc9e86d, emissive: 0x243200 });
|
||||
const sourceScale = 0.01;
|
||||
const frameAssembly = new THREE.Group(); frameAssembly.name = "frame"; machine.add(frameAssembly);
|
||||
const base = new THREE.Mesh(new THREE.BoxGeometry(2, 0.2, 5.6), frameMaterial); base.position.set(0, -0.1, 0); frameAssembly.add(base);
|
||||
const column = new THREE.Mesh(new THREE.BoxGeometry(1.2, 4, 0.6), frameMaterial); column.position.set(0, 2, -2.3); frameAssembly.add(column);
|
||||
const top = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.4, 1.2), frameMaterial); top.position.set(0, 4.2, -2.2); frameAssembly.add(top);
|
||||
const yAssembly = new THREE.Group(); yAssembly.name = "yassembly"; machine.add(yAssembly);
|
||||
const yBase = new THREE.Mesh(new THREE.BoxGeometry(2, 0.1, 2), yMaterial); yBase.position.set(0, 0.25, 0); yAssembly.add(yBase);
|
||||
const xAssembly = new THREE.Group(); xAssembly.name = "xassembly"; yAssembly.add(xAssembly);
|
||||
const xBase = new THREE.Mesh(new THREE.BoxGeometry(10, 0.3, 2), xMaterial); xBase.position.set(0, 0.35, 0); xAssembly.add(xBase);
|
||||
const work = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.12, 1.2), new THREE.MeshStandardMaterial({ color: 0x8f9da4, roughness: 0.9 }));
|
||||
work.position.set(0, 0.56, 0); xAssembly.add(work);
|
||||
const zAssembly = new THREE.Group(); zAssembly.name = "zassembly"; machine.add(zAssembly);
|
||||
const zBody = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.0, 0.9), zMaterial); zBody.position.set(0, 3.4, 0); zAssembly.add(zBody);
|
||||
const spindle = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.3, 1.0, 24), zMaterial); spindle.position.set(0, 2.9, 0); zAssembly.add(spindle);
|
||||
const cat30 = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.3, 0.35, 20), new THREE.MeshStandardMaterial({ color: 0xd15a52 })); cat30.position.set(0, 2.35, 0); zAssembly.add(cat30);
|
||||
const tool = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.8, 16), toolMaterial); tool.position.set(0, 1.85, 0); zAssembly.add(tool);
|
||||
scene.add(machine);
|
||||
// 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 ambient = new THREE.HemisphereLight(0xc8e4ec, 0x111820, 1.8);
|
||||
scene.add(ambient);
|
||||
const key = new THREE.DirectionalLight(0xffffff, 2.2);
|
||||
key.position.set(3, 6, 4);
|
||||
scene.add(key);
|
||||
|
||||
const resize = () => {
|
||||
const width = Math.max(element.clientWidth, 320);
|
||||
const height = Math.max(element.clientHeight, 280);
|
||||
@@ -81,7 +51,6 @@ export function ThreeMachineView({ axes, view = "p", zoom = 1, rotateMode = fals
|
||||
resize();
|
||||
let frame = 0;
|
||||
const render = () => {
|
||||
const [x = 0, y = 0, z = 0] = axesRef.current;
|
||||
const bounds = segmentsRef.current.reduce((box, segment) => {
|
||||
for (const point of [segment.start ?? {}, segment.end ?? {}]) {
|
||||
for (const axis of ["x", "y", "z"] as const) {
|
||||
@@ -94,22 +63,46 @@ export function ThreeMachineView({ axes, view = "p", zoom = 1, rotateMode = fals
|
||||
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 };
|
||||
xAssembly.position.x = Number(x || 0) * sourceScale;
|
||||
yAssembly.position.z = Number(y || 0) * sourceScale;
|
||||
zAssembly.position.y = Number(z || 0) * sourceScale;
|
||||
grid.visible = layersRef.current.showGrid;
|
||||
tool.visible = layersRef.current.showTool;
|
||||
// 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: 0x5c80a0 });
|
||||
const liveMaterial = new THREE.LineBasicMaterial({ color: 0x5fc4d8 });
|
||||
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 = [new THREE.Vector3((Number(start.x ?? 0) - center.x) * scale, (Number(start.z ?? 0) - center.z) * scale, (Number(start.y ?? 0) - center.y) * scale), new THREE.Vector3((Number(end.x ?? 0) - center.x) * scale, (Number(end.z ?? 0) - center.z) * scale, (Number(end.y ?? 0) - center.y) * scale)];
|
||||
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;
|
||||
target.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), segment.active ? liveMaterial : completedMaterial));
|
||||
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 }));
|
||||
@@ -124,7 +117,7 @@ export function ThreeMachineView({ axes, view = "p", zoom = 1, rotateMode = fals
|
||||
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, 1.8, 0);
|
||||
camera.lookAt(0, 0, 0);
|
||||
scene.rotation.y = rotateRef.current ? (performance.now() / 5000) % (Math.PI * 2) : 0;
|
||||
renderer.render(scene, camera);
|
||||
frame = requestAnimationFrame(render);
|
||||
@@ -133,5 +126,9 @@ export function ThreeMachineView({ axes, view = "p", zoom = 1, rotateMode = fals
|
||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); renderer.dispose(); element.replaceChildren(); };
|
||||
}, []);
|
||||
|
||||
return <div className="three-viewer" ref={host} aria-label="Three.js LinuxCNC machine visualization"><span className="three-viewer-label">Three.js / authoritative pose</span></div>;
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -100,10 +100,10 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
||||
.mdi-control li button:hover { color: #245d91; text-decoration: underline; }
|
||||
.empty-history { color: var(--muted); }
|
||||
|
||||
.axis-preview { display: flex; min-height: 0; flex: 1; padding: 4px; overflow: hidden; background: #101419; }
|
||||
.three-viewer { position: relative; width: 100%; height: 100%; min-height: 0; flex: 1; overflow: hidden; border: 1px solid #303b43; background: #080e13; }
|
||||
.axis-preview { display: flex; min-height: 0; flex: 1; padding: 4px; overflow: hidden; background: #000; }
|
||||
.three-viewer { position: relative; width: 100%; height: 100%; min-height: 0; flex: 1; overflow: hidden; border: 1px solid #303030; background: #000; }
|
||||
.three-viewer canvas { display: block; width: 100%; height: 100%; }
|
||||
.three-viewer-label { position: absolute; z-index: 1; top: 6px; left: 7px; padding: 2px 5px; color: #c9e86d; background: rgb(8 14 19 / 75%); font: 10px ui-monospace, monospace; pointer-events: none; }
|
||||
.axis-preview-hud { position: absolute; z-index: 1; top: 11px; left: 34px; margin: 0; color: #f2f2f2; font: 12px/1.45 ui-monospace, monospace; white-space: pre; pointer-events: none; text-shadow: 0 1px #000; }
|
||||
.dro-view { background: #ececec; }
|
||||
.dro-mode-row, .dro-column-headings, .dro-row { display: grid; grid-template-columns: 42px repeat(3, minmax(100px, 1fr)); align-items: center; gap: 9px; }
|
||||
.dro-mode-row { padding: 5px 6px; border-bottom: 1px solid #888; color: #555; font-size: 11px; }
|
||||
|
||||
Reference in New Issue
Block a user