Update gcode-preview Dependency and Refactor GCodePreview Component
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Upgraded gcode-preview package from version 2.18.0 to 3.0.0-alpha.6, incorporating breaking changes and new features.
- Refactored GCodePreview component to align with the updated API, including changes to color handling and camera management.
- Improved rendering logic to utilize sceneManager for better performance and maintainability.
- Enhanced user interaction by updating event handling and resizing logic in the GCodePreview component.
This commit is contained in:
Tom Butcher 2026-09-14 10:21:28 +01:00
parent 5dca5ef42f
commit a8932f1e66
4 changed files with 88 additions and 67 deletions

View File

@ -56,7 +56,7 @@
"echarts": "^6.1.0", "echarts": "^6.1.0",
"echarts-for-react": "^3.0.6", "echarts-for-react": "^3.0.6",
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"gcode-preview": "^2.18.0", "gcode-preview": "3.0.0-alpha.6",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
"keycloak-js": "^26.2.0", "keycloak-js": "^26.2.0",
"lodash": "^4.17.23", "lodash": "^4.17.23",
@ -1500,7 +1500,7 @@
"galactus": ["galactus@1.0.0", "", { "dependencies": { "debug": "^4.3.4", "flora-colossus": "^2.0.0", "fs-extra": "^10.1.0" } }, "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ=="], "galactus": ["galactus@1.0.0", "", { "dependencies": { "debug": "^4.3.4", "flora-colossus": "^2.0.0", "fs-extra": "^10.1.0" } }, "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ=="],
"gcode-preview": ["gcode-preview@2.18.0", "", { "dependencies": { "lil-gui": "0.19.2", "three": "0.159.0" } }, "sha512-uc9QYciG6ES/A6BWJpUZk4fHxCPvt5EnvDhHIHDbNdR/m3f9VkGvpSMh9HDygXjAXX0x1Lbz/e9ZGlIrYNB29A=="], "gcode-preview": ["gcode-preview@3.0.0-alpha.6", "", { "dependencies": { "lil-gui": "^0.20.0", "three": ">=0.166.0 <0.186.0" } }, "sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ=="],
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
@ -1748,7 +1748,7 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "1.2.1", "type-check": "0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "1.2.1", "type-check": "0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lil-gui": ["lil-gui@0.19.2", "", {}, "sha512-nU8j4ND702ouGfQZoaTN4dfXxacvGOAVK0DtmZBVcUYUAeYQXLQAjAN50igMHiba3T5jZyKEjXZU+Ntm1Qs6ZQ=="], "lil-gui": ["lil-gui@0.20.0", "", {}, "sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@ -2894,8 +2894,6 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gcode-preview/three": ["three@0.159.0", "", {}, "sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA=="],
"get-package-info/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "get-package-info/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"get-source/data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], "get-source/data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],

View File

@ -60,7 +60,7 @@
"echarts": "^6.1.0", "echarts": "^6.1.0",
"echarts-for-react": "^3.0.6", "echarts-for-react": "^3.0.6",
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"gcode-preview": "^2.18.0", "gcode-preview": "3.0.0-alpha.6",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
"keycloak-js": "^26.2.0", "keycloak-js": "^26.2.0",
"lodash": "^4.17.23", "lodash": "^4.17.23",

25
pnpm-lock.yaml generated
View File

@ -170,8 +170,8 @@ importers:
specifier: ^11.0.2 specifier: ^11.0.2
version: 11.0.2 version: 11.0.2
gcode-preview: gcode-preview:
specifier: ^2.18.0 specifier: 3.0.0-alpha.6
version: 2.18.0 version: 3.0.0-alpha.6
json-schema-traverse: json-schema-traverse:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -3798,8 +3798,8 @@ packages:
resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
gcode-preview@2.18.0: gcode-preview@3.0.0-alpha.6:
resolution: {integrity: sha512-uc9QYciG6ES/A6BWJpUZk4fHxCPvt5EnvDhHIHDbNdR/m3f9VkGvpSMh9HDygXjAXX0x1Lbz/e9ZGlIrYNB29A==} resolution: {integrity: sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ==}
generator-function@2.0.1: generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
@ -4325,8 +4325,8 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
lil-gui@0.19.2: lil-gui@0.20.0:
resolution: {integrity: sha512-nU8j4ND702ouGfQZoaTN4dfXxacvGOAVK0DtmZBVcUYUAeYQXLQAjAN50igMHiba3T5jZyKEjXZU+Ntm1Qs6ZQ==} resolution: {integrity: sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA==}
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -6134,9 +6134,6 @@ packages:
text-table@0.2.0: text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
three@0.159.0:
resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==}
three@0.165.0: three@0.165.0:
resolution: {integrity: sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==} resolution: {integrity: sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==}
@ -10857,10 +10854,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
gcode-preview@2.18.0: gcode-preview@3.0.0-alpha.6:
dependencies: dependencies:
lil-gui: 0.19.2 lil-gui: 0.20.0
three: 0.159.0 three: 0.179.1
generator-function@2.0.1: {} generator-function@2.0.1: {}
@ -11443,7 +11440,7 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 type-check: 0.4.0
lil-gui@0.19.2: {} lil-gui@0.20.0: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
@ -13813,8 +13810,6 @@ snapshots:
text-table@0.2.0: {} text-table@0.2.0: {}
three@0.159.0: {}
three@0.165.0: {} three@0.165.0: {}
three@0.176.0: {} three@0.176.0: {}

