Fix React/Konva DOM conflicts and zoom reset issues

- Separate Konva container from React-managed overlays to prevent removeChild errors
- Move wheel zoom handler into stage initialization to maintain stable reference
- Remove zoomLevel from background effect dependencies to prevent zoom reset
- Add double-initialization guard for stage creation
- Clear refs before stage destruction to prevent race conditions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik 2025-12-05 22:34:10 +01:00
parent 0f40cec8ec
commit 3c8c2d49fd

View file

@ -36,6 +36,9 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
// Prevent double initialization
if (stageRef.current) return;
const container = containerRef.current; const container = containerRef.current;
// Create stage // Create stage
@ -78,7 +81,50 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
stage.container().style.cursor = 'grab'; stage.container().style.cursor = 'grab';
}); });
// Mouse wheel zoom handler
const handleWheel = (e: Konva.KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
if (!pointer) return;
const scaleBy = 1.1;
const direction = e.evt.deltaY > 0 ? -1 : 1;
let newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
// Apply constraints
newScale = Math.max(0.1, Math.min(10, newScale));
// Zoom towards pointer
const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
const newPos = {
x: pointer.x - mousePointTo.x * newScale,
y: pointer.y - mousePointTo.y * newScale,
};
stage.scale({ x: newScale, y: newScale });
stage.position(newPos);
setZoomLevel(newScale);
stage.batchDraw();
};
// Attach wheel event
stage.on('wheel', handleWheel);
return () => { return () => {
// Clear refs before destroying to prevent race conditions
stageRef.current = null;
backgroundLayerRef.current = null;
patternLayerRef.current = null;
currentPosLayerRef.current = null;
patternGroupRef.current = null;
// Destroy the stage (this removes the canvas from DOM)
stage.destroy(); stage.destroy();
}; };
}, []); }, []);
@ -116,53 +162,6 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
return () => resizeObserver.disconnect(); return () => resizeObserver.disconnect();
}, []); }, []);
// Mouse wheel zoom handler
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault();
const stage = e.target.getStage();
if (!stage) return;
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
if (!pointer) return;
const scaleBy = 1.1;
const direction = e.evt.deltaY > 0 ? -1 : 1;
let newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
// Apply constraints
newScale = Math.max(0.1, Math.min(10, newScale));
// Zoom towards pointer
const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
const newPos = {
x: pointer.x - mousePointTo.x * newScale,
y: pointer.y - mousePointTo.y * newScale,
};
stage.scale({ x: newScale, y: newScale });
stage.position(newPos);
setZoomLevel(newScale);
stage.batchDraw();
}, []);
// Attach wheel event handler
useEffect(() => {
const stage = stageRef.current;
if (!stage) return;
stage.on('wheel', handleWheel);
return () => {
stage.off('wheel', handleWheel);
};
}, [handleWheel]);
// Helper function to zoom to a specific point // Helper function to zoom to a specific point
const zoomToPoint = useCallback( const zoomToPoint = useCallback(
(point: { x: number; y: number }, newScale: number) => { (point: { x: number; y: number }, newScale: number) => {
@ -246,10 +245,12 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
const initialScale = calculateInitialScale(stage.width(), stage.height(), viewWidth, viewHeight); const initialScale = calculateInitialScale(stage.width(), stage.height(), viewWidth, viewHeight);
initialScaleRef.current = initialScale; initialScaleRef.current = initialScale;
// Always reset to initial scale when background is re-rendered (e.g., when pattern or hoop changes) // Only set initial scale if this is the first render (zoom level is still 1)
stage.scale({ x: initialScale, y: initialScale }); if (zoomLevel === 1) {
stage.position({ x: stage.width() / 2, y: stage.height() / 2 }); stage.scale({ x: initialScale, y: initialScale });
setZoomLevel(initialScale); stage.position({ x: stage.width() / 2, y: stage.height() / 2 });
setZoomLevel(initialScale);
}
// Render background elements // Render background elements
const gridSize = 100; // 10mm grid (100 units in 0.1mm) const gridSize = 100; // 10mm grid (100 units in 0.1mm)
@ -263,7 +264,7 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
// Cache the background layer for performance // Cache the background layer for performance
layer.cache(); layer.cache();
layer.batchDraw(); layer.batchDraw();
}, [machineInfo, pesData, zoomLevel]); }, [machineInfo, pesData]);
// Render pattern layer (stitches and bounds in a draggable group) // Render pattern layer (stitches and bounds in a draggable group)
// This effect only runs when the pattern changes, NOT when sewing progress changes // This effect only runs when the pattern changes, NOT when sewing progress changes
@ -382,12 +383,18 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
return ( return (
<div className="canvas-panel"> <div className="canvas-panel">
<h2>Pattern Preview</h2> <h2>Pattern Preview</h2>
<div className="canvas-container" ref={containerRef}> <div className="canvas-container">
{/* Konva container - separate from React-managed overlays */}
<div ref={containerRef} style={{ width: '100%', height: '100%', position: 'absolute' }} />
{/* Placeholder overlay when no pattern is loaded */}
{!pesData && ( {!pesData && (
<div className="canvas-placeholder"> <div className="canvas-placeholder">
Load a PES file to preview the pattern Load a PES file to preview the pattern
</div> </div>
)} )}
{/* Pattern info overlays */}
{pesData && ( {pesData && (
<> <>
{/* Thread Legend Overlay */} {/* Thread Legend Overlay */}