From c294e3f70557beb1ad23e3a8bf8b70787b9d0455 Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 29 Mar 2026 12:51:26 -0500 Subject: [PATCH 1/2] fix: offset gcode in viewer --- apps/web/src/routes/Monitor/index.tsx | 33 ++-------- .../routes/Setup/panels/VisualizerPanel.tsx | 65 ++++--------------- 2 files changed, 18 insertions(+), 80 deletions(-) diff --git a/apps/web/src/routes/Monitor/index.tsx b/apps/web/src/routes/Monitor/index.tsx index fa9221d1a..07ed04a47 100644 --- a/apps/web/src/routes/Monitor/index.tsx +++ b/apps/web/src/routes/Monitor/index.tsx @@ -38,7 +38,6 @@ import { useFeedrate, } from '@/store/hooks' import { machineStateSync } from '@/services/machineStateSync' -import { processGCode } from '@/lib/gcodeVisualizer' import { Vector3 } from 'three' import { machineToThree, type MachineLimits } from '@/lib/coordinates' import type { HomingCorner } from '@/lib/machineLimits' @@ -401,43 +400,23 @@ function VisualizerCameraView({ machinePosition, processedLines }: VisualizerCam return } - const result = processGCode(loadedGcode.gcode) - - if (!result?.firstVertex) { - return - } - const limits: MachineLimits = settings.machine.limits const homingCorner: HomingCorner = settings.machine.homingCorner ?? 'front-left' - + // Calculate work offset: WorkOffset = MPos - WPos const workOffset = { x: machinePosition.x - workPosition.x, y: machinePosition.y - workPosition.y, z: machinePosition.z - workPosition.z } - + // WCS origin (0,0,0) in machine coordinates is the work offset // Convert WCS origin to Three.js coordinates + // G-code coordinates are in WCS, so the offset to map them into Three.js space + // is simply the Three.js position of WCS (0,0,0) const wcsOriginThree = machineToThree(workOffset, limits, homingCorner) - - // G-code coordinates from gcode-toolpath are in WCS coordinates - // They are currently being rendered directly as Three.js coordinates (no conversion) - // So the G-code origin location in Three.js is just the firstVertex value - const gcodeOriginThree = { - x: result.firstVertex.x, - y: result.firstVertex.y, - z: result.firstVertex.z - } - - // Calculate offset to move G-code origin to WCS origin location - const offset = new Vector3( - wcsOriginThree.x - gcodeOriginThree.x, - wcsOriginThree.y - gcodeOriginThree.y, - wcsOriginThree.z - gcodeOriginThree.z - ) - - const offsetValue = { x: offset.x, y: offset.y, z: offset.z } + + const offsetValue = { x: wcsOriginThree.x, y: wcsOriginThree.y, z: wcsOriginThree.z } setModelOffset(offsetValue) placedGcodeRef.current = loadedGcode.name // Save offset to localStorage for persistence across views diff --git a/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx b/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx index 61dedecc6..e040bb5c2 100644 --- a/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx +++ b/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx @@ -11,7 +11,6 @@ import { SingleMethodProbeFlow } from '@/components/SingleMethodProbeFlow' import { ToolChangeTab } from '@/components/ToolChangeTab' import { JobSetupWizard } from '@/components/JobSetupWizard' import { useToolChange } from '@/contexts/ToolChangeContext' -import { processGCode } from '@/lib/gcodeVisualizer' import { calculateOutline, type Point2D } from '@/lib/gcodeOutline' import { Vector3 } from 'three' import { machineToThree, type MachineLimits } from '@/lib/coordinates' @@ -491,43 +490,23 @@ export function VisualizerPanel({ return } - const result = processGCode(loadedGcode.gcode) - - if (!result?.firstVertex) { - return - } - const limits: MachineLimits = settings.machine.limits const homingCorner: HomingCorner = settings.machine.homingCorner ?? 'front-left' - + // Calculate work offset: WorkOffset = MPos - WPos const workOffset = { x: machinePosition.x - workPosition.x, y: machinePosition.y - workPosition.y, z: machinePosition.z - workPosition.z } - + // WCS origin (0,0,0) in machine coordinates is the work offset // Convert WCS origin to Three.js coordinates + // G-code coordinates are in WCS, so the offset to map them into Three.js space + // is simply the Three.js position of WCS (0,0,0) const wcsOriginThree = machineToThree(workOffset, limits, homingCorner) - - // G-code coordinates from gcode-toolpath are in WCS coordinates - // They are currently being rendered directly as Three.js coordinates (no conversion) - // So the G-code origin location in Three.js is just the firstVertex value - const gcodeOriginThree = { - x: result.firstVertex.x, - y: result.firstVertex.y, - z: result.firstVertex.z - } - - // Calculate offset to move G-code origin to WCS origin location - const offset = new Vector3( - wcsOriginThree.x - gcodeOriginThree.x, - wcsOriginThree.y - gcodeOriginThree.y, - wcsOriginThree.z - gcodeOriginThree.z - ) - - const offsetValue = { x: offset.x, y: offset.y, z: offset.z } + + const offsetValue = { x: wcsOriginThree.x, y: wcsOriginThree.y, z: wcsOriginThree.z } setModelOffset(offsetValue) placedGcodeRef.current = loadedGcode.name // Save offset to localStorage for persistence across views @@ -546,43 +525,23 @@ export function VisualizerPanel({ return } - const result = processGCode(loadedGcode.gcode) - - if (!result?.firstVertex) { - return - } - const limits: MachineLimits = settings.machine.limits const homingCorner: HomingCorner = settings.machine.homingCorner ?? 'front-left' - + // Calculate work offset: WorkOffset = MPos - WPos const workOffset = { x: machinePosition.x - workPosition.x, y: machinePosition.y - workPosition.y, z: machinePosition.z - workPosition.z } - + // WCS origin (0,0,0) in machine coordinates is the work offset // Convert WCS origin to Three.js coordinates + // G-code coordinates are in WCS, so the offset to map them into Three.js space + // is simply the Three.js position of WCS (0,0,0) const wcsOriginThree = machineToThree(workOffset, limits, homingCorner) - - // G-code coordinates from gcode-toolpath are in WCS coordinates - // They are currently being rendered directly as Three.js coordinates (no conversion) - // So the G-code origin location in Three.js is just the firstVertex value - const gcodeOriginThree = { - x: result.firstVertex.x, - y: result.firstVertex.y, - z: result.firstVertex.z - } - - // Calculate offset to move G-code origin to WCS origin location - const offset = new Vector3( - wcsOriginThree.x - gcodeOriginThree.x, - wcsOriginThree.y - gcodeOriginThree.y, - wcsOriginThree.z - gcodeOriginThree.z - ) - - const offsetValue = { x: offset.x, y: offset.y, z: offset.z } + + const offsetValue = { x: wcsOriginThree.x, y: wcsOriginThree.y, z: wcsOriginThree.z } setModelOffset(offsetValue) placedGcodeRef.current = loadedGcode.name // Save offset to localStorage for persistence across views From 89e597699866bd6d86e58173a1d941a2e4266707 Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 29 Mar 2026 13:43:41 -0500 Subject: [PATCH 2/2] feat: Add visualizer mode setting to toggle between machine and WCS views Adds a machine setting to switch the 3D visualizer between absolute machine coordinates (with full work envelope) and WCS-relative coordinates (toolpath only, centered on work origin). Applies to both Setup and Monitor pages. Co-Authored-By: Claude Opus 4.6 --- apps/shared/src/schemas/settings.js | 1 + apps/web/src/routes/Monitor/index.tsx | 19 +- apps/web/src/routes/Settings/index.tsx | 14 +- .../Settings/sections/MachineSection.tsx | 27 +++ .../Setup/components/VisualizerScene.tsx | 187 ++++++++++++------ .../routes/Setup/panels/VisualizerPanel.tsx | 24 +-- 6 files changed, 187 insertions(+), 85 deletions(-) diff --git a/apps/shared/src/schemas/settings.js b/apps/shared/src/schemas/settings.js index 8e9c2d659..eb8722e9d 100644 --- a/apps/shared/src/schemas/settings.js +++ b/apps/shared/src/schemas/settings.js @@ -55,6 +55,7 @@ export const MachineSettingsSchema = z.object({ toolSpinup: ToolSpinupSchema.optional(), spindleWarmup: SpindleWarmupSchema.optional(), autoSwitchToMonitor: z.boolean().default(true), + visualizerMode: z.enum(['machine', 'wcs']).default('machine'), }); // ============================================================================= diff --git a/apps/web/src/routes/Monitor/index.tsx b/apps/web/src/routes/Monitor/index.tsx index 07ed04a47..40ebca54b 100644 --- a/apps/web/src/routes/Monitor/index.tsx +++ b/apps/web/src/routes/Monitor/index.tsx @@ -252,11 +252,12 @@ function VisualizerCameraView({ machinePosition, processedLines }: VisualizerCam const [viewMode, setViewMode] = useState('side-by-side') const { data: settings } = useGetSettingsQuery() const dispatch = useAppDispatch() - + const vizMode = settings?.machine?.visualizerMode ?? 'machine' + // Get shared machine state for positions const workPosition = useWorkPosition() const connectedPort = useConnectedPort() // Use Redux state instead of settings - + // G-code state for visualizer const [loadedGcode, setLoadedGcode] = useState<{ name: string; gcode: string } | null>(null) const [modelOffset, setModelOffset] = useState<{ x: number; y: number; z: number } | null>(null) @@ -446,14 +447,15 @@ function VisualizerCameraView({ machinePosition, processedLines }: VisualizerCam ${viewMode === 'side-by-side' ? 'w-1/2' : 'w-full'} flex-1 relative `}> - {/* PiP camera overlay when visualizer is full screen */} {viewMode === 'pip-visual' && ( @@ -483,14 +485,15 @@ function VisualizerCameraView({ machinePosition, processedLines }: VisualizerCam {t('3D View')}
-
diff --git a/apps/web/src/routes/Settings/index.tsx b/apps/web/src/routes/Settings/index.tsx index b947e48df..752ce0a77 100644 --- a/apps/web/src/routes/Settings/index.tsx +++ b/apps/web/src/routes/Settings/index.tsx @@ -114,6 +114,7 @@ const DEFAULT_MACHINE_CONFIG: MachineConfig = { zmax: 0, }, homingCorner: 'front-left', // Most common homing position + visualizerMode: 'machine' as const, autoSwitchToMonitorEnabled: true, // Enabled by default toolSpinupDelayEnabled: true, // Enabled by default toolSpinupDelaySeconds: 5, // 5 seconds default delay @@ -427,6 +428,9 @@ export default function Settings() { } // Controller settings + if (settings.machine?.visualizerMode !== undefined) { + setMachineConfig(prev => ({ ...prev, visualizerMode: settings.machine!.visualizerMode! })) + } if (settings.machine?.autoSwitchToMonitor !== undefined) { setMachineConfig(prev => ({ ...prev, autoSwitchToMonitorEnabled: settings.machine!.autoSwitchToMonitor! })) } @@ -940,6 +944,7 @@ export default function Settings() { name: importedSettings.machine?.name ?? prev.name, limits: importedSettings.machine?.limits ?? prev.limits, homingCorner: importedSettings.machine?.homingCorner ?? prev.homingCorner, + visualizerMode: importedSettings.machine?.visualizerMode ?? prev.visualizerMode, autoSwitchToMonitorEnabled: importedSettings.machine?.autoSwitchToMonitor ?? prev.autoSwitchToMonitorEnabled, toolSpinupDelayEnabled: importedSettings.machine?.toolSpinup?.enabled ?? prev.toolSpinupDelayEnabled, toolSpinupDelaySeconds: importedSettings.machine?.toolSpinup?.delaySeconds ?? prev.toolSpinupDelaySeconds, @@ -1119,6 +1124,7 @@ export default function Settings() { machine: { name: DEFAULT_MACHINE_CONFIG.name, limits: DEFAULT_MACHINE_CONFIG.limits, + visualizerMode: DEFAULT_MACHINE_CONFIG.visualizerMode, autoSwitchToMonitor: DEFAULT_MACHINE_CONFIG.autoSwitchToMonitorEnabled, toolSpinup: { enabled: DEFAULT_MACHINE_CONFIG.toolSpinupDelayEnabled, @@ -1304,6 +1310,9 @@ export default function Settings() { if (changes.homingCorner !== undefined) { updated.homingCorner = changes.homingCorner } + if (changes.visualizerMode !== undefined) { + updated.visualizerMode = changes.visualizerMode + } if (changes.autoSwitchToMonitorEnabled !== undefined) { updated.autoSwitchToMonitorEnabled = changes.autoSwitchToMonitorEnabled } @@ -1333,11 +1342,14 @@ export default function Settings() { // Save to backend const saveData: PartialSettings = {} - if (changes.name !== undefined || changes.limits || changes.homingCorner !== undefined || changes.autoSwitchToMonitorEnabled !== undefined) { + if (changes.name !== undefined || changes.limits || changes.homingCorner !== undefined || changes.visualizerMode !== undefined || changes.autoSwitchToMonitorEnabled !== undefined) { saveData.machine = saveData.machine || {} if (changes.name !== undefined) saveData.machine.name = changes.name if (changes.limits) saveData.machine.limits = changes.limits if (changes.homingCorner !== undefined) saveData.machine.homingCorner = changes.homingCorner + if (changes.visualizerMode !== undefined) { + saveData.machine.visualizerMode = changes.visualizerMode + } if (changes.autoSwitchToMonitorEnabled !== undefined) { saveData.machine.autoSwitchToMonitor = changes.autoSwitchToMonitorEnabled } diff --git a/apps/web/src/routes/Settings/sections/MachineSection.tsx b/apps/web/src/routes/Settings/sections/MachineSection.tsx index 839a32e11..05d3e1949 100644 --- a/apps/web/src/routes/Settings/sections/MachineSection.tsx +++ b/apps/web/src/routes/Settings/sections/MachineSection.tsx @@ -77,6 +77,7 @@ export interface MachineConfig { zmax: number } homingCorner?: HomingCorner // Optional: inferred if not provided + visualizerMode: 'machine' | 'wcs' autoSwitchToMonitorEnabled: boolean toolSpinupDelayEnabled: boolean toolSpinupDelaySeconds: number @@ -423,6 +424,32 @@ export function MachineSection({ {/* Controller Behavior */}
+ +
+ + +
+
+ // Outline points to visualize + vizMode?: VizMode // 'machine' = absolute envelope view (default), 'wcs' = relative to gcode WCS origin } // Grid component - draws a grid on the z=0 plane, starting at origin and extending in positive X and Y @@ -434,6 +437,60 @@ function BillboardText({ position, children, fontSize = 20, ...props }: React.Co ) } +// Simple WCS-mode axes at origin with labeled arrows +function WCSAxes({ arrowLength = 50 }: { arrowLength?: number }) { + const scene = useThree((state) => state.scene) + + useEffect(() => { + const headLength = arrowLength * 0.15 + const headWidth = headLength * 0.3 + const xArrow = new ArrowHelper(new Vector3(1, 0, 0), new Vector3(0, 0, 0), arrowLength, 0xff0000, headLength, headWidth) + const yArrow = new ArrowHelper(new Vector3(0, 1, 0), new Vector3(0, 0, 0), arrowLength, 0x00ff00, headLength, headWidth) + const zArrow = new ArrowHelper(new Vector3(0, 0, 1), new Vector3(0, 0, 0), arrowLength, 0x0000ff, headLength, headWidth) + scene.add(xArrow) + scene.add(yArrow) + scene.add(zArrow) + + return () => { + scene.remove(xArrow) + scene.remove(yArrow) + scene.remove(zArrow) + } + }, [arrowLength, scene]) + + return null +} + +// Infinite-style grid centered on WCS origin for WCS mode +function WCSGrid({ size = 200, divisions = 20 }: { size?: number; divisions?: number }) { + const geometry = useMemo(() => { + const geo = new BufferGeometry() + const positions: number[] = [] + const half = size / 2 + const step = size / divisions + + // Lines parallel to Y + for (let i = 0; i <= divisions; i++) { + const x = -half + i * step + positions.push(x, -half, 0, x, half, 0) + } + // Lines parallel to X + for (let i = 0; i <= divisions; i++) { + const y = -half + i * step + positions.push(-half, y, 0, half, y, 0) + } + + geo.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3)) + return geo + }, [size, divisions]) + + return ( + + + + ) +} + // Camera controller component that responds to view changes function CameraController({ xSize, ySize, zSize, view, viewKey }: { xSize: number; ySize: number; zSize: number; view?: 'top' | 'front' | 'iso' | 'fit'; viewKey?: number }) { const { camera } = useThree() @@ -545,7 +602,7 @@ function CameraController({ xSize, ySize, zSize, view, viewKey }: { xSize: numbe } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function VisualizerScene({ gcode, limits: _limits, view, viewKey, machinePosition, modelOffset, processedLines, outlinePoints }: VisualizerSceneProps = {}) { +export function VisualizerScene({ gcode, limits: _limits, view, viewKey, machinePosition, modelOffset, processedLines, outlinePoints, vizMode = 'machine' }: VisualizerSceneProps = {}) { const { t } = useTranslation() const [webglAvailable, setWebglAvailable] = useState(null) @@ -605,7 +662,24 @@ export function VisualizerScene({ gcode, limits: _limits, view, viewKey, machine // Machine position for display (default to 0,0,0 if not available) const displayMachinePos = machinePosition || { x: 0, y: 0, z: 0 } - + + // In WCS mode, compute bounding box from gcode to size the grid/camera + const wcsGcodeResult = useMemo(() => { + if (vizMode !== 'wcs' || !gcode) return null + return processGCode(gcode) + }, [vizMode, gcode]) + + const wcsSceneSize = useMemo(() => { + if (!wcsGcodeResult?.boundingBox) return 200 + const bb = wcsGcodeResult.boundingBox + return Math.max(bb.max.x - bb.min.x, bb.max.y - bb.min.y, Math.abs(bb.max.z - bb.min.z), 100) + }, [wcsGcodeResult]) + + // Scene sizing depends on mode + const sceneXSize = vizMode === 'machine' ? xSize : wcsSceneSize + const sceneYSize = vizMode === 'machine' ? ySize : wcsSceneSize + const sceneZSize = vizMode === 'machine' ? zSize : wcsSceneSize + if (!webglAvailable) { return (
@@ -626,79 +700,62 @@ export function VisualizerScene({ gcode, limits: _limits, view, viewKey, machine far={10000} up={[0, 0, 1]} /> - + {/* Camera controls */} - - + + {/* Lighting */} - - {/* Grid on z=0 plane, using actual machine dimensions */} - - - {/* X-axis arrows - red arrows along X edges pointing in positive direction */} - - - {/* Y-axis arrows - green arrows along Y edges pointing in positive direction */} - - - {/* Z-axis arrows - blue arrow at origin, blue lines at other 3 corners */} - - - {/* Gray rectangle connecting the four tops of the Z-axis lines */} - - - {/* X-axis label - at y=0 edge */} - - X - - - {/* Y-axis label - at x=0 edge */} - - Y - - - {/* Z-axis label - near the Z arrow at origin */} - - Z - - - {/* Origin marker - red dot at 0,0,0 */} - - - - - - {/* G-code toolpath visualization */} + + {vizMode === 'machine' ? ( + <> + {/* Machine envelope visualization */} + + + + + + + X + Y + Z + + {/* Origin marker */} + + + + + + {/* Tool/endmill indicator - positioned at current machine coordinates */} + + + ) : ( + <> + {/* WCS mode: simple grid + axes at origin, no envelope */} + + + + X + Y + Z + + {/* Origin marker */} + + + + + + )} + + {/* G-code toolpath visualization (shown in both modes) */} - + {/* Outline visualization - pink line showing toolpath boundary */} {outlinePoints && outlinePoints.length > 0 && ( )} - - {/* Tool/endmill indicator - positioned at current machine coordinates */} - {/* Position readout overlay - only show if debug mode is enabled */} diff --git a/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx b/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx index e040bb5c2..7ac2fba7b 100644 --- a/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx +++ b/apps/web/src/routes/Setup/panels/VisualizerPanel.tsx @@ -230,6 +230,7 @@ export function VisualizerPanel({ const [tab, setTab] = useState<'3d' | 'console' | 'camera' | 'wizard' | 'toolchange' | 'setup'>('3d') const [view, setView] = useState<'top' | 'front' | 'iso' | 'fit' | undefined>('iso') const [viewKey, setViewKey] = useState(0) + const vizMode = settings?.machine?.visualizerMode ?? 'machine' // Switch to wizard tab when wizard method is set, and back to 3D view when it closes useEffect(() => { @@ -695,17 +696,18 @@ export function VisualizerPanel({ {/* 3D View Tab */}
- - + {/* View controls overlay */}
@@ -713,17 +715,17 @@ export function VisualizerPanel({
- - {/* Place Model button */} - {loadedGcode && ( + + {/* Place Model button - only in machine mode */} + {loadedGcode && vizMode === 'machine' && (
-