import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; import { FlooringPreview, SkirtingPreview } from '../src/components/CutPreview'; import { CutDialog } from '../src/components/CutDialog'; import { defaultProject } from '../src/lib/defaults'; import { dimensionedOutline, exactMm, flooringTemplate, skirtingEndBoxes, } from '../src/lib/cut-preview'; import type { FlooringPiece, Point, SkirtingPiece } from '../src/lib/types'; const floor: FlooringPiece = { id: 'F1', roomId: 'room-1', row: 1, column: 1, polygon: [], length: 600.001, width: 190, stockId: 'B1', reused: false, cut: true, shaped: false, template: [], }; const template: Point[] = [ { x: 18, y: 0 }, { x: 1182, y: 0 }, { x: 1200, y: 18 }, { x: 0, y: 18 }, ]; const skirt: SkirtingPiece = { id: 'A1', roomId: 'room-1', wallId: 'w1', wallIndex: 0, start: { x: 0, y: 0 }, end: { x: 1164, y: 0 }, length: 1200, backLength: 1164, leftMitre: 45, rightMitre: 45, leftType: 'external', rightType: 'external', stockId: 'S1', template, }; describe('cut shape previews', () => { it('draws rectangular floor cuts too, keeping their precise measured dimensions', () => { const points = flooringTemplate(floor); expect(points).toEqual([ { x: 0, y: 0 }, { x: 600.001, y: 0 }, { x: 600.001, y: 190 }, { x: 0, y: 190 }, ]); const markup = renderToStaticMarkup(createElement(FlooringPreview, { piece: floor })); expect(markup).toContain('aria-label="Flooring shape F1"'); expect(markup).toContain('preserveAspectRatio="xMidYMid meet"'); expect(markup).toContain('600.001 × 190 mm'); expect(markup).toContain('>600.001'); expect(markup).toContain('>190'); expect(markup.match(/class="dimension-label"/g)).toHaveLength(2); expect(exactMm(600.001)).toBe('600.001'); }); it('keeps skirting endpoints in original coordinates and gives both ends equal square windows', () => { const original = structuredClone(template); const boxes = skirtingEndBoxes(template, 18); expect(boxes.left.width).toBe(boxes.left.height); expect(boxes.right.width).toBe(boxes.right.height); expect(boxes.left.width).toBe(boxes.right.width); // Equal x/y scales preserve the measured 18-by-18 mm, 45-degree cut. const scaleX = 120 / boxes.left.width, scaleY = 120 / boxes.left.height; const angle = (Math.atan2( Math.abs(template[3].y - template[0].y) * scaleY, Math.abs(template[3].x - template[0].x) * scaleX, ) * 180) / Math.PI; expect(angle).toBeCloseTo(45, 8); expect(template).toEqual(original); const markup = renderToStaticMarkup( createElement(SkirtingPreview, { piece: skirt, thickness: 18 }), ); expect(markup).toContain('points="18,0 1182,0 1200,18 0,18"'); expect(markup.match(/preserveAspectRatio="xMidYMid meet"/g)).toHaveLength(3); expect(markup).toContain('1,200 × 18 mm'); expect(markup).toContain('>1,164'); expect(markup).toContain('>1,200'); expect(markup).not.toContain('exaggerat'); }); it('labels every edge of a 320 by 100 mm notch without collisions or tiny mobile text', () => { const notch = [ { x: 0, y: 0 }, { x: 880, y: 0 }, { x: 880, y: 100 }, { x: 1200, y: 100 }, { x: 1200, y: 190 }, { x: 0, y: 190 }, ]; const original = structuredClone(notch); const preview = dimensionedOutline(notch); expect(preview.dimensions.map((dimension) => dimension.length)).toEqual([ 880, 100, 320, 90, 1200, 190, ]); expect((preview.font * 280) / preview.box.width).toBeGreaterThanOrEqual(12); preview.dimensions.forEach((dimension, i) => { const box = dimension.labelBox; expect(box.x).toBeGreaterThan(preview.box.x); expect(box.y).toBeGreaterThan(preview.box.y); expect(box.x + box.width).toBeLessThan(preview.box.x + preview.box.width); expect(box.y + box.height).toBeLessThan(preview.box.y + preview.box.height); const projection = (point: Point) => point.x * dimension.normal.x + point.y * dimension.normal.y; const nearestLabel = Math.min( ...[ { x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }, { x: box.x, y: box.y + box.height }, { x: box.x + box.width, y: box.y + box.height }, ].map(projection), ); expect(nearestLabel, 'Dimension text stays outside the shape').toBeGreaterThan( Math.max(...notch.map(projection)), ); for (const other of preview.dimensions.slice(i + 1)) { const b = other.labelBox; expect( box.x + box.width <= b.x || b.x + b.width <= box.x || box.y + box.height <= b.y || b.y + b.height <= box.y, ).toBe(true); } }); expect(notch).toEqual(original); expect(dimensionedOutline(notch)).toEqual(preview); const markup = renderToStaticMarkup( createElement(FlooringPreview, { piece: { ...floor, length: 1200, shaped: true, template: notch }, }), ); expect(markup).toContain('>320'); expect(markup).toContain('>100'); expect(markup).not.toMatch(/>[1-6]<\/text>/); }); it('measures angled edges along the cut and retains the original outline', () => { const points = [ { x: 0, y: 0 }, { x: 800, y: 0 }, { x: 920, y: 160 }, { x: 0, y: 160 }, ]; const preview = dimensionedOutline(points); expect((preview.font * 280) / preview.box.width).toBeGreaterThanOrEqual(12); expect(preview.dimensions[1].length).toBe(200); expect(preview.dimensions[1].text).toBe('200'); const angle = preview.dimensions[1].angle!; expect(angle).toBeCloseTo(53.130102, 6); const markedAngle = (Number(exactMm(angle)) * Math.PI) / 180; expect(200 * Math.cos(markedAngle)).toBeCloseTo(120, 2); expect(200 * Math.sin(markedAngle)).toBeCloseTo(160, 2); expect(preview.dimensions[1].start).toEqual({ x: 800, y: 0 }); expect(preview.dimensions[1].end).toEqual({ x: 920, y: 160 }); const reversed = dimensionedOutline([...points].reverse()); expect(reversed.dimensions.map((dimension) => dimension.length).sort()).toEqual( preview.dimensions.map((dimension) => dimension.length).sort(), ); expect(preview.dimensions.filter((dimension) => dimension.angle !== undefined)).toHaveLength(1); const markup = renderToStaticMarkup( createElement(FlooringPreview, { piece: { ...floor, length: 920, width: 160, shaped: true, template: points }, }), ); expect(markup).toContain('>53.13°'); expect(markup).toContain('angles from horizontal'); }); it('keeps dialog instructions useful without source codes or numbered coordinate lists', () => { const markup = renderToStaticMarkup( createElement(CutDialog, { floor: { ...floor, factoryLeft: true, ripEdge: 'top' }, project: defaultProject(), close: () => {}, }), ); expect(markup).toContain('Living room · Row 1'); expect(markup).toContain('Keep the left factory end; trim from the right.'); expect(markup).toContain('Rip the top long edge; keep the bottom factory edge.'); expect(markup).not.toContain('Source'); expect(markup).not.toContain('coordinates'); expect(markup).not.toContain('cut-edge-lengths'); }); it('shows skirting dimensions and mitre directions without a second measurement list', () => { const markup = renderToStaticMarkup( createElement(CutDialog, { skirt, project: defaultProject(), close: () => {}, }), ); expect(markup).toContain('Living room · Wall 1'); expect(markup).toContain('>1,164'); expect(markup).toContain('>1,200'); expect(markup).toContain('45° external'); expect(markup).not.toContain('Source'); expect(markup).not.toContain('coordinates'); expect(markup).not.toContain('