From 49e23299dac80fdf3213d848cc0f02c900da5ac4 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 13 Sep 2026 23:06:24 +0100 Subject: [PATCH] Enhance FileInfo and GCodePreview Components with Loading State and Theme Support - Wrapped FilePreview in a Spin component to indicate loading state when fetching file data in FileInfo. - Removed hardcoded color properties from GCodePreview and implemented dynamic theme color support based on user settings. - Refactored camera and control handling in GCodePreview for improved user interaction and responsiveness. - Updated File model to include 'extension' field for better file management and sorting capabilities. --- .../Dashboard/Management/Files/FileInfo.jsx | 20 +- .../Dashboard/common/FilePreview.jsx | 2 - .../Dashboard/common/GCodePreview.jsx | 302 ++++++++- .../Dashboard/common/ThreeDPreview.jsx | 623 ++++++++++++++++-- .../Dashboard/hooks/usePinchZoom.js | 2 +- src/database/models/File.js | 29 +- 6 files changed, 902 insertions(+), 76 deletions(-) diff --git a/src/components/Dashboard/Management/Files/FileInfo.jsx b/src/components/Dashboard/Management/Files/FileInfo.jsx index 7274948e..e6f3c0be 100644 --- a/src/components/Dashboard/Management/Files/FileInfo.jsx +++ b/src/components/Dashboard/Management/Files/FileInfo.jsx @@ -206,12 +206,20 @@ const FileInfo = () => { collapseKey='preview' > {objectFormState?.objectData?._id ? ( - - - + } + > + + + + ) : ( )} diff --git a/src/components/Dashboard/common/FilePreview.jsx b/src/components/Dashboard/common/FilePreview.jsx index 55df8cf5..78c2352f 100644 --- a/src/components/Dashboard/common/FilePreview.jsx +++ b/src/components/Dashboard/common/FilePreview.jsx @@ -89,8 +89,6 @@ const FilePreview = ({ file, style = {} }) => { return ( 0 ? distance : null +} + +function getCameraScale(preview, baseDistance) { + const distance = getCameraDistance(preview) + return distance == null ? null : clampPreviewScale(baseDistance / distance) +} + +function applyCameraScale(preview, baseDistance, scale) { + const { camera, controls } = preview + const distance = getCameraDistance(preview) + if (distance == null) return + + const nextDistance = baseDistance / scale + if (Math.abs(nextDistance - distance) <= distance * 1e-4) return + + const offset = camera.position.clone().sub(controls.target) + camera.position + .copy(controls.target) + .addScaledVector(offset, nextDistance / distance) + controls.update() +} function GCodePreviewUI(props) { const { @@ -13,40 +73,107 @@ function GCodePreviewUI(props) { lineWidth, style = {} } = props + const { isDarkMode, themeConfig } = useThemeContext() + const { colorPrimary, colorWarning, colorPink } = themeConfig.token const canvasRef = useRef(null) + const previewPaneRef = useRef(null) const previewRef = useRef(null) + const baseDistanceRef = useRef(1) + const applyingScaleRef = useRef(false) + const [preview, setPreview] = useState(null) + const [previewScale, setPreviewScale] = useState(1) + const [panMode, setPanMode] = useState(false) + const [isDragging, setIsDragging] = useState(false) const resizePreview = useCallback(() => { previewRef.current?.resize() }, []) - // Ex-ref methods removed; this component is now a regular functional component - useEffect(() => { if (!canvasRef.current) return + const themeColors = getPreviewThemeColors({ + isDarkMode, + token: { colorPrimary, colorWarning, colorPink }, + topLayerColor, + lastSegmentColor + }) + previewRef.current?.dispose?.() - previewRef.current = GCodePreview.init({ + const instance = GCodePreview.init({ canvas: canvasRef.current, startLayer, endLayer, lineWidth, - topLayerColor: new THREE.Color(topLayerColor).getHex(), - lastSegmentColor: new THREE.Color(lastSegmentColor).getHex(), + 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' + ), buildVolume: { x: 250, y: 220, z: 150 }, initialCameraPosition: [0, 400, 450], allowDragNDrop: false }) + previewRef.current = instance + const { camera, controls } = instance + baseDistanceRef.current = getCameraDistance(instance) || 1 + // The camera dollies instead of scaling pixels, so the frustum has to + // cover the full zoom range. + camera.far = Math.max( + camera.far, + (baseDistanceRef.current / MIN_PREVIEW_SCALE) * 2 + ) + camera.updateProjectionMatrix() + controls.minDistance = baseDistanceRef.current / MAX_PREVIEW_SCALE + controls.maxDistance = baseDistanceRef.current / MIN_PREVIEW_SCALE + + const handleControlsChange = () => { + if (applyingScaleRef.current) return + const scale = getCameraScale(instance, baseDistanceRef.current) + if (scale == null) return + setPreviewScale((prev) => (Math.abs(prev - scale) < 0.001 ? prev : scale)) + } + const handleControlsStart = () => setIsDragging(true) + const handleControlsEnd = () => setIsDragging(false) + + controls.addEventListener('change', handleControlsChange) + controls.addEventListener('start', handleControlsStart) + controls.addEventListener('end', handleControlsEnd) + + instance.resize() + setPreview(instance) + + const pane = previewPaneRef.current + const resizeObserver = + pane && typeof ResizeObserver === 'function' + ? new ResizeObserver(resizePreview) + : null + resizeObserver?.observe(pane) window.addEventListener('resize', resizePreview) return () => { + resizeObserver?.disconnect() window.removeEventListener('resize', resizePreview) - previewRef.current?.dispose?.() - previewRef.current = null + controls.removeEventListener('change', handleControlsChange) + controls.removeEventListener('start', handleControlsStart) + controls.removeEventListener('end', handleControlsEnd) + setPreview(null) + setIsDragging(false) + instance.dispose?.() + if (previewRef.current === instance) { + previewRef.current = null + } } }, [ + colorPink, + colorPrimary, + colorWarning, endLayer, + isDarkMode, lastSegmentColor, lineWidth, startLayer, @@ -54,17 +181,49 @@ function GCodePreviewUI(props) { resizePreview ]) + useEffect(() => { + if (!preview) return + applyingScaleRef.current = true + applyCameraScale(preview, baseDistanceRef.current, previewScale) + applyingScaleRef.current = false + }, [preview, previewScale]) + + useEffect(() => { + if (!preview) return + preview.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 = { + ONE: panMode ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE, + TWO: THREE.TOUCH.DOLLY_PAN + } + }, [preview, panMode]) + + usePinchZoom({ + containerRef: previewPaneRef, + scale: previewScale, + onScaleChange: setPreviewScale, + enabled: preview != null + }) + + const resetView = useCallback(() => { + preview?.controls.reset() + setPreviewScale(1) + }, [preview]) + useEffect(() => { let cancelled = false const loadFromSrc = async () => { - const preview = previewRef.current - if (!src || !preview) return + const instance = previewRef.current + if (!src || !instance) return try { const response = await fetch(src) const text = await response.text() - if (cancelled || previewRef.current !== preview) return - preview.processGCode(text) + if (cancelled || previewRef.current !== instance) return + instance.processGCode(text) } catch (e) { if (cancelled) return console.error('Failed to load G-code from src', e) @@ -75,9 +234,124 @@ function GCodePreviewUI(props) { return () => { cancelled = true } - }, [endLayer, lastSegmentColor, lineWidth, src, startLayer, topLayerColor]) + }, [ + colorPink, + colorPrimary, + colorWarning, + endLayer, + isDarkMode, + lastSegmentColor, + lineWidth, + src, + startLayer, + topLayerColor + ]) - return + return ( + + + { + setPanMode((prev) => !prev) + }} + > + + +