mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
feature: Extract canvas state management into custom React hooks
Create two specialized custom hooks for PatternCanvas: **useCanvasViewport (166 lines)** - Manages canvas zoom, pan, and container resize - Handles wheel zoom and button zoom operations - Tracks container size with ResizeObserver - Calculates initial scale when pattern changes - Returns: stagePos, stageScale, containerSize, zoom handlers **usePatternTransform (179 lines)** - Manages pattern position, rotation, and transform state - Handles drag and transform end events - Syncs local state with global pattern store - Manages transformer attachment/detachment - Returns: offsets, rotation, refs, event handlers Benefits: - Reduced PatternCanvas from 608 to 388 lines (-220 lines, -36%) - Better separation of concerns (viewport vs pattern transform) - Reusable hooks for other canvas components - Easier to test state management logic in isolation - Cleaner component with focused responsibility Combined refactoring impact (Phase 1+2+3): - Original: 786 lines in single file - After Phase 3: 388 lines main + hooks + components - Total reduction: -398 lines (-51%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b008fd3aa8
commit
99d32f9029
3 changed files with 387 additions and 257 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef, useState, useCallback } from "react";
|
import { useRef } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import {
|
import {
|
||||||
useMachineStore,
|
useMachineStore,
|
||||||
|
|
@ -7,10 +7,7 @@ import {
|
||||||
import { usePatternStore } from "../../stores/usePatternStore";
|
import { usePatternStore } from "../../stores/usePatternStore";
|
||||||
import { Stage, Layer, Group, Transformer } from "react-konva";
|
import { Stage, Layer, Group, Transformer } from "react-konva";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { KonvaEventObject } from "konva/lib/Node";
|
|
||||||
import { PhotoIcon } from "@heroicons/react/24/solid";
|
import { PhotoIcon } from "@heroicons/react/24/solid";
|
||||||
import type { PesPatternData } from "../../formats/import/pesImporter";
|
|
||||||
import { calculateInitialScale } from "../../utils/konvaRenderers";
|
|
||||||
import {
|
import {
|
||||||
Grid,
|
Grid,
|
||||||
Origin,
|
Origin,
|
||||||
|
|
@ -29,11 +26,12 @@ import {
|
||||||
import {
|
import {
|
||||||
calculatePatternCenter,
|
calculatePatternCenter,
|
||||||
convertPenStitchesToPesFormat,
|
convertPenStitchesToPesFormat,
|
||||||
calculateZoomToPoint,
|
|
||||||
} from "./patternCanvasHelpers";
|
} from "./patternCanvasHelpers";
|
||||||
import { ThreadLegend } from "./ThreadLegend";
|
import { ThreadLegend } from "./ThreadLegend";
|
||||||
import { PatternPositionIndicator } from "./PatternPositionIndicator";
|
import { PatternPositionIndicator } from "./PatternPositionIndicator";
|
||||||
import { ZoomControls } from "./ZoomControls";
|
import { ZoomControls } from "./ZoomControls";
|
||||||
|
import { useCanvasViewport } from "../../hooks/useCanvasViewport";
|
||||||
|
import { usePatternTransform } from "../../hooks/usePatternTransform";
|
||||||
|
|
||||||
export function PatternCanvas() {
|
export function PatternCanvas() {
|
||||||
// Machine store
|
// Machine store
|
||||||
|
|
@ -70,260 +68,42 @@ export function PatternCanvas() {
|
||||||
const patternUploaded = usePatternUploaded();
|
const patternUploaded = usePatternUploaded();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const stageRef = useRef<Konva.Stage | null>(null);
|
const stageRef = useRef<Konva.Stage | null>(null);
|
||||||
const patternGroupRef = useRef<Konva.Group | null>(null);
|
|
||||||
const transformerRef = useRef<Konva.Transformer | null>(null);
|
|
||||||
|
|
||||||
const [stagePos, setStagePos] = useState({ x: 0, y: 0 });
|
// Canvas viewport (zoom, pan, container size)
|
||||||
const [stageScale, setStageScale] = useState(1);
|
const {
|
||||||
const [localPatternOffset, setLocalPatternOffset] = useState(
|
stagePos,
|
||||||
initialPatternOffset || { x: 0, y: 0 },
|
stageScale,
|
||||||
);
|
containerSize,
|
||||||
const [localPatternRotation, setLocalPatternRotation] = useState(
|
handleWheel,
|
||||||
initialPatternRotation || 0,
|
handleZoomIn,
|
||||||
);
|
handleZoomOut,
|
||||||
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
handleZoomReset,
|
||||||
const initialScaleRef = useRef<number>(1);
|
} = useCanvasViewport({
|
||||||
const prevPesDataRef = useRef<PesPatternData | null>(null);
|
containerRef,
|
||||||
|
pesData,
|
||||||
|
uploadedPesData,
|
||||||
|
machineInfo,
|
||||||
|
});
|
||||||
|
|
||||||
// Update pattern offset when initialPatternOffset changes
|
// Pattern transform (position, rotation, drag/transform)
|
||||||
if (
|
const {
|
||||||
initialPatternOffset &&
|
localPatternOffset,
|
||||||
(localPatternOffset.x !== initialPatternOffset.x ||
|
localPatternRotation,
|
||||||
localPatternOffset.y !== initialPatternOffset.y)
|
patternGroupRef,
|
||||||
) {
|
transformerRef,
|
||||||
setLocalPatternOffset(initialPatternOffset);
|
attachTransformer,
|
||||||
console.log(
|
handleCenterPattern,
|
||||||
"[PatternCanvas] Restored pattern offset:",
|
handlePatternDragEnd,
|
||||||
initialPatternOffset,
|
handleTransformEnd,
|
||||||
);
|
} = usePatternTransform({
|
||||||
}
|
pesData,
|
||||||
|
initialPatternOffset,
|
||||||
// Update pattern rotation when initialPatternRotation changes
|
initialPatternRotation,
|
||||||
if (
|
setPatternOffset,
|
||||||
initialPatternRotation !== undefined &&
|
setPatternRotation,
|
||||||
localPatternRotation !== initialPatternRotation
|
patternUploaded,
|
||||||
) {
|
isUploading,
|
||||||
setLocalPatternRotation(initialPatternRotation);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Track container size
|
|
||||||
useEffect(() => {
|
|
||||||
if (!containerRef.current) return;
|
|
||||||
|
|
||||||
const updateSize = () => {
|
|
||||||
if (containerRef.current) {
|
|
||||||
const width = containerRef.current.clientWidth;
|
|
||||||
const height = containerRef.current.clientHeight;
|
|
||||||
setContainerSize({ width, height });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial size
|
|
||||||
updateSize();
|
|
||||||
|
|
||||||
// Watch for resize
|
|
||||||
const resizeObserver = new ResizeObserver(updateSize);
|
|
||||||
resizeObserver.observe(containerRef.current);
|
|
||||||
|
|
||||||
return () => resizeObserver.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Calculate and store initial scale when pattern or hoop changes
|
|
||||||
useEffect(() => {
|
|
||||||
// Use whichever pattern is available (uploaded or original)
|
|
||||||
const currentPattern = uploadedPesData || pesData;
|
|
||||||
if (!currentPattern || containerSize.width === 0) {
|
|
||||||
prevPesDataRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only recalculate if pattern changed
|
|
||||||
if (prevPesDataRef.current !== currentPattern) {
|
|
||||||
prevPesDataRef.current = currentPattern;
|
|
||||||
|
|
||||||
const { bounds } = currentPattern;
|
|
||||||
const viewWidth = machineInfo
|
|
||||||
? machineInfo.maxWidth
|
|
||||||
: bounds.maxX - bounds.minX;
|
|
||||||
const viewHeight = machineInfo
|
|
||||||
? machineInfo.maxHeight
|
|
||||||
: bounds.maxY - bounds.minY;
|
|
||||||
|
|
||||||
const initialScale = calculateInitialScale(
|
|
||||||
containerSize.width,
|
|
||||||
containerSize.height,
|
|
||||||
viewWidth,
|
|
||||||
viewHeight,
|
|
||||||
);
|
|
||||||
initialScaleRef.current = initialScale;
|
|
||||||
|
|
||||||
// Reset view when pattern changes
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setStageScale(initialScale);
|
|
||||||
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
|
||||||
}
|
|
||||||
}, [pesData, uploadedPesData, machineInfo, containerSize]);
|
|
||||||
|
|
||||||
// Wheel zoom handler
|
|
||||||
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
|
|
||||||
e.evt.preventDefault();
|
|
||||||
|
|
||||||
const stage = e.target.getStage();
|
|
||||||
if (!stage) return;
|
|
||||||
|
|
||||||
const pointer = stage.getPointerPosition();
|
|
||||||
if (!pointer) return;
|
|
||||||
|
|
||||||
const scaleBy = 1.1;
|
|
||||||
const direction = e.evt.deltaY > 0 ? -1 : 1;
|
|
||||||
|
|
||||||
setStageScale((oldScale) => {
|
|
||||||
const newScale = Math.max(
|
|
||||||
0.1,
|
|
||||||
Math.min(direction > 0 ? oldScale * scaleBy : oldScale / scaleBy, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Zoom towards pointer
|
|
||||||
setStagePos((prevPos) =>
|
|
||||||
calculateZoomToPoint(oldScale, newScale, pointer, prevPos),
|
|
||||||
);
|
|
||||||
|
|
||||||
return newScale;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Zoom control handlers
|
|
||||||
const handleZoomIn = useCallback(() => {
|
|
||||||
setStageScale((oldScale) => {
|
|
||||||
const newScale = Math.max(0.1, Math.min(oldScale * 1.2, 2));
|
|
||||||
|
|
||||||
// Zoom towards center of viewport
|
|
||||||
const center = {
|
|
||||||
x: containerSize.width / 2,
|
|
||||||
y: containerSize.height / 2,
|
|
||||||
};
|
|
||||||
setStagePos((prevPos) =>
|
|
||||||
calculateZoomToPoint(oldScale, newScale, center, prevPos),
|
|
||||||
);
|
|
||||||
|
|
||||||
return newScale;
|
|
||||||
});
|
|
||||||
}, [containerSize]);
|
|
||||||
|
|
||||||
const handleZoomOut = useCallback(() => {
|
|
||||||
setStageScale((oldScale) => {
|
|
||||||
const newScale = Math.max(0.1, Math.min(oldScale / 1.2, 2));
|
|
||||||
|
|
||||||
// Zoom towards center of viewport
|
|
||||||
const center = {
|
|
||||||
x: containerSize.width / 2,
|
|
||||||
y: containerSize.height / 2,
|
|
||||||
};
|
|
||||||
setStagePos((prevPos) =>
|
|
||||||
calculateZoomToPoint(oldScale, newScale, center, prevPos),
|
|
||||||
);
|
|
||||||
|
|
||||||
return newScale;
|
|
||||||
});
|
|
||||||
}, [containerSize]);
|
|
||||||
|
|
||||||
const handleZoomReset = useCallback(() => {
|
|
||||||
const initialScale = initialScaleRef.current;
|
|
||||||
setStageScale(initialScale);
|
|
||||||
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
|
||||||
}, [containerSize]);
|
|
||||||
|
|
||||||
const handleCenterPattern = useCallback(() => {
|
|
||||||
if (!pesData) return;
|
|
||||||
|
|
||||||
const { bounds } = pesData;
|
|
||||||
const centerOffsetX = -(bounds.minX + bounds.maxX) / 2;
|
|
||||||
const centerOffsetY = -(bounds.minY + bounds.maxY) / 2;
|
|
||||||
|
|
||||||
setLocalPatternOffset({ x: centerOffsetX, y: centerOffsetY });
|
|
||||||
setPatternOffset(centerOffsetX, centerOffsetY);
|
|
||||||
}, [pesData, setPatternOffset]);
|
|
||||||
|
|
||||||
// Pattern drag handlers
|
|
||||||
const handlePatternDragEnd = useCallback(
|
|
||||||
(e: Konva.KonvaEventObject<DragEvent>) => {
|
|
||||||
const newOffset = {
|
|
||||||
x: e.target.x(),
|
|
||||||
y: e.target.y(),
|
|
||||||
};
|
|
||||||
setLocalPatternOffset(newOffset);
|
|
||||||
setPatternOffset(newOffset.x, newOffset.y);
|
|
||||||
},
|
|
||||||
[setPatternOffset],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Attach/detach transformer based on state
|
|
||||||
const attachTransformer = useCallback(() => {
|
|
||||||
if (!transformerRef.current || !patternGroupRef.current) {
|
|
||||||
console.log(
|
|
||||||
"[PatternCanvas] Cannot attach transformer - refs not ready",
|
|
||||||
{
|
|
||||||
hasTransformer: !!transformerRef.current,
|
|
||||||
hasPatternGroup: !!patternGroupRef.current,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!patternUploaded && !isUploading) {
|
|
||||||
console.log("[PatternCanvas] Attaching transformer");
|
|
||||||
transformerRef.current.nodes([patternGroupRef.current]);
|
|
||||||
transformerRef.current.getLayer()?.batchDraw();
|
|
||||||
} else {
|
|
||||||
console.log("[PatternCanvas] Detaching transformer");
|
|
||||||
transformerRef.current.nodes([]);
|
|
||||||
}
|
|
||||||
}, [patternUploaded, isUploading]);
|
|
||||||
|
|
||||||
// Call attachTransformer when conditions change
|
|
||||||
useEffect(() => {
|
|
||||||
attachTransformer();
|
|
||||||
}, [attachTransformer, pesData]);
|
|
||||||
|
|
||||||
// Sync node rotation with state (important for when rotation is reset to 0 after upload)
|
|
||||||
useEffect(() => {
|
|
||||||
if (patternGroupRef.current) {
|
|
||||||
patternGroupRef.current.rotation(localPatternRotation);
|
|
||||||
}
|
|
||||||
}, [localPatternRotation]);
|
|
||||||
|
|
||||||
// Handle transformer rotation - just store the angle, apply at upload time
|
|
||||||
const handleTransformEnd = useCallback(
|
|
||||||
(e: KonvaEventObject<Event>) => {
|
|
||||||
if (!pesData) return;
|
|
||||||
|
|
||||||
const node = e.target;
|
|
||||||
// Read rotation from the node
|
|
||||||
const totalRotation = node.rotation();
|
|
||||||
const normalizedRotation = ((totalRotation % 360) + 360) % 360;
|
|
||||||
|
|
||||||
setLocalPatternRotation(normalizedRotation);
|
|
||||||
|
|
||||||
// Also read position in case the Transformer affected it
|
|
||||||
const newOffset = {
|
|
||||||
x: node.x(),
|
|
||||||
y: node.y(),
|
|
||||||
};
|
|
||||||
setLocalPatternOffset(newOffset);
|
|
||||||
|
|
||||||
// Store rotation angle and position
|
|
||||||
setPatternRotation(normalizedRotation);
|
|
||||||
setPatternOffset(newOffset.x, newOffset.y);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[Canvas] Transform end - rotation:",
|
|
||||||
normalizedRotation,
|
|
||||||
"degrees, position:",
|
|
||||||
newOffset,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[setPatternRotation, setPatternOffset, pesData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPattern = pesData || uploadedPesData;
|
const hasPattern = pesData || uploadedPesData;
|
||||||
const borderColor = hasPattern
|
const borderColor = hasPattern
|
||||||
|
|
|
||||||
179
src/hooks/useCanvasViewport.ts
Normal file
179
src/hooks/useCanvasViewport.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
/**
|
||||||
|
* useCanvasViewport Hook
|
||||||
|
*
|
||||||
|
* Manages canvas viewport state including zoom, pan, and container size
|
||||||
|
* Handles wheel zoom and button zoom operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
type RefObject,
|
||||||
|
} from "react";
|
||||||
|
import type Konva from "konva";
|
||||||
|
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||||
|
import type { MachineInfo } from "../types/machine";
|
||||||
|
import { calculateInitialScale } from "../utils/konvaRenderers";
|
||||||
|
import { calculateZoomToPoint } from "../components/PatternCanvas/patternCanvasHelpers";
|
||||||
|
|
||||||
|
interface UseCanvasViewportOptions {
|
||||||
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
pesData: PesPatternData | null;
|
||||||
|
uploadedPesData: PesPatternData | null;
|
||||||
|
machineInfo: MachineInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCanvasViewport({
|
||||||
|
containerRef,
|
||||||
|
pesData,
|
||||||
|
uploadedPesData,
|
||||||
|
machineInfo,
|
||||||
|
}: UseCanvasViewportOptions) {
|
||||||
|
const [stagePos, setStagePos] = useState({ x: 0, y: 0 });
|
||||||
|
const [stageScale, setStageScale] = useState(1);
|
||||||
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
||||||
|
const initialScaleRef = useRef<number>(1);
|
||||||
|
const prevPesDataRef = useRef<PesPatternData | null>(null);
|
||||||
|
|
||||||
|
// Track container size with ResizeObserver
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const updateSize = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const width = containerRef.current.clientWidth;
|
||||||
|
const height = containerRef.current.clientHeight;
|
||||||
|
setContainerSize({ width, height });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial size
|
||||||
|
updateSize();
|
||||||
|
|
||||||
|
// Watch for resize
|
||||||
|
const resizeObserver = new ResizeObserver(updateSize);
|
||||||
|
resizeObserver.observe(containerRef.current);
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect();
|
||||||
|
}, [containerRef]);
|
||||||
|
|
||||||
|
// Calculate and store initial scale when pattern or hoop changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Use whichever pattern is available (uploaded or original)
|
||||||
|
const currentPattern = uploadedPesData || pesData;
|
||||||
|
if (!currentPattern || containerSize.width === 0) {
|
||||||
|
prevPesDataRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only recalculate if pattern changed
|
||||||
|
if (prevPesDataRef.current !== currentPattern) {
|
||||||
|
prevPesDataRef.current = currentPattern;
|
||||||
|
|
||||||
|
const { bounds } = currentPattern;
|
||||||
|
const viewWidth = machineInfo
|
||||||
|
? machineInfo.maxWidth
|
||||||
|
: bounds.maxX - bounds.minX;
|
||||||
|
const viewHeight = machineInfo
|
||||||
|
? machineInfo.maxHeight
|
||||||
|
: bounds.maxY - bounds.minY;
|
||||||
|
|
||||||
|
const initialScale = calculateInitialScale(
|
||||||
|
containerSize.width,
|
||||||
|
containerSize.height,
|
||||||
|
viewWidth,
|
||||||
|
viewHeight,
|
||||||
|
);
|
||||||
|
initialScaleRef.current = initialScale;
|
||||||
|
|
||||||
|
// Reset view when pattern changes
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
setStageScale(initialScale);
|
||||||
|
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
||||||
|
}
|
||||||
|
}, [pesData, uploadedPesData, machineInfo, containerSize]);
|
||||||
|
|
||||||
|
// Wheel zoom handler
|
||||||
|
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
const stage = e.target.getStage();
|
||||||
|
if (!stage) return;
|
||||||
|
|
||||||
|
const pointer = stage.getPointerPosition();
|
||||||
|
if (!pointer) return;
|
||||||
|
|
||||||
|
const scaleBy = 1.1;
|
||||||
|
const direction = e.evt.deltaY > 0 ? -1 : 1;
|
||||||
|
|
||||||
|
setStageScale((oldScale) => {
|
||||||
|
const newScale = Math.max(
|
||||||
|
0.1,
|
||||||
|
Math.min(direction > 0 ? oldScale * scaleBy : oldScale / scaleBy, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Zoom towards pointer
|
||||||
|
setStagePos((prevPos) =>
|
||||||
|
calculateZoomToPoint(oldScale, newScale, pointer, prevPos),
|
||||||
|
);
|
||||||
|
|
||||||
|
return newScale;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Zoom control handlers
|
||||||
|
const handleZoomIn = useCallback(() => {
|
||||||
|
setStageScale((oldScale) => {
|
||||||
|
const newScale = Math.max(0.1, Math.min(oldScale * 1.2, 2));
|
||||||
|
|
||||||
|
// Zoom towards center of viewport
|
||||||
|
const center = {
|
||||||
|
x: containerSize.width / 2,
|
||||||
|
y: containerSize.height / 2,
|
||||||
|
};
|
||||||
|
setStagePos((prevPos) =>
|
||||||
|
calculateZoomToPoint(oldScale, newScale, center, prevPos),
|
||||||
|
);
|
||||||
|
|
||||||
|
return newScale;
|
||||||
|
});
|
||||||
|
}, [containerSize]);
|
||||||
|
|
||||||
|
const handleZoomOut = useCallback(() => {
|
||||||
|
setStageScale((oldScale) => {
|
||||||
|
const newScale = Math.max(0.1, Math.min(oldScale / 1.2, 2));
|
||||||
|
|
||||||
|
// Zoom towards center of viewport
|
||||||
|
const center = {
|
||||||
|
x: containerSize.width / 2,
|
||||||
|
y: containerSize.height / 2,
|
||||||
|
};
|
||||||
|
setStagePos((prevPos) =>
|
||||||
|
calculateZoomToPoint(oldScale, newScale, center, prevPos),
|
||||||
|
);
|
||||||
|
|
||||||
|
return newScale;
|
||||||
|
});
|
||||||
|
}, [containerSize]);
|
||||||
|
|
||||||
|
const handleZoomReset = useCallback(() => {
|
||||||
|
const initialScale = initialScaleRef.current;
|
||||||
|
setStageScale(initialScale);
|
||||||
|
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
||||||
|
}, [containerSize]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
stagePos,
|
||||||
|
stageScale,
|
||||||
|
containerSize,
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
handleWheel,
|
||||||
|
handleZoomIn,
|
||||||
|
handleZoomOut,
|
||||||
|
handleZoomReset,
|
||||||
|
};
|
||||||
|
}
|
||||||
171
src/hooks/usePatternTransform.ts
Normal file
171
src/hooks/usePatternTransform.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
/**
|
||||||
|
* usePatternTransform Hook
|
||||||
|
*
|
||||||
|
* Manages pattern transformation state including position, rotation, and drag/transform handling
|
||||||
|
* Syncs local state with global pattern store
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import type Konva from "konva";
|
||||||
|
import type { KonvaEventObject } from "konva/lib/Node";
|
||||||
|
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||||
|
|
||||||
|
interface UsePatternTransformOptions {
|
||||||
|
pesData: PesPatternData | null;
|
||||||
|
initialPatternOffset: { x: number; y: number };
|
||||||
|
initialPatternRotation: number;
|
||||||
|
setPatternOffset: (x: number, y: number) => void;
|
||||||
|
setPatternRotation: (rotation: number) => void;
|
||||||
|
patternUploaded: boolean;
|
||||||
|
isUploading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePatternTransform({
|
||||||
|
pesData,
|
||||||
|
initialPatternOffset,
|
||||||
|
initialPatternRotation,
|
||||||
|
setPatternOffset,
|
||||||
|
setPatternRotation,
|
||||||
|
patternUploaded,
|
||||||
|
isUploading,
|
||||||
|
}: UsePatternTransformOptions) {
|
||||||
|
const [localPatternOffset, setLocalPatternOffset] = useState(
|
||||||
|
initialPatternOffset || { x: 0, y: 0 },
|
||||||
|
);
|
||||||
|
const [localPatternRotation, setLocalPatternRotation] = useState(
|
||||||
|
initialPatternRotation || 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const patternGroupRef = useRef<Konva.Group | null>(null);
|
||||||
|
const transformerRef = useRef<Konva.Transformer | null>(null);
|
||||||
|
|
||||||
|
// Update pattern offset when initialPatternOffset changes
|
||||||
|
if (
|
||||||
|
initialPatternOffset &&
|
||||||
|
(localPatternOffset.x !== initialPatternOffset.x ||
|
||||||
|
localPatternOffset.y !== initialPatternOffset.y)
|
||||||
|
) {
|
||||||
|
setLocalPatternOffset(initialPatternOffset);
|
||||||
|
console.log(
|
||||||
|
"[PatternTransform] Restored pattern offset:",
|
||||||
|
initialPatternOffset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update pattern rotation when initialPatternRotation changes
|
||||||
|
if (
|
||||||
|
initialPatternRotation !== undefined &&
|
||||||
|
localPatternRotation !== initialPatternRotation
|
||||||
|
) {
|
||||||
|
setLocalPatternRotation(initialPatternRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach/detach transformer based on state
|
||||||
|
const attachTransformer = useCallback(() => {
|
||||||
|
if (!transformerRef.current || !patternGroupRef.current) {
|
||||||
|
console.log(
|
||||||
|
"[PatternTransform] Cannot attach transformer - refs not ready",
|
||||||
|
{
|
||||||
|
hasTransformer: !!transformerRef.current,
|
||||||
|
hasPatternGroup: !!patternGroupRef.current,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patternUploaded && !isUploading) {
|
||||||
|
console.log("[PatternTransform] Attaching transformer");
|
||||||
|
transformerRef.current.nodes([patternGroupRef.current]);
|
||||||
|
transformerRef.current.getLayer()?.batchDraw();
|
||||||
|
} else {
|
||||||
|
console.log("[PatternTransform] Detaching transformer");
|
||||||
|
transformerRef.current.nodes([]);
|
||||||
|
}
|
||||||
|
}, [patternUploaded, isUploading]);
|
||||||
|
|
||||||
|
// Call attachTransformer when conditions change
|
||||||
|
useEffect(() => {
|
||||||
|
attachTransformer();
|
||||||
|
}, [attachTransformer, pesData]);
|
||||||
|
|
||||||
|
// Sync node rotation with state (important for when rotation is reset to 0 after upload)
|
||||||
|
useEffect(() => {
|
||||||
|
if (patternGroupRef.current) {
|
||||||
|
patternGroupRef.current.rotation(localPatternRotation);
|
||||||
|
}
|
||||||
|
}, [localPatternRotation]);
|
||||||
|
|
||||||
|
// Center pattern in hoop
|
||||||
|
const handleCenterPattern = useCallback(() => {
|
||||||
|
if (!pesData) return;
|
||||||
|
|
||||||
|
const { bounds } = pesData;
|
||||||
|
const centerOffsetX = -(bounds.minX + bounds.maxX) / 2;
|
||||||
|
const centerOffsetY = -(bounds.minY + bounds.maxY) / 2;
|
||||||
|
|
||||||
|
setLocalPatternOffset({ x: centerOffsetX, y: centerOffsetY });
|
||||||
|
setPatternOffset(centerOffsetX, centerOffsetY);
|
||||||
|
}, [pesData, setPatternOffset]);
|
||||||
|
|
||||||
|
// Pattern drag handlers
|
||||||
|
const handlePatternDragEnd = useCallback(
|
||||||
|
(e: Konva.KonvaEventObject<DragEvent>) => {
|
||||||
|
const newOffset = {
|
||||||
|
x: e.target.x(),
|
||||||
|
y: e.target.y(),
|
||||||
|
};
|
||||||
|
setLocalPatternOffset(newOffset);
|
||||||
|
setPatternOffset(newOffset.x, newOffset.y);
|
||||||
|
},
|
||||||
|
[setPatternOffset],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle transformer rotation - just store the angle, apply at upload time
|
||||||
|
const handleTransformEnd = useCallback(
|
||||||
|
(e: KonvaEventObject<Event>) => {
|
||||||
|
if (!pesData) return;
|
||||||
|
|
||||||
|
const node = e.target;
|
||||||
|
// Read rotation from the node
|
||||||
|
const totalRotation = node.rotation();
|
||||||
|
const normalizedRotation = ((totalRotation % 360) + 360) % 360;
|
||||||
|
|
||||||
|
setLocalPatternRotation(normalizedRotation);
|
||||||
|
|
||||||
|
// Also read position in case the Transformer affected it
|
||||||
|
const newOffset = {
|
||||||
|
x: node.x(),
|
||||||
|
y: node.y(),
|
||||||
|
};
|
||||||
|
setLocalPatternOffset(newOffset);
|
||||||
|
|
||||||
|
// Store rotation angle and position
|
||||||
|
setPatternRotation(normalizedRotation);
|
||||||
|
setPatternOffset(newOffset.x, newOffset.y);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[PatternTransform] Transform end - rotation:",
|
||||||
|
normalizedRotation,
|
||||||
|
"degrees, position:",
|
||||||
|
newOffset,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setPatternRotation, setPatternOffset, pesData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
localPatternOffset,
|
||||||
|
localPatternRotation,
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
patternGroupRef,
|
||||||
|
transformerRef,
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
attachTransformer,
|
||||||
|
handleCenterPattern,
|
||||||
|
handlePatternDragEnd,
|
||||||
|
handleTransformEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue