diff --git a/bun.lock b/bun.lock index 0e4e1261..4a87bd3d 100644 --- a/bun.lock +++ b/bun.lock @@ -56,7 +56,7 @@ "echarts": "^6.1.0", "echarts-for-react": "^3.0.6", "electron-store": "^11.0.2", - "gcode-preview": "^2.18.0", + "gcode-preview": "3.0.0-alpha.6", "json-schema-traverse": "^1.0.0", "keycloak-js": "^26.2.0", "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=="], - "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=="], @@ -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=="], - "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=="], @@ -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=="], - "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-source/data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], diff --git a/package.json b/package.json index dfc43864..9b11c850 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "echarts": "^6.1.0", "echarts-for-react": "^3.0.6", "electron-store": "^11.0.2", - "gcode-preview": "^2.18.0", + "gcode-preview": "3.0.0-alpha.6", "json-schema-traverse": "^1.0.0", "keycloak-js": "^26.2.0", "lodash": "^4.17.23", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b53881b1..b593e65d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,8 +170,8 @@ importers: specifier: ^11.0.2 version: 11.0.2 gcode-preview: - specifier: ^2.18.0 - version: 2.18.0 + specifier: 3.0.0-alpha.6 + version: 3.0.0-alpha.6 json-schema-traverse: specifier: ^1.0.0 version: 1.0.0 @@ -3798,8 +3798,8 @@ packages: resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} engines: {node: '>= 12'} - gcode-preview@2.18.0: - resolution: {integrity: sha512-uc9QYciG6ES/A6BWJpUZk4fHxCPvt5EnvDhHIHDbNdR/m3f9VkGvpSMh9HDygXjAXX0x1Lbz/e9ZGlIrYNB29A==} + gcode-preview@3.0.0-alpha.6: + resolution: {integrity: sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ==} generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} @@ -4325,8 +4325,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lil-gui@0.19.2: - resolution: {integrity: sha512-nU8j4ND702ouGfQZoaTN4dfXxacvGOAVK0DtmZBVcUYUAeYQXLQAjAN50igMHiba3T5jZyKEjXZU+Ntm1Qs6ZQ==} + lil-gui@0.20.0: + resolution: {integrity: sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA==} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -6134,9 +6134,6 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - three@0.159.0: - resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==} - three@0.165.0: resolution: {integrity: sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==} @@ -10857,10 +10854,10 @@ snapshots: transitivePeerDependencies: - supports-color - gcode-preview@2.18.0: + gcode-preview@3.0.0-alpha.6: dependencies: - lil-gui: 0.19.2 - three: 0.159.0 + lil-gui: 0.20.0 + three: 0.179.1 generator-function@2.0.1: {} @@ -11443,7 +11440,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lil-gui@0.19.2: {} + lil-gui@0.20.0: {} lines-and-columns@1.2.4: {} @@ -13813,8 +13810,6 @@ snapshots: text-table@0.2.0: {} - three@0.159.0: {} - three@0.165.0: {} three@0.176.0: {} diff --git a/src/components/Dashboard/common/GCodePreview.jsx b/src/components/Dashboard/common/GCodePreview.jsx index baa58f53..285b1d9c 100644 --- a/src/components/Dashboard/common/GCodePreview.jsx +++ b/src/components/Dashboard/common/GCodePreview.jsx @@ -1,4 +1,4 @@ -import * as GCodePreview from 'gcode-preview' +import { GCodePreview } from 'gcode-preview' import PropTypes from 'prop-types' import { Button, Card, Flex, Select, Slider } from 'antd' import { useCallback, useEffect, useRef, useState } from 'react' @@ -15,11 +15,11 @@ import usePinchZoom, { } from '../hooks/usePinchZoom.js' import { useThemeContext } from '../context/ThemeContext' -function toThreeColorHex(color, fallback) { +function toCssColor(color, fallback) { try { - return new THREE.Color(color || fallback).getHex() + return `#${new THREE.Color(color || fallback).getHexString()}` } catch { - return new THREE.Color(fallback).getHex() + return fallback } } @@ -38,19 +38,21 @@ function getPreviewThemeColors({ } } -function getCameraDistance(preview) { - const distance = preview.camera.position.distanceTo(preview.controls.target) +function getCameraDistance(sceneManager) { + const distance = sceneManager.camera.position.distanceTo( + sceneManager.controls.target + ) return Number.isFinite(distance) && distance > 0 ? distance : null } -function getCameraScale(preview, baseDistance) { - const distance = getCameraDistance(preview) +function getCameraScale(sceneManager, baseDistance) { + const distance = getCameraDistance(sceneManager) return distance == null ? null : clampPreviewScale(baseDistance / distance) } -function applyCameraScale(preview, baseDistance, scale) { - const { camera, controls } = preview - const distance = getCameraDistance(preview) +function applyCameraScale(sceneManager, baseDistance, scale) { + const { camera, controls } = sceneManager + const distance = getCameraDistance(sceneManager) if (distance == null) return const nextDistance = baseDistance / scale @@ -92,7 +94,7 @@ function GCodePreviewUI(props) { travelVisibilityRef.current = travelVisibility const resizePreview = useCallback(() => { - previewRef.current?.resize() + previewRef.current?.sceneManager?.resize() }, []) useEffect(() => { @@ -105,30 +107,27 @@ function GCodePreviewUI(props) { lastSegmentColor }) - previewRef.current?.dispose?.() - const instance = GCodePreview.init({ + previewRef.current?.dispose() + const instance = new GCodePreview({ canvas: canvasRef.current, startLayer, endLayer, lineWidth, - backgroundColor: toThreeColorHex(themeColors.backgroundColor, '#ffffff'), - extrusionColor: toThreeColorHex(themeColors.extrusionColor, '#0091FF'), - travelColor: toThreeColorHex(themeColors.travelColor, '#8c8c8c'), - topLayerColor: toThreeColorHex(themeColors.topLayerColor, '#FF9230'), - lastSegmentColor: toThreeColorHex( - themeColors.lastSegmentColor, - '#FF69B4' - ), + backgroundColor: toCssColor(themeColors.backgroundColor, '#ffffff'), + extrusionColor: toCssColor(themeColors.extrusionColor, '#0091ff'), + travelColor: toCssColor(themeColors.travelColor, '#8c8c8c'), + topLayerColor: toCssColor(themeColors.topLayerColor, '#ff9230'), + lastSegmentColor: toCssColor(themeColors.lastSegmentColor, '#ff69b4'), buildVolume: { x: 250, y: 220, z: 150 }, 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 - const { camera, controls } = instance - baseDistanceRef.current = getCameraDistance(instance) || 1 + const { camera, controls } = instance.sceneManager + baseDistanceRef.current = getCameraDistance(instance.sceneManager) || 1 // The camera dollies instead of scaling pixels, so the frustum has to // cover the full zoom range. camera.far = Math.max( @@ -141,7 +140,10 @@ function GCodePreviewUI(props) { const handleControlsChange = () => { if (applyingScaleRef.current) return - const scale = getCameraScale(instance, baseDistanceRef.current) + const scale = getCameraScale( + instance.sceneManager, + baseDistanceRef.current + ) if (scale == null) return setPreviewScale((prev) => (Math.abs(prev - scale) < 0.001 ? prev : scale)) } @@ -152,7 +154,7 @@ function GCodePreviewUI(props) { controls.addEventListener('start', handleControlsStart) controls.addEventListener('end', handleControlsEnd) - instance.resize() + instance.sceneManager.resize() setPreview(instance) const pane = previewPaneRef.current @@ -171,7 +173,8 @@ function GCodePreviewUI(props) { controls.removeEventListener('end', handleControlsEnd) setPreview(null) setIsDragging(false) - instance.dispose?.() + instance.clear() + instance.dispose() if (previewRef.current === instance) { previewRef.current = null } @@ -192,33 +195,39 @@ function GCodePreviewUI(props) { useEffect(() => { if (!preview) return applyingScaleRef.current = true - applyCameraScale(preview, baseDistanceRef.current, previewScale) + applyCameraScale( + preview.sceneManager, + baseDistanceRef.current, + previewScale + ) applyingScaleRef.current = false }, [preview, previewScale]) useEffect(() => { if (!preview) return + const { sceneManager } = preview const renderTubes = renderMode === 'pipes' const renderTravel = travelVisibility === 'show' if ( - preview.renderTubes === renderTubes && - preview.renderTravel === renderTravel + sceneManager.renderTubes === renderTubes && + sceneManager.renderTravel === renderTravel ) { return } - preview.renderTubes = renderTubes - preview.renderTravel = renderTravel - preview.render() + sceneManager.renderTubes = renderTubes + sceneManager.renderTravel = renderTravel + sceneManager.render() }, [preview, renderMode, travelVisibility]) useEffect(() => { if (!preview) return - preview.controls.mouseButtons = { + const { controls } = preview.sceneManager + controls.mouseButtons = { LEFT: panMode ? THREE.MOUSE.PAN : THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: panMode ? THREE.MOUSE.ROTATE : THREE.MOUSE.PAN } - preview.controls.touches = { + controls.touches = { ONE: panMode ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN } @@ -232,30 +241,49 @@ function GCodePreviewUI(props) { }) const resetView = useCallback(() => { - preview?.controls.reset() + preview?.sceneManager.controls.reset() setPreviewScale(1) }, [preview]) useEffect(() => { - let cancelled = false + const abortController = new AbortController() const loadFromSrc = async () => { const instance = previewRef.current if (!src || !instance) return try { - const response = await fetch(src) - const text = await response.text() - if (cancelled || previewRef.current !== instance) return - instance.processGCode(text) + const response = await fetch(src, { signal: abortController.signal }) + if (!response.ok || !response.body) { + throw new Error(`Failed to load G-code (${response.status})`) + } + 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) { - if (cancelled) return + if (abortController.signal.aborted) return console.error('Failed to load G-code from src', e) } } loadFromSrc() return () => { - cancelled = true + abortController.abort() } }, [ colorPink,