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 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 }) => {
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
title={'Filament Stock'}
|
||||
icon={<FilamentStockIcon />}
|
||||
|
||||
@ -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 ? (
|
||||
<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>
|
||||
|
||||
<div
|
||||
@ -432,7 +510,17 @@ GCodePreviewUI.propTypes = {
|
||||
startLayer: PropTypes.number,
|
||||
endLayer: 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
|
||||
|
||||
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