diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx index 1bdc6fef..61075a15 100644 --- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx @@ -22,11 +22,13 @@ import MultiMotionIcon from '../../../Icons/MultiMotionIcon.jsx' import SettingsIcon from '../../../Icons/SettingsIcon.jsx' import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel.jsx' +import PrinterGCodeFilePanel from '../../common/PrinterGCodeFilePanel.jsx' import PrinterPositionPanel from '../../common/PrinterPositionPanel.jsx' import PrinterMovementPanel from '../../common/PrinterMovementPanel.jsx' import PrinterMiscPanel from '../../common/PrinterMiscPanel.jsx' import JobIcon from '../../../Icons/JobIcon.jsx' import SubJobIcon from '../../../Icons/SubJobIcon.jsx' +import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx' import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx' import MissingPlaceholder from '../../common/MissingPlaceholder.jsx' import { useMediaQuery } from 'react-responsive' @@ -59,6 +61,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => { printer: true, job: true, subjob: true, + gcodeFile: true, queue: true, filamentStock: true, temperature: true, @@ -307,6 +310,10 @@ const ControlPrinter = ({ slicerIntegration = false }) => { key: 'subJob', label: 'Sub Job' }, + { + key: 'gcodeFile', + label: 'GCode File' + }, { key: 'queue', label: 'Queue' @@ -553,6 +560,23 @@ const ControlPrinter = ({ slicerIntegration = false }) => { /> )} + } + collapseKey='gcodeFile' + active={collapseState.gcodeFile} + onToggle={(expanded) => + updateCollapseState('gcodeFile', expanded) + } + > + + } diff --git a/src/components/Dashboard/common/GCodePreview.jsx b/src/components/Dashboard/common/GCodePreview.jsx index 285b1d9c..c81db6a1 100644 --- a/src/components/Dashboard/common/GCodePreview.jsx +++ b/src/components/Dashboard/common/GCodePreview.jsx @@ -50,6 +50,30 @@ function getCameraScale(sceneManager, baseDistance) { return distance == null ? null : clampPreviewScale(baseDistance / distance) } +function layerEndForGcodePosition(preview, gcodePosition) { + const layers = preview?.job?.layers + const count = layers?.length || preview?.countLayers || 0 + if (!count || !gcodePosition) return undefined + if (gcodePosition.isActive === false) return undefined + + const currentLayer = gcodePosition.currentLayer + if (typeof currentLayer === 'number' && currentLayer > 0) { + return Math.min(Math.round(currentLayer), count) + } + + const z = gcodePosition.position?.[2] + if (typeof z !== 'number' || !Number.isFinite(z) || !layers?.length) { + return undefined + } + + let index = -1 + for (let i = 0; i < layers.length; i++) { + if (layers[i].z <= z + 0.05) index = i + else break + } + return index < 0 ? 1 : index + 1 +} + function applyCameraScale(sceneManager, baseDistance, scale) { const { camera, controls } = sceneManager const distance = getCameraDistance(sceneManager) @@ -73,7 +97,9 @@ function GCodePreviewUI(props) { startLayer, endLayer, lineWidth, - style = {} + style = {}, + showFollowCurrentSubJob = false, + gcodePosition = null } = props const { isDarkMode, themeConfig } = useThemeContext() const { colorPrimary, colorWarning, colorPink } = themeConfig.token @@ -88,10 +114,33 @@ function GCodePreviewUI(props) { const [isDragging, setIsDragging] = useState(false) const [renderMode, setRenderMode] = useState('lines') const [travelVisibility, setTravelVisibility] = useState('hide') + const [followSelection, setFollowSelection] = useState('follow') const renderModeRef = useRef(renderMode) const travelVisibilityRef = useRef(travelVisibility) + const followRef = useRef({ + enabled: false, + gcodePosition: null, + endLayer + }) renderModeRef.current = renderMode travelVisibilityRef.current = travelVisibility + followRef.current = { + enabled: showFollowCurrentSubJob && followSelection === 'follow', + gcodePosition, + endLayer + } + + const applyFollowEndLayer = useCallback((instance) => { + if (!instance || previewRef.current !== instance) return + const sceneManager = instance.sceneManager + if (!sceneManager) return + const follow = followRef.current + const next = follow.enabled + ? layerEndForGcodePosition(instance, follow.gcodePosition) + : follow.endLayer + if (sceneManager.endLayer === next) return + sceneManager.endLayer = next + }, []) const resizePreview = useCallback(() => { previewRef.current?.sceneManager?.resize() @@ -154,6 +203,9 @@ function GCodePreviewUI(props) { controls.addEventListener('start', handleControlsStart) controls.addEventListener('end', handleControlsEnd) + instance.onJobUpdated = () => { + applyFollowEndLayer(instance) + } instance.sceneManager.resize() setPreview(instance) @@ -166,6 +218,7 @@ function GCodePreviewUI(props) { window.addEventListener('resize', resizePreview) return () => { + instance.onJobUpdated = undefined resizeObserver?.disconnect() window.removeEventListener('resize', resizePreview) controls.removeEventListener('change', handleControlsChange) @@ -189,7 +242,8 @@ function GCodePreviewUI(props) { lineWidth, startLayer, topLayerColor, - resizePreview + resizePreview, + applyFollowEndLayer ]) useEffect(() => { @@ -275,6 +329,7 @@ function GCodePreviewUI(props) { if (typeof endLayer === 'number') { instance.sceneManager.endLayer = endLayer } + applyFollowEndLayer(instance) } catch (e) { if (abortController.signal.aborted) return console.error('Failed to load G-code from src', e) @@ -295,7 +350,18 @@ function GCodePreviewUI(props) { lineWidth, src, startLayer, - topLayerColor + topLayerColor, + applyFollowEndLayer + ]) + + useEffect(() => { + applyFollowEndLayer(previewRef.current) + }, [ + applyFollowEndLayer, + followSelection, + gcodePosition, + preview, + showFollowCurrentSubJob ]) return ( @@ -364,6 +430,18 @@ function GCodePreviewUI(props) { popupMatchSelectWidth={false} onChange={setTravelVisibility} /> + {showFollowCurrentSubJob ? ( +