import { memo, useMemo } from "react";
import { Group, Line, Rect, Text, Circle } from "react-konva";
import type { PesPatternData } from "../../formats/import/pesImporter";
import { getThreadColor } from "../../formats/import/pesImporter";
import type { MachineInfo } from "../../types/machine";
import { MOVE } from "../../formats/import/constants";
import { canvasColors } from "../../utils/cssVariables";
interface GridProps {
gridSize: number;
bounds: { minX: number; maxX: number; minY: number; maxY: number };
machineInfo: MachineInfo | null;
}
export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
const lines = useMemo(() => {
const gridMinX = machineInfo ? -machineInfo.maxWidth / 2 : bounds.minX;
const gridMaxX = machineInfo ? machineInfo.maxWidth / 2 : bounds.maxX;
const gridMinY = machineInfo ? -machineInfo.maxHeight / 2 : bounds.minY;
const gridMaxY = machineInfo ? machineInfo.maxHeight / 2 : bounds.maxY;
const verticalLines: number[][] = [];
const horizontalLines: number[][] = [];
// Vertical lines
for (
let x = Math.floor(gridMinX / gridSize) * gridSize;
x <= gridMaxX;
x += gridSize
) {
verticalLines.push([x, gridMinY, x, gridMaxY]);
}
// Horizontal lines
for (
let y = Math.floor(gridMinY / gridSize) * gridSize;
y <= gridMaxY;
y += gridSize
) {
horizontalLines.push([gridMinX, y, gridMaxX, y]);
}
return { verticalLines, horizontalLines };
}, [gridSize, bounds, machineInfo]);
const gridColor = canvasColors.grid();
return (
{lines.verticalLines.map((points, i) => (
))}
{lines.horizontalLines.map((points, i) => (
))}
);
});
Grid.displayName = "Grid";
export const Origin = memo(() => {
const originColor = canvasColors.origin();
return (
);
});
Origin.displayName = "Origin";
interface HoopProps {
machineInfo: MachineInfo;
}
export const Hoop = memo(({ machineInfo }: HoopProps) => {
const { maxWidth, maxHeight } = machineInfo;
const hoopLeft = -maxWidth / 2;
const hoopTop = -maxHeight / 2;
const hoopColor = canvasColors.hoop();
return (
);
});
Hoop.displayName = "Hoop";
interface PatternBoundsProps {
bounds: { minX: number; maxX: number; minY: number; maxY: number };
}
export const PatternBounds = memo(({ bounds }: PatternBoundsProps) => {
const { minX, maxX, minY, maxY } = bounds;
const width = maxX - minX;
const height = maxY - minY;
const boundsColor = canvasColors.bounds();
return (
);
});
PatternBounds.displayName = "PatternBounds";
interface StitchesProps {
stitches: number[][];
pesData: PesPatternData;
currentStitchIndex: number;
showProgress?: boolean;
}
export const Stitches = memo(
({
stitches,
pesData,
currentStitchIndex,
showProgress = false,
}: StitchesProps) => {
// PERFORMANCE OPTIMIZATION:
// Separate static group structure (doesn't change during sewing)
// from dynamic completion status (changes with currentStitchIndex).
// This prevents recalculating all groups on every progress update.
//
// For very large patterns (>100k stitches), consider:
// - Virtualization: render only visible stitches based on viewport
// - LOD (Level of Detail): reduce stitch density when zoomed out
// - Web Workers: offload grouping calculations to background thread
interface StaticStitchGroup {
color: string;
points: number[];
isJump: boolean;
startIndex: number; // First stitch index in this group
endIndex: number; // Last stitch index in this group
}
// Static grouping - only recalculates when stitches or colors change
const staticGroups = useMemo(() => {
const groups: StaticStitchGroup[] = [];
let currentGroup: StaticStitchGroup | null = null;
let prevX = 0;
let prevY = 0;
for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i];
const [x, y, cmd, colorIndex] = stitch;
const isJump = (cmd & MOVE) !== 0;
const color = getThreadColor(pesData, colorIndex);
// Start new group if color or type changes (NOT completion status)
if (
!currentGroup ||
currentGroup.color !== color ||
currentGroup.isJump !== isJump
) {
// For jump stitches, include previous position
if (isJump && i > 0) {
currentGroup = {
color,
points: [prevX, prevY, x, y],
isJump,
startIndex: i,
endIndex: i,
};
} else {
currentGroup = {
color,
points: [x, y],
isJump,
startIndex: i,
endIndex: i,
};
}
groups.push(currentGroup);
} else {
currentGroup.points.push(x, y);
currentGroup.endIndex = i;
}
prevX = x;
prevY = y;
}
return groups;
}, [stitches, pesData]);
// Dynamic grouping - adds completion status based on currentStitchIndex
// Only needs to check group boundaries, not rebuild everything
const stitchGroups = useMemo(() => {
interface StitchGroup {
color: string;
points: number[];
completed: boolean;
isJump: boolean;
}
const groups: StitchGroup[] = [];
for (const staticGroup of staticGroups) {
// Check if this group needs to be split based on completion
if (
currentStitchIndex > staticGroup.startIndex &&
currentStitchIndex <= staticGroup.endIndex
) {
// Group is partially completed - need to split
// This is rare during sewing (only happens when crossing group boundaries)
// Rebuild this group with completion split
let currentSubGroup: StitchGroup | null = null;
const groupStitches = stitches.slice(
staticGroup.startIndex,
staticGroup.endIndex + 1,
);
let prevX =
staticGroup.startIndex > 0
? stitches[staticGroup.startIndex - 1][0]
: 0;
let prevY =
staticGroup.startIndex > 0
? stitches[staticGroup.startIndex - 1][1]
: 0;
for (let i = 0; i < groupStitches.length; i++) {
const absoluteIndex = staticGroup.startIndex + i;
const stitch = groupStitches[i];
const [x, y] = stitch;
const isCompleted = absoluteIndex < currentStitchIndex;
if (!currentSubGroup || currentSubGroup.completed !== isCompleted) {
if (staticGroup.isJump && i > 0) {
currentSubGroup = {
color: staticGroup.color,
points: [prevX, prevY, x, y],
completed: isCompleted,
isJump: staticGroup.isJump,
};
} else {
currentSubGroup = {
color: staticGroup.color,
points: [x, y],
completed: isCompleted,
isJump: staticGroup.isJump,
};
}
groups.push(currentSubGroup);
} else {
currentSubGroup.points.push(x, y);
}
prevX = x;
prevY = y;
}
} else {
// Group is fully completed or fully incomplete
groups.push({
color: staticGroup.color,
points: staticGroup.points,
completed: currentStitchIndex > staticGroup.endIndex,
isJump: staticGroup.isJump,
});
}
}
return groups;
}, [staticGroups, currentStitchIndex, stitches]);
return (
{stitchGroups.map((group, i) => (
))}
);
},
// Custom comparison to prevent unnecessary re-renders
(prevProps, nextProps) => {
// Re-render only if these values actually changed
return (
prevProps.stitches === nextProps.stitches &&
prevProps.pesData === nextProps.pesData &&
prevProps.currentStitchIndex === nextProps.currentStitchIndex &&
prevProps.showProgress === nextProps.showProgress
);
},
);
Stitches.displayName = "Stitches";
interface CurrentPositionProps {
currentStitchIndex: number;
stitches: number[][];
}
export const CurrentPosition = memo(
({ currentStitchIndex, stitches }: CurrentPositionProps) => {
if (currentStitchIndex <= 0 || currentStitchIndex >= stitches.length) {
return null;
}
const [x, y] = stitches[currentStitchIndex];
const positionColor = canvasColors.position();
return (
);
},
);
CurrentPosition.displayName = "CurrentPosition";