respira/src/components/PatternCanvas/PatternLayer.tsx
copilot-swe-agent[bot] 1d79ffb2a4 feature: Add computed selectors to usePatternStore for pattern transformations
- Add selectPatternCenter for pattern center calculation
- Add selectRotatedBounds for rotated bounds with memoization
- Add selectRotationCenterShift for center shift calculation
- Add selectPatternValidation for bounds checking against hoop
- Add comprehensive tests for all selectors
- Update usePatternValidation to use store selectors
- All tests passing and linter clean

Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com>
2025-12-27 16:56:54 +00:00

149 lines
4.5 KiB
TypeScript

/**
* PatternLayer Component
*
* Unified component for rendering pattern layers (both original and uploaded)
* Handles both interactive (draggable/rotatable) and locked states
*/
import { useMemo, memo, type RefObject } from "react";
import { Group, Transformer } from "react-konva";
import type Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import type { PesPatternData } from "../../formats/import/pesImporter";
import {
calculatePatternCenter,
convertPenStitchesToPesFormat,
} from "./patternCanvasHelpers";
import { Stitches, PatternBounds, CurrentPosition } from "./KonvaComponents";
interface PatternLayerProps {
pesData: PesPatternData;
offset: { x: number; y: number };
rotation?: number;
isInteractive: boolean;
showProgress?: boolean;
currentStitchIndex?: number;
patternGroupRef?: RefObject<Konva.Group | null>;
transformerRef?: RefObject<Konva.Transformer | null>;
onDragEnd?: (e: Konva.KonvaEventObject<DragEvent>) => void;
onTransformEnd?: (e: KonvaEventObject<Event>) => void;
attachTransformer?: () => void;
}
export const PatternLayer = memo(function PatternLayer({
pesData,
offset,
rotation = 0,
isInteractive,
showProgress = false,
currentStitchIndex = 0,
patternGroupRef,
transformerRef,
onDragEnd,
onTransformEnd,
attachTransformer,
}: PatternLayerProps) {
// Memoize center calculation - this is the pattern center calculation
// Note: We keep this local calculation here since the component receives
// pesData as a prop which could be either original or uploaded pattern
const center = useMemo(
() => calculatePatternCenter(pesData.bounds),
[pesData.bounds],
);
const stitches = useMemo(
() => convertPenStitchesToPesFormat(pesData.penStitches),
[pesData.penStitches],
);
const groupName = isInteractive ? "pattern-group" : "uploaded-pattern-group";
return (
<>
<Group
name={groupName}
ref={
isInteractive
? (node) => {
if (patternGroupRef) {
patternGroupRef.current = node;
}
// Set initial rotation from state
if (node && isInteractive) {
node.rotation(rotation);
// Try to attach transformer when group is mounted
if (attachTransformer) {
attachTransformer();
}
}
}
: undefined
}
draggable={isInteractive}
x={offset.x}
y={offset.y}
offsetX={center.x}
offsetY={center.y}
onDragEnd={isInteractive ? onDragEnd : undefined}
onTransformEnd={isInteractive ? onTransformEnd : undefined}
onMouseEnter={
isInteractive
? (e) => {
const stage = e.target.getStage();
if (stage) stage.container().style.cursor = "move";
}
: undefined
}
onMouseLeave={
isInteractive
? (e) => {
const stage = e.target.getStage();
if (stage) stage.container().style.cursor = "grab";
}
: undefined
}
>
<Stitches
stitches={stitches}
pesData={pesData}
currentStitchIndex={currentStitchIndex}
showProgress={showProgress}
/>
<PatternBounds bounds={pesData.bounds} />
</Group>
{/* Transformer only for interactive layer */}
{isInteractive && transformerRef && (
<Transformer
ref={(node) => {
if (transformerRef) {
transformerRef.current = node;
}
// Try to attach transformer when transformer is mounted
if (node && attachTransformer) {
attachTransformer();
}
}}
enabledAnchors={[]}
rotateEnabled={true}
borderEnabled={true}
borderStroke="#FF6B6B"
borderStrokeWidth={2}
rotationSnaps={[0, 45, 90, 135, 180, 225, 270, 315]}
ignoreStroke={true}
rotateAnchorOffset={20}
/>
)}
{/* Current position indicator (only for uploaded pattern with progress) */}
{!isInteractive && showProgress && currentStitchIndex > 0 && (
<Group x={offset.x} y={offset.y} offsetX={center.x} offsetY={center.y}>
<CurrentPosition
currentStitchIndex={currentStitchIndex}
stitches={stitches}
/>
</Group>
)}
</>
);
});