farmcontrol-ui/src/components/Dashboard/common/PrinterGCodeFilePanel.jsx
Tom Butcher 608eba41cf 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.
2026-11-07 23:17:02 +00:00

205 lines
5.2 KiB
JavaScript

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