import Konva from 'konva'; import type { PesPatternData } from './pystitchConverter'; import { getThreadColor } from './pystitchConverter'; import type { MachineInfo } from '../types/machine'; import { MOVE } from './embroideryConstants'; /** * Renders a grid with specified spacing */ export function renderGrid( layer: Konva.Layer, gridSize: number, bounds: { minX: number; maxX: number; minY: number; maxY: number }, machineInfo: MachineInfo | null ): void { const gridGroup = new Konva.Group({ name: 'grid' }); // Determine grid bounds based on hoop or pattern 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; // Vertical lines for (let x = Math.floor(gridMinX / gridSize) * gridSize; x <= gridMaxX; x += gridSize) { const line = new Konva.Line({ points: [x, gridMinY, x, gridMaxY], stroke: '#e0e0e0', strokeWidth: 1, }); gridGroup.add(line); } // Horizontal lines for (let y = Math.floor(gridMinY / gridSize) * gridSize; y <= gridMaxY; y += gridSize) { const line = new Konva.Line({ points: [gridMinX, y, gridMaxX, y], stroke: '#e0e0e0', strokeWidth: 1, }); gridGroup.add(line); } layer.add(gridGroup); } /** * Renders the origin crosshair at (0,0) */ export function renderOrigin(layer: Konva.Layer): void { const originGroup = new Konva.Group({ name: 'origin' }); // Horizontal line const hLine = new Konva.Line({ points: [-10, 0, 10, 0], stroke: '#888', strokeWidth: 2, }); // Vertical line const vLine = new Konva.Line({ points: [0, -10, 0, 10], stroke: '#888', strokeWidth: 2, }); originGroup.add(hLine, vLine); layer.add(originGroup); } /** * Renders the hoop boundary and label */ export function renderHoop(layer: Konva.Layer, machineInfo: MachineInfo): void { const hoopGroup = new Konva.Group({ name: 'hoop' }); const hoopWidth = machineInfo.maxWidth; const hoopHeight = machineInfo.maxHeight; // Hoop is centered at origin (0, 0) const hoopLeft = -hoopWidth / 2; const hoopTop = -hoopHeight / 2; // Hoop boundary rectangle const rect = new Konva.Rect({ x: hoopLeft, y: hoopTop, width: hoopWidth, height: hoopHeight, stroke: '#2196F3', strokeWidth: 3, dash: [10, 5], }); // Hoop label const label = new Konva.Text({ x: hoopLeft + 10, y: hoopTop + 10, text: `Hoop: ${(hoopWidth / 10).toFixed(0)} x ${(hoopHeight / 10).toFixed(0)} mm`, fontSize: 14, fontFamily: 'sans-serif', fontStyle: 'bold', fill: '#2196F3', }); hoopGroup.add(rect, label); layer.add(hoopGroup); } /** * Renders embroidery stitches consolidated by color and completion status */ export function renderStitches( container: Konva.Layer | Konva.Group, stitches: number[][], pesData: PesPatternData, currentStitchIndex: number ): void { const stitchesGroup = new Konva.Group({ name: 'stitches' }); // Group stitches by color, completion status, and type (stitch vs jump) interface StitchGroup { color: string; points: number[]; completed: boolean; isJump: boolean; } const groups: StitchGroup[] = []; let currentGroup: StitchGroup | null = null; for (let i = 0; i < stitches.length; i++) { const stitch = stitches[i]; const [x, y, cmd, colorIndex] = stitch; const isCompleted = i < currentStitchIndex; const isJump = (cmd & MOVE) !== 0; const color = getThreadColor(pesData, colorIndex); // Start new group if color/status/type changes, or if it's the first stitch if ( !currentGroup || currentGroup.color !== color || currentGroup.completed !== isCompleted || currentGroup.isJump !== isJump ) { currentGroup = { color, points: [x, y], completed: isCompleted, isJump, }; groups.push(currentGroup); } else { // Continue the current group currentGroup.points.push(x, y); } } // Create Konva.Line for each group groups.forEach((group) => { if (group.isJump) { // Jump stitches - dashed lines in thread color const line = new Konva.Line({ points: group.points, stroke: group.color, strokeWidth: 1.0, lineCap: 'round', lineJoin: 'round', dash: [5, 5], opacity: group.completed ? 0.6 : 0.25, }); stitchesGroup.add(line); } else { // Regular stitches - solid lines with actual thread color const line = new Konva.Line({ points: group.points, stroke: group.color, strokeWidth: 1.5, lineCap: 'round', lineJoin: 'round', opacity: group.completed ? 1.0 : 0.3, }); stitchesGroup.add(line); } }); container.add(stitchesGroup); } /** * Renders pattern bounds rectangle */ export function renderPatternBounds( container: Konva.Layer | Konva.Group, bounds: { minX: number; maxX: number; minY: number; maxY: number } ): void { const { minX, maxX, minY, maxY } = bounds; const patternWidth = maxX - minX; const patternHeight = maxY - minY; const rect = new Konva.Rect({ x: minX, y: minY, width: patternWidth, height: patternHeight, stroke: '#ff0000', strokeWidth: 2, dash: [5, 5], }); container.add(rect); } /** * Renders the current position indicator */ export function renderCurrentPosition( container: Konva.Layer | Konva.Group, currentStitchIndex: number, stitches: number[][] ): void { if (currentStitchIndex <= 0 || currentStitchIndex >= stitches.length) return; const stitch = stitches[currentStitchIndex]; const [x, y] = stitch; const posGroup = new Konva.Group({ name: 'currentPosition' }); // Circle with fill const circle = new Konva.Circle({ x, y, radius: 8, fill: 'rgba(255, 0, 0, 0.3)', stroke: '#ff0000', strokeWidth: 3, }); // Crosshair lines const hLine1 = new Konva.Line({ points: [x - 12, y, x - 3, y], stroke: '#ff0000', strokeWidth: 2, }); const hLine2 = new Konva.Line({ points: [x + 12, y, x + 3, y], stroke: '#ff0000', strokeWidth: 2, }); const vLine1 = new Konva.Line({ points: [x, y - 12, x, y - 3], stroke: '#ff0000', strokeWidth: 2, }); const vLine2 = new Konva.Line({ points: [x, y + 12, x, y + 3], stroke: '#ff0000', strokeWidth: 2, }); posGroup.add(circle, hLine1, hLine2, vLine1, vLine2); container.add(posGroup); } /** * Renders thread color legend (positioned at top-left of viewport) */ export function renderLegend( layer: Konva.Layer, pesData: PesPatternData ): void { const legendGroup = new Konva.Group({ name: 'legend' }); // Semi-transparent background for better readability const bgPadding = 8; const itemHeight = 25; const legendHeight = pesData.threads.length * itemHeight + bgPadding * 2; const background = new Konva.Rect({ x: 10, y: 10, width: 100, height: legendHeight, fill: 'rgba(255, 255, 255, 0.9)', cornerRadius: 4, shadowColor: 'rgba(0, 0, 0, 0.2)', shadowBlur: 4, shadowOffset: { x: 0, y: 2 }, }); legendGroup.add(background); let legendY = 10 + bgPadding; // Draw legend for each thread for (let i = 0; i < pesData.threads.length; i++) { const color = getThreadColor(pesData, i); // Color swatch const swatch = new Konva.Rect({ x: 18, y: legendY, width: 20, height: 20, fill: color, stroke: '#000', strokeWidth: 1, }); // Thread label const label = new Konva.Text({ x: 43, y: legendY + 5, text: `Thread ${i + 1}`, fontSize: 12, fontFamily: 'sans-serif', fill: '#000', }); legendGroup.add(swatch, label); legendY += itemHeight; } layer.add(legendGroup); } /** * Renders pattern dimensions text (positioned at bottom-right of viewport) */ export function renderDimensions( layer: Konva.Layer, patternWidth: number, patternHeight: number, stageWidth: number, stageHeight: number ): void { const dimensionText = `${(patternWidth / 10).toFixed(1)} x ${(patternHeight / 10).toFixed(1)} mm`; // Background for better readability const textWidth = 140; const textHeight = 30; const padding = 8; const background = new Konva.Rect({ x: stageWidth - textWidth - padding - 10, y: stageHeight - textHeight - padding - 80, // Above zoom controls width: textWidth, height: textHeight, fill: 'rgba(255, 255, 255, 0.9)', cornerRadius: 4, shadowColor: 'rgba(0, 0, 0, 0.2)', shadowBlur: 4, shadowOffset: { x: 0, y: 2 }, }); const text = new Konva.Text({ x: stageWidth - textWidth - 10, y: stageHeight - textHeight - 80, width: textWidth, height: textHeight, text: dimensionText, fontSize: 14, fontFamily: 'sans-serif', fill: '#000', align: 'center', verticalAlign: 'middle', }); layer.add(background, text); } /** * Calculates initial scale to fit the view (hoop or pattern) */ export function calculateInitialScale( stageWidth: number, stageHeight: number, viewWidth: number, viewHeight: number, padding: number = 40 ): number { const scaleX = (stageWidth - 2 * padding) / viewWidth; const scaleY = (stageHeight - 2 * padding) / viewHeight; return Math.min(scaleX, scaleY); }