View File

@ -1,4 +1,4 @@
import * as GCodePreview from 'gcode-preview' import { GCodePreview } from 'gcode-preview'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Button, Card, Flex, Select, Slider } from 'antd' import { Button, Card, Flex, Select, Slider } from 'antd'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
@ -15,11 +15,11 @@ import usePinchZoom, {
} from '../hooks/usePinchZoom.js' } from '../hooks/usePinchZoom.js'
import { useThemeContext } from '../context/ThemeContext' import { useThemeContext } from '../context/ThemeContext'
function toThreeColorHex(color, fallback) { function toCssColor(color, fallback) {
try { try {
return new THREE.Color(color || fallback).getHex() return `#${new THREE.Color(color || fallback).getHexString()}`
} catch { } catch {
return new THREE.Color(fallback).getHex() return fallback
} }
} }
@ -38,19 +38,21 @@ function getPreviewThemeColors({
} }
} }
function getCameraDistance(preview) { function getCameraDistance(sceneManager) {
const distance = preview.camera.position.distanceTo(preview.controls.target) const distance = sceneManager.camera.position.distanceTo(
sceneManager.controls.target
)
return Number.isFinite(distance) && distance > 0 ? distance : null return Number.isFinite(distance) && distance > 0 ? distance : null
} }
function getCameraScale(preview, baseDistance) { function getCameraScale(sceneManager, baseDistance) {
const distance = getCameraDistance(preview) const distance = getCameraDistance(sceneManager)
return distance == null ? null : clampPreviewScale(baseDistance / distance) return distance == null ? null : clampPreviewScale(baseDistance / distance)
} }
function applyCameraScale(preview, baseDistance, scale) { function applyCameraScale(sceneManager, baseDistance, scale) {
const { camera, controls } = preview const { camera, controls } = sceneManager
const distance = getCameraDistance(preview) const distance = getCameraDistance(sceneManager)
if (distance == null) return if (distance == null) return
const nextDistance = baseDistance / scale const nextDistance = baseDistance / scale
@ -92,7 +94,7 @@ function GCodePreviewUI(props) {
travelVisibilityRef.current = travelVisibility travelVisibilityRef.current = travelVisibility
const resizePreview = useCallback(() => { const resizePreview = useCallback(() => {
previewRef.current?.resize() previewRef.current?.sceneManager?.resize()
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -105,30 +107,27 @@ function GCodePreviewUI(props) {
lastSegmentColor lastSegmentColor
}) })
previewRef.current?.dispose?.() previewRef.current?.dispose()
const instance = GCodePreview.init({ const instance = new GCodePreview({
canvas: canvasRef.current, canvas: canvasRef.current,
startLayer, startLayer,
endLayer, endLayer,
lineWidth, lineWidth,
backgroundColor: toThreeColorHex(themeColors.backgroundColor, '#ffffff'), backgroundColor: toCssColor(themeColors.backgroundColor, '#ffffff'),
extrusionColor: toThreeColorHex(themeColors.extrusionColor, '#0091FF'), extrusionColor: toCssColor(themeColors.extrusionColor, '#0091ff'),
travelColor: toThreeColorHex(themeColors.travelColor, '#8c8c8c'), travelColor: toCssColor(themeColors.travelColor, '#8c8c8c'),
topLayerColor: toThreeColorHex(themeColors.topLayerColor, '#FF9230'), topLayerColor: toCssColor(themeColors.topLayerColor, '#ff9230'),
lastSegmentColor: toThreeColorHex( lastSegmentColor: toCssColor(themeColors.lastSegmentColor, '#ff69b4'),
themeColors.lastSegmentColor,
'#FF69B4'
),
buildVolume: { x: 250, y: 220, z: 150 }, buildVolume: { x: 250, y: 220, z: 150 },
initialCameraPosition: [0, 400, 450], initialCameraPosition: [0, 400, 450],
allowDragNDrop: false droppable: false,
renderTubes: renderModeRef.current === 'pipes',
renderTravel: travelVisibilityRef.current === 'show'
}) })
instance.renderTubes = renderModeRef.current === 'pipes'
instance.renderTravel = travelVisibilityRef.current === 'show'
previewRef.current = instance previewRef.current = instance
const { camera, controls } = instance const { camera, controls } = instance.sceneManager
baseDistanceRef.current = getCameraDistance(instance) || 1 baseDistanceRef.current = getCameraDistance(instance.sceneManager) || 1
// The camera dollies instead of scaling pixels, so the frustum has to // The camera dollies instead of scaling pixels, so the frustum has to
// cover the full zoom range. // cover the full zoom range.
camera.far = Math.max( camera.far = Math.max(
@ -141,7 +140,10 @@ function GCodePreviewUI(props) {
const handleControlsChange = () => { const handleControlsChange = () => {
if (applyingScaleRef.current) return if (applyingScaleRef.current) return
const scale = getCameraScale(instance, baseDistanceRef.current) const scale = getCameraScale(
instance.sceneManager,
baseDistanceRef.current
)
if (scale == null) return if (scale == null) return
setPreviewScale((prev) => (Math.abs(prev - scale) < 0.001 ? prev : scale)) setPreviewScale((prev) => (Math.abs(prev - scale) < 0.001 ? prev : scale))
} }
@ -152,7 +154,7 @@ function GCodePreviewUI(props) {
controls.addEventListener('start', handleControlsStart) controls.addEventListener('start', handleControlsStart)
controls.addEventListener('end', handleControlsEnd) controls.addEventListener('end', handleControlsEnd)
instance.resize() instance.sceneManager.resize()
setPreview(instance) setPreview(instance)
const pane = previewPaneRef.current const pane = previewPaneRef.current
@ -171,7 +173,8 @@ function GCodePreviewUI(props) {
controls.removeEventListener('end', handleControlsEnd) controls.removeEventListener('end', handleControlsEnd)
setPreview(null) setPreview(null)
setIsDragging(false) setIsDragging(false)
instance.dispose?.() instance.clear()
instance.dispose()
if (previewRef.current === instance) { if (previewRef.current === instance) {
previewRef.current = null previewRef.current = null
} }
@ -192,33 +195,39 @@ function GCodePreviewUI(props) {
useEffect(() => { useEffect(() => {
if (!preview) return if (!preview) return
applyingScaleRef.current = true applyingScaleRef.current = true
applyCameraScale(preview, baseDistanceRef.current, previewScale) applyCameraScale(
preview.sceneManager,
baseDistanceRef.current,
previewScale
)
applyingScaleRef.current = false applyingScaleRef.current = false
}, [preview, previewScale]) }, [preview, previewScale])
useEffect(() => { useEffect(() => {
if (!preview) return if (!preview) return
const { sceneManager } = preview
const renderTubes = renderMode === 'pipes' const renderTubes = renderMode === 'pipes'
const renderTravel = travelVisibility === 'show' const renderTravel = travelVisibility === 'show'
if ( if (
preview.renderTubes === renderTubes && sceneManager.renderTubes === renderTubes &&
preview.renderTravel === renderTravel sceneManager.renderTravel === renderTravel
) { ) {
return return
} }
preview.renderTubes = renderTubes sceneManager.renderTubes = renderTubes
preview.renderTravel = renderTravel sceneManager.renderTravel = renderTravel
preview.render() sceneManager.render()
}, [preview, renderMode, travelVisibility]) }, [preview, renderMode, travelVisibility])
useEffect(() => { useEffect(() => {
if (!preview) return if (!preview) return
preview.controls.mouseButtons = { const { controls } = preview.sceneManager
controls.mouseButtons = {
LEFT: panMode ? THREE.MOUSE.PAN : THREE.MOUSE.ROTATE, LEFT: panMode ? THREE.MOUSE.PAN : THREE.MOUSE.ROTATE,
MIDDLE: THREE.MOUSE.DOLLY, MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: panMode ? THREE.MOUSE.ROTATE : THREE.MOUSE.PAN RIGHT: panMode ? THREE.MOUSE.ROTATE : THREE.MOUSE.PAN
} }
preview.controls.touches = { controls.touches = {
ONE: panMode ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE, ONE: panMode ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN TWO: THREE.TOUCH.DOLLY_PAN
} }
@ -232,30 +241,49 @@ function GCodePreviewUI(props) {
}) })
const resetView = useCallback(() => { const resetView = useCallback(() => {
preview?.controls.reset() preview?.sceneManager.controls.reset()
setPreviewScale(1) setPreviewScale(1)
}, [preview]) }, [preview])
useEffect(() => { useEffect(() => {
let cancelled = false const abortController = new AbortController()
const loadFromSrc = async () => { const loadFromSrc = async () => {
const instance = previewRef.current const instance = previewRef.current
if (!src || !instance) return if (!src || !instance) return
try { try {
const response = await fetch(src) const response = await fetch(src, { signal: abortController.signal })
const text = await response.text() if (!response.ok || !response.body) {
if (cancelled || previewRef.current !== instance) return throw new Error(`Failed to load G-code (${response.status})`)
instance.processGCode(text) }
if (abortController.signal.aborted || previewRef.current !== instance) {
return
}
if (instance.countLayers > 0) {
instance.clear()
instance.sceneManager.startLayer = startLayer
instance.sceneManager.endLayer = endLayer
}
const stream = response.body.pipeThrough(new TextDecoderStream())
await instance.processGCodeStream(stream)
if (abortController.signal.aborted || previewRef.current !== instance) {
return
}
if (typeof startLayer === 'number') {
instance.sceneManager.startLayer = startLayer
}
if (typeof endLayer === 'number') {
instance.sceneManager.endLayer = endLayer
}
} catch (e) { } catch (e) {
if (cancelled) 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)
} }
} }
loadFromSrc() loadFromSrc()
return () => { return () => {
cancelled = true abortController.abort()
} }
}, [ }, [
colorPink, colorPink,