Add GCode File Panel and Enhance Dashboard Printer Controls
- Introduced PrinterGCodeFilePanel component to display G-code file information and loading progress. - Updated ControlPrinter component to include GCode File section with collapsible panel for G-code file management. - Enhanced GCodePreview component to support following the current sub-job layer during G-code visualization. - Added logic to handle G-code position updates and improve user experience during file loading and display.
This commit is contained in:
parent
12c5cd83f4
commit
608eba41cf
@ -22,11 +22,13 @@ import MultiMotionIcon from '../../../Icons/MultiMotionIcon.jsx'
|
|||||||
import SettingsIcon from '../../../Icons/SettingsIcon.jsx'
|
import SettingsIcon from '../../../Icons/SettingsIcon.jsx'
|
||||||
|
|
||||||
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel.jsx'
|
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel.jsx'
|
||||||
|
import PrinterGCodeFilePanel from '../../common/PrinterGCodeFilePanel.jsx'
|
||||||
import PrinterPositionPanel from '../../common/PrinterPositionPanel.jsx'
|
import PrinterPositionPanel from '../../common/PrinterPositionPanel.jsx'
|
||||||
import PrinterMovementPanel from '../../common/PrinterMovementPanel.jsx'
|
import PrinterMovementPanel from '../../common/PrinterMovementPanel.jsx'
|
||||||
import PrinterMiscPanel from '../../common/PrinterMiscPanel.jsx'
|
import PrinterMiscPanel from '../../common/PrinterMiscPanel.jsx'
|
||||||
import JobIcon from '../../../Icons/JobIcon.jsx'
|
import JobIcon from '../../../Icons/JobIcon.jsx'
|
||||||
import SubJobIcon from '../../../Icons/SubJobIcon.jsx'
|
import SubJobIcon from '../../../Icons/SubJobIcon.jsx'
|
||||||
|
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
|
||||||
import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx'
|
import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx'
|
||||||
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
@ -59,6 +61,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
|||||||
printer: true,
|
printer: true,
|
||||||
job: true,
|
job: true,
|
||||||
subjob: true,
|
subjob: true,
|
||||||
|
gcodeFile: true,
|
||||||
queue: true,
|
queue: true,
|
||||||
filamentStock: true,
|
filamentStock: true,
|
||||||
temperature: true,
|
temperature: true,
|
||||||
@ -307,6 +310,10 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
|||||||
key: 'subJob',
|
key: 'subJob',
|
||||||
label: 'Sub Job'
|
label: 'Sub Job'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'gcodeFile',
|
||||||
|
label: 'GCode File'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'queue',
|
key: 'queue',
|
||||||
label: 'Queue'
|
label: 'Queue'
|
||||||
@ -553,6 +560,23 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
<InfoCollapse
|
||||||
|
title={'GCode File'}
|
||||||
|
icon={<GCodeFileIcon />}
|
||||||
|
collapseKey='gcodeFile'
|
||||||
|
active={collapseState.gcodeFile}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('gcodeFile', expanded)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PrinterGCodeFilePanel
|
||||||
|
id={printerId}
|
||||||
|
subJobId={
|
||||||
|
objectFormState.objectData?.currentSubJob?._id
|
||||||
|
}
|
||||||
|
offline={!objectFormState.objectData?.online}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title={'Filament Stock'}
|
title={'Filament Stock'}
|
||||||
icon={<FilamentStockIcon />}
|
icon={<FilamentStockIcon />}
|
||||||
|
|||||||
@ -50,6 +50,30 @@ function getCameraScale(sceneManager, baseDistance) {
|
|||||||
return distance == null ? null : clampPreviewScale(baseDistance / distance)
|
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) {
|
function applyCameraScale(sceneManager, baseDistance, scale) {
|
||||||
const { camera, controls } = sceneManager
|
const { camera, controls } = sceneManager
|
||||||
const distance = getCameraDistance(sceneManager)
|
const distance = getCameraDistance(sceneManager)
|
||||||
@ -73,7 +97,9 @@ function GCodePreviewUI(props) {
|
|||||||
startLayer,
|
startLayer,
|
||||||
endLayer,
|
endLayer,
|
||||||
lineWidth,
|
lineWidth,
|
||||||
style = {}
|
style = {},
|
||||||
|
showFollowCurrentSubJob = false,
|
||||||
|
gcodePosition = null
|
||||||
} = props
|
} = props
|
||||||
const { isDarkMode, themeConfig } = useThemeContext()
|
const { isDarkMode, themeConfig } = useThemeContext()
|
||||||
const { colorPrimary, colorWarning, colorPink } = themeConfig.token
|
const { colorPrimary, colorWarning, colorPink } = themeConfig.token
|
||||||
@ -88,10 +114,33 @@ function GCodePreviewUI(props) {
|
|||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [renderMode, setRenderMode] = useState('lines')
|
const [renderMode, setRenderMode] = useState('lines')
|
||||||
const [travelVisibility, setTravelVisibility] = useState('hide')
|
const [travelVisibility, setTravelVisibility] = useState('hide')
|
||||||
|
const [followSelection, setFollowSelection] = useState('follow')
|
||||||
const renderModeRef = useRef(renderMode)
|
const renderModeRef = useRef(renderMode)
|
||||||
const travelVisibilityRef = useRef(travelVisibility)
|
const travelVisibilityRef = useRef(travelVisibility)
|
||||||
|
const followRef = useRef({
|
||||||
|
enabled: false,
|
||||||
|
gcodePosition: null,
|
||||||
|
endLayer
|
||||||
|
})
|
||||||
renderModeRef.current = renderMode
|
renderModeRef.current = renderMode
|
||||||
travelVisibilityRef.current = travelVisibility
|
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(() => {
|
const resizePreview = useCallback(() => {
|
||||||
previewRef.current?.sceneManager?.resize()
|
previewRef.current?.sceneManager?.resize()
|
||||||
@ -154,6 +203,9 @@ function GCodePreviewUI(props) {
|
|||||||
controls.addEventListener('start', handleControlsStart)
|
controls.addEventListener('start', handleControlsStart)
|
||||||
controls.addEventListener('end', handleControlsEnd)
|
controls.addEventListener('end', handleControlsEnd)
|
||||||
|
|
||||||
|
instance.onJobUpdated = () => {
|
||||||
|
applyFollowEndLayer(instance)
|
||||||
|
}
|
||||||
instance.sceneManager.resize()
|
instance.sceneManager.resize()
|
||||||
setPreview(instance)
|
setPreview(instance)
|
||||||
|
|
||||||
@ -166,6 +218,7 @@ function GCodePreviewUI(props) {
|
|||||||
window.addEventListener('resize', resizePreview)
|
window.addEventListener('resize', resizePreview)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
instance.onJobUpdated = undefined
|
||||||
resizeObserver?.disconnect()
|
resizeObserver?.disconnect()
|
||||||
window.removeEventListener('resize', resizePreview)
|
window.removeEventListener('resize', resizePreview)
|
||||||
controls.removeEventListener('change', handleControlsChange)
|
controls.removeEventListener('change', handleControlsChange)
|
||||||
@ -189,7 +242,8 @@ function GCodePreviewUI(props) {
|
|||||||
lineWidth,
|
lineWidth,
|
||||||
startLayer,
|
startLayer,
|
||||||
topLayerColor,
|
topLayerColor,
|
||||||
resizePreview
|
resizePreview,
|
||||||
|
applyFollowEndLayer
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -275,6 +329,7 @@ function GCodePreviewUI(props) {
|
|||||||
if (typeof endLayer === 'number') {
|
if (typeof endLayer === 'number') {
|
||||||
instance.sceneManager.endLayer = endLayer
|
instance.sceneManager.endLayer = endLayer
|
||||||
}
|
}
|
||||||
|
applyFollowEndLayer(instance)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (abortController.signal.aborted) return
|
if (abortController.signal.aborted) return
|
||||||
console.error('Failed to load G-code from src', e)
|
console.error('Failed to load G-code from src', e)
|
||||||
@ -295,7 +350,18 @@ function GCodePreviewUI(props) {
|
|||||||
lineWidth,
|
lineWidth,
|
||||||
src,
|
src,
|
||||||
startLayer,
|
startLayer,
|
||||||
topLayerColor
|
topLayerColor,
|
||||||
|
applyFollowEndLayer
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyFollowEndLayer(previewRef.current)
|
||||||
|
}, [
|
||||||
|
applyFollowEndLayer,
|
||||||
|
followSelection,
|
||||||
|
gcodePosition,
|
||||||
|
preview,
|
||||||
|
showFollowCurrentSubJob
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -364,6 +430,18 @@ function GCodePreviewUI(props) {
|
|||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
onChange={setTravelVisibility}
|
onChange={setTravelVisibility}
|
||||||
/>
|
/>
|
||||||
|
{showFollowCurrentSubJob ? (
|
||||||
|
<Select
|
||||||
|
value={followSelection}
|
||||||
|
options={[
|
||||||
|
{ value: 'follow', label: 'Follow current sub job' },
|
||||||
|
{ value: 'file', label: 'Entire file' }
|
||||||
|
]}
|
||||||
|
style={{ minWidth: 190 }}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
onChange={setFollowSelection}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -432,7 +510,17 @@ GCodePreviewUI.propTypes = {
|
|||||||
startLayer: PropTypes.number,
|
startLayer: PropTypes.number,
|
||||||
endLayer: PropTypes.number,
|
endLayer: PropTypes.number,
|
||||||
lineWidth: PropTypes.number,
|
lineWidth: PropTypes.number,
|
||||||
style: PropTypes.object
|
style: PropTypes.object,
|
||||||
|
showFollowCurrentSubJob: PropTypes.bool,
|
||||||
|
gcodePosition: PropTypes.shape({
|
||||||
|
position: PropTypes.arrayOf(PropTypes.number),
|
||||||
|
filePosition: PropTypes.number,
|
||||||
|
fileSize: PropTypes.number,
|
||||||
|
progress: PropTypes.number,
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
currentLayer: PropTypes.number,
|
||||||
|
totalLayer: PropTypes.number
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GCodePreviewUI
|
export default GCodePreviewUI
|
||||||
|
|||||||
204
src/components/Dashboard/common/PrinterGCodeFilePanel.jsx
Normal file
204
src/components/Dashboard/common/PrinterGCodeFilePanel.jsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import { useContext, useEffect, useRef, useState } from 'react'
|
||||||
|
import { Card, Flex, Spin } from 'antd'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import merge from 'lodash/merge'
|
||||||
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
|
import { AuthContext } from '../context/AuthContext'
|
||||||
|
import GCodePreview from './GCodePreview'
|
||||||
|
import MissingPlaceholder from './MissingPlaceholder'
|
||||||
|
import ProgressDisplay from './ProgressDisplay'
|
||||||
|
|
||||||
|
const emptyGcodePosition = {
|
||||||
|
position: [0, 0, 0, 0],
|
||||||
|
filePosition: 0,
|
||||||
|
fileSize: 0,
|
||||||
|
progress: 0,
|
||||||
|
isActive: false,
|
||||||
|
currentLayer: 0,
|
||||||
|
totalLayer: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrinterGCodeFilePanel = ({ id, subJobId, offline = true }) => {
|
||||||
|
const { subscribeToObjectEvent, connected, fetchObject, fetchFileContent } =
|
||||||
|
useContext(ApiServerContext)
|
||||||
|
const { token } = useContext(AuthContext)
|
||||||
|
const apiRef = useRef({ fetchObject, fetchFileContent })
|
||||||
|
apiRef.current = { fetchObject, fetchFileContent }
|
||||||
|
const objectUrlRef = useRef(null)
|
||||||
|
|
||||||
|
const [gcodePosition, setGcodePosition] = useState(emptyGcodePosition)
|
||||||
|
const [src, setSrc] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (offline) {
|
||||||
|
setGcodePosition(emptyGcodePosition)
|
||||||
|
}
|
||||||
|
}, [offline])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && connected == true && offline == false) {
|
||||||
|
const gcodePositionUnsubscribe = subscribeToObjectEvent(
|
||||||
|
id?.toLowerCase(),
|
||||||
|
'printer',
|
||||||
|
'gcodePosition',
|
||||||
|
(event) => {
|
||||||
|
setGcodePosition((prev) => merge({}, prev, event.data))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return () => {
|
||||||
|
if (gcodePositionUnsubscribe) gcodePositionUnsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [id, connected, subscribeToObjectEvent, offline])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
const replaceSrc = (nextSrc) => {
|
||||||
|
if (objectUrlRef.current && objectUrlRef.current !== nextSrc) {
|
||||||
|
URL.revokeObjectURL(objectUrlRef.current)
|
||||||
|
}
|
||||||
|
objectUrlRef.current = nextSrc
|
||||||
|
setSrc(nextSrc)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadGcodeFile = async () => {
|
||||||
|
replaceSrc(null)
|
||||||
|
setError(null)
|
||||||
|
if (!subJobId || token == null) {
|
||||||
|
setLoading(false)
|
||||||
|
setDownloadProgress(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setDownloadProgress({ progress: 0, message: 'Loading G-code file...' })
|
||||||
|
const subJob = await apiRef.current.fetchObject(subJobId, 'subJob')
|
||||||
|
if (cancelled) return
|
||||||
|
const gcodeFileId = subJob?.gcodeFile?._id
|
||||||
|
if (!gcodeFileId) {
|
||||||
|
setLoading(false)
|
||||||
|
setDownloadProgress(null)
|
||||||
|
setError('No G-code file.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const gcodeFile = await apiRef.current.fetchObject(
|
||||||
|
gcodeFileId,
|
||||||
|
'gcodeFile'
|
||||||
|
)
|
||||||
|
if (cancelled) return
|
||||||
|
const file = gcodeFile?.file
|
||||||
|
if (!file?._id) {
|
||||||
|
setLoading(false)
|
||||||
|
setDownloadProgress(null)
|
||||||
|
setError('No G-code file.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectUrl = await apiRef.current.fetchFileContent(
|
||||||
|
file,
|
||||||
|
false,
|
||||||
|
(progress) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setDownloadProgress(progress)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (cancelled) {
|
||||||
|
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!objectUrl) {
|
||||||
|
setLoading(false)
|
||||||
|
setDownloadProgress(null)
|
||||||
|
setError('Failed to load G-code file.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
replaceSrc(objectUrl)
|
||||||
|
setDownloadProgress(null)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGcodeFile()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [subJobId, token])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (objectUrlRef.current) {
|
||||||
|
URL.revokeObjectURL(objectUrlRef.current)
|
||||||
|
objectUrlRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!subJobId) {
|
||||||
|
return (
|
||||||
|
<MissingPlaceholder
|
||||||
|
message={'No sub job.'}
|
||||||
|
padding='24px'
|
||||||
|
hasBackground={false}
|
||||||
|
mutedBorder={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<MissingPlaceholder
|
||||||
|
message={error}
|
||||||
|
padding='24px'
|
||||||
|
hasBackground={false}
|
||||||
|
mutedBorder={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
{loading || !src ? (
|
||||||
|
<Flex align='center' justify='center' style={{ minHeight: 240 }}>
|
||||||
|
{downloadProgress ? (
|
||||||
|
<div style={{ width: '100%', maxWidth: 360 }}>
|
||||||
|
<ProgressDisplay
|
||||||
|
percent={Math.round(
|
||||||
|
(Number(downloadProgress.progress) || 0) * 100
|
||||||
|
)}
|
||||||
|
status='active'
|
||||||
|
>
|
||||||
|
{downloadProgress.message || 'Downloading...'}
|
||||||
|
</ProgressDisplay>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Spin indicator={<LoadingOutlined spin />} size='large' />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<GCodePreview
|
||||||
|
src={src}
|
||||||
|
startLayer={0}
|
||||||
|
endLayer={undefined}
|
||||||
|
lineWidth={1}
|
||||||
|
showFollowCurrentSubJob={true}
|
||||||
|
gcodePosition={gcodePosition}
|
||||||
|
style={{ width: '100%', height: '60vh' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrinterGCodeFilePanel.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
subJobId: PropTypes.string,
|
||||||
|
offline: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrinterGCodeFilePanel
|
||||||
Loading…
x
Reference in New Issue
Block a user