mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
feature: Create unified PatternLayer component to eliminate duplication
Unified three separate layer implementations into single PatternLayer component: **PatternLayer.tsx (156 lines)** - Handles both interactive (original) and locked (uploaded) pattern rendering - Props-driven configuration for dragging, rotation, transformer - Includes CurrentPosition indicator for uploaded patterns - Memoized stitches and center calculations for performance - Single source of truth for pattern rendering logic **Benefits:** - Eliminated ~140 lines of duplicated layer code - Reduced PatternCanvas from 388 to 271 lines (-117 lines, -30%) - Consistent rendering behavior across pattern states - Easier to maintain and test pattern rendering - Cleaner component composition in PatternCanvas **Removed from PatternCanvas:** - Original pattern layer implementation (74 lines) - Uploaded pattern layer implementation (33 lines) - Current position layer implementation (26 lines) - Duplicate Group, Transformer, Stitches setup - Redundant center calculations and stitch conversions **Complete refactoring impact (All Phases):** - Original: 786 lines in single monolithic file - After Phase 4: 271 lines main + hooks + components - **Total reduction: -515 lines (-65%)** File structure now: - PatternCanvas.tsx: 271 lines (orchestration) - PatternLayer.tsx: 156 lines (rendering) - useCanvasViewport.ts: 166 lines (viewport) - usePatternTransform.ts: 179 lines (transform) - 3 display components + 3 utilities 🤖 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
99d32f9029
commit
469c08d45b
2 changed files with 183 additions and 144 deletions
|
|
@ -5,17 +5,10 @@ import {
|
||||||
usePatternUploaded,
|
usePatternUploaded,
|
||||||
} from "../../stores/useMachineStore";
|
} from "../../stores/useMachineStore";
|
||||||
import { usePatternStore } from "../../stores/usePatternStore";
|
import { usePatternStore } from "../../stores/usePatternStore";
|
||||||
import { Stage, Layer, Group, Transformer } from "react-konva";
|
import { Stage, Layer } from "react-konva";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import { PhotoIcon } from "@heroicons/react/24/solid";
|
import { PhotoIcon } from "@heroicons/react/24/solid";
|
||||||
import {
|
import { Grid, Origin, Hoop } from "../KonvaComponents";
|
||||||
Grid,
|
|
||||||
Origin,
|
|
||||||
Hoop,
|
|
||||||
Stitches,
|
|
||||||
PatternBounds,
|
|
||||||
CurrentPosition,
|
|
||||||
} from "../KonvaComponents";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
|
@ -23,13 +16,10 @@ import {
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
|
||||||
calculatePatternCenter,
|
|
||||||
convertPenStitchesToPesFormat,
|
|
||||||
} 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 { PatternLayer } from "./PatternLayer";
|
||||||
import { useCanvasViewport } from "../../hooks/useCanvasViewport";
|
import { useCanvasViewport } from "../../hooks/useCanvasViewport";
|
||||||
import { usePatternTransform } from "../../hooks/usePatternTransform";
|
import { usePatternTransform } from "../../hooks/usePatternTransform";
|
||||||
|
|
||||||
|
|
@ -201,141 +191,34 @@ export function PatternCanvas() {
|
||||||
|
|
||||||
{/* Original pattern layer: draggable with transformer (shown before upload starts) */}
|
{/* Original pattern layer: draggable with transformer (shown before upload starts) */}
|
||||||
<Layer visible={!isUploading && !patternUploaded}>
|
<Layer visible={!isUploading && !patternUploaded}>
|
||||||
{pesData &&
|
{pesData && (
|
||||||
(() => {
|
<PatternLayer
|
||||||
const originalCenter = calculatePatternCenter(
|
pesData={pesData}
|
||||||
pesData.bounds,
|
offset={localPatternOffset}
|
||||||
);
|
rotation={localPatternRotation}
|
||||||
console.log("[Canvas] Rendering original pattern:", {
|
isInteractive={true}
|
||||||
position: localPatternOffset,
|
showProgress={false}
|
||||||
rotation: localPatternRotation,
|
currentStitchIndex={0}
|
||||||
center: originalCenter,
|
patternGroupRef={patternGroupRef}
|
||||||
bounds: pesData.bounds,
|
transformerRef={transformerRef}
|
||||||
});
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Group
|
|
||||||
name="pattern-group"
|
|
||||||
ref={(node) => {
|
|
||||||
patternGroupRef.current = node;
|
|
||||||
// Set initial rotation from state
|
|
||||||
if (node) {
|
|
||||||
node.rotation(localPatternRotation);
|
|
||||||
// Try to attach transformer when group is mounted
|
|
||||||
attachTransformer();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
draggable={!isUploading}
|
|
||||||
x={localPatternOffset.x}
|
|
||||||
y={localPatternOffset.y}
|
|
||||||
offsetX={originalCenter.x}
|
|
||||||
offsetY={originalCenter.y}
|
|
||||||
onDragEnd={handlePatternDragEnd}
|
onDragEnd={handlePatternDragEnd}
|
||||||
onTransformEnd={handleTransformEnd}
|
onTransformEnd={handleTransformEnd}
|
||||||
onMouseEnter={(e) => {
|
attachTransformer={attachTransformer}
|
||||||
const stage = e.target.getStage();
|
/>
|
||||||
if (stage && !isUploading)
|
|
||||||
stage.container().style.cursor = "move";
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
const stage = e.target.getStage();
|
|
||||||
if (stage && !isUploading)
|
|
||||||
stage.container().style.cursor = "grab";
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stitches
|
|
||||||
stitches={convertPenStitchesToPesFormat(
|
|
||||||
pesData.penStitches,
|
|
||||||
)}
|
)}
|
||||||
pesData={pesData}
|
|
||||||
currentStitchIndex={0}
|
|
||||||
showProgress={false}
|
|
||||||
/>
|
|
||||||
<PatternBounds bounds={pesData.bounds} />
|
|
||||||
</Group>
|
|
||||||
<Transformer
|
|
||||||
ref={(node) => {
|
|
||||||
transformerRef.current = node;
|
|
||||||
// Try to attach transformer when transformer is mounted
|
|
||||||
if (node) {
|
|
||||||
attachTransformer();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
enabledAnchors={[]}
|
|
||||||
rotateEnabled={true}
|
|
||||||
borderEnabled={true}
|
|
||||||
borderStroke="#FF6B6B"
|
|
||||||
borderStrokeWidth={2}
|
|
||||||
rotationSnaps={[0, 45, 90, 135, 180, 225, 270, 315]}
|
|
||||||
ignoreStroke={true}
|
|
||||||
rotateAnchorOffset={20}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</Layer>
|
</Layer>
|
||||||
|
|
||||||
{/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */}
|
{/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */}
|
||||||
<Layer visible={isUploading || patternUploaded}>
|
<Layer visible={isUploading || patternUploaded}>
|
||||||
{uploadedPesData &&
|
{uploadedPesData && (
|
||||||
(() => {
|
<PatternLayer
|
||||||
const uploadedCenter = calculatePatternCenter(
|
|
||||||
uploadedPesData.bounds,
|
|
||||||
);
|
|
||||||
console.log("[Canvas] Rendering uploaded pattern:", {
|
|
||||||
position: initialUploadedPatternOffset,
|
|
||||||
center: uploadedCenter,
|
|
||||||
bounds: uploadedPesData.bounds,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
name="uploaded-pattern-group"
|
|
||||||
x={initialUploadedPatternOffset.x}
|
|
||||||
y={initialUploadedPatternOffset.y}
|
|
||||||
offsetX={uploadedCenter.x}
|
|
||||||
offsetY={uploadedCenter.y}
|
|
||||||
>
|
|
||||||
<Stitches
|
|
||||||
stitches={convertPenStitchesToPesFormat(
|
|
||||||
uploadedPesData.penStitches,
|
|
||||||
)}
|
|
||||||
pesData={uploadedPesData}
|
pesData={uploadedPesData}
|
||||||
currentStitchIndex={
|
offset={initialUploadedPatternOffset}
|
||||||
sewingProgress?.currentStitch || 0
|
isInteractive={false}
|
||||||
}
|
|
||||||
showProgress={true}
|
showProgress={true}
|
||||||
|
currentStitchIndex={sewingProgress?.currentStitch || 0}
|
||||||
/>
|
/>
|
||||||
<PatternBounds bounds={uploadedPesData.bounds} />
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</Layer>
|
|
||||||
|
|
||||||
{/* Current position layer (for uploaded pattern during sewing) */}
|
|
||||||
<Layer visible={isUploading || patternUploaded}>
|
|
||||||
{uploadedPesData &&
|
|
||||||
sewingProgress &&
|
|
||||||
sewingProgress.currentStitch > 0 &&
|
|
||||||
(() => {
|
|
||||||
const center = calculatePatternCenter(
|
|
||||||
uploadedPesData.bounds,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
x={initialUploadedPatternOffset.x}
|
|
||||||
y={initialUploadedPatternOffset.y}
|
|
||||||
offsetX={center.x}
|
|
||||||
offsetY={center.y}
|
|
||||||
>
|
|
||||||
<CurrentPosition
|
|
||||||
currentStitchIndex={sewingProgress.currentStitch}
|
|
||||||
stitches={convertPenStitchesToPesFormat(
|
|
||||||
uploadedPesData.penStitches,
|
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
156
src/components/PatternCanvas/PatternLayer.tsx
Normal file
156
src/components/PatternCanvas/PatternLayer.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* PatternLayer Component
|
||||||
|
*
|
||||||
|
* Unified component for rendering pattern layers (both original and uploaded)
|
||||||
|
* Handles both interactive (draggable/rotatable) and locked states
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo, 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 function PatternLayer({
|
||||||
|
pesData,
|
||||||
|
offset,
|
||||||
|
rotation = 0,
|
||||||
|
isInteractive,
|
||||||
|
showProgress = false,
|
||||||
|
currentStitchIndex = 0,
|
||||||
|
patternGroupRef,
|
||||||
|
transformerRef,
|
||||||
|
onDragEnd,
|
||||||
|
onTransformEnd,
|
||||||
|
attachTransformer,
|
||||||
|
}: PatternLayerProps) {
|
||||||
|
const center = useMemo(
|
||||||
|
() => calculatePatternCenter(pesData.bounds),
|
||||||
|
[pesData.bounds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stitches = useMemo(
|
||||||
|
() => convertPenStitchesToPesFormat(pesData.penStitches),
|
||||||
|
[pesData.penStitches],
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupName = isInteractive ? "pattern-group" : "uploaded-pattern-group";
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[PatternLayer] Rendering ${isInteractive ? "original" : "uploaded"} pattern:`,
|
||||||
|
{
|
||||||
|
position: offset,
|
||||||
|
rotation: isInteractive ? rotation : "n/a",
|
||||||
|
center,
|
||||||
|
bounds: pesData.bounds,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue