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,
|
||||
} from "../../stores/useMachineStore";
|
||||
import { usePatternStore } from "../../stores/usePatternStore";
|
||||
import { Stage, Layer, Group, Transformer } from "react-konva";
|
||||
import { Stage, Layer } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import { PhotoIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Grid,
|
||||
Origin,
|
||||
Hoop,
|
||||
Stitches,
|
||||
PatternBounds,
|
||||
CurrentPosition,
|
||||
} from "../KonvaComponents";
|
||||
import { Grid, Origin, Hoop } from "../KonvaComponents";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
|
|
@ -23,13 +16,10 @@ import {
|
|||
CardDescription,
|
||||
CardContent,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
calculatePatternCenter,
|
||||
convertPenStitchesToPesFormat,
|
||||
} from "./patternCanvasHelpers";
|
||||
import { ThreadLegend } from "./ThreadLegend";
|
||||
import { PatternPositionIndicator } from "./PatternPositionIndicator";
|
||||
import { ZoomControls } from "./ZoomControls";
|
||||
import { PatternLayer } from "./PatternLayer";
|
||||
import { useCanvasViewport } from "../../hooks/useCanvasViewport";
|
||||
import { usePatternTransform } from "../../hooks/usePatternTransform";
|
||||
|
||||
|
|
@ -201,141 +191,34 @@ export function PatternCanvas() {
|
|||
|
||||
{/* Original pattern layer: draggable with transformer (shown before upload starts) */}
|
||||
<Layer visible={!isUploading && !patternUploaded}>
|
||||
{pesData &&
|
||||
(() => {
|
||||
const originalCenter = calculatePatternCenter(
|
||||
pesData.bounds,
|
||||
);
|
||||
console.log("[Canvas] Rendering original pattern:", {
|
||||
position: localPatternOffset,
|
||||
rotation: localPatternRotation,
|
||||
center: originalCenter,
|
||||
bounds: pesData.bounds,
|
||||
});
|
||||
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}
|
||||
{pesData && (
|
||||
<PatternLayer
|
||||
pesData={pesData}
|
||||
offset={localPatternOffset}
|
||||
rotation={localPatternRotation}
|
||||
isInteractive={true}
|
||||
showProgress={false}
|
||||
currentStitchIndex={0}
|
||||
patternGroupRef={patternGroupRef}
|
||||
transformerRef={transformerRef}
|
||||
onDragEnd={handlePatternDragEnd}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
onMouseEnter={(e) => {
|
||||
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,
|
||||
attachTransformer={attachTransformer}
|
||||
/>
|
||||
)}
|
||||
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>
|
||||
|
||||
{/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */}
|
||||
<Layer visible={isUploading || patternUploaded}>
|
||||
{uploadedPesData &&
|
||||
(() => {
|
||||
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,
|
||||
)}
|
||||
{uploadedPesData && (
|
||||
<PatternLayer
|
||||
pesData={uploadedPesData}
|
||||
currentStitchIndex={
|
||||
sewingProgress?.currentStitch || 0
|
||||
}
|
||||
offset={initialUploadedPatternOffset}
|
||||
isInteractive={false}
|
||||
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>
|
||||
</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