This commit is contained in:
Andras Schmelczer 2026-09-11 16:58:56 +01:00
commit 9689bfcc18
23 changed files with 1815 additions and 955 deletions

View file

@ -1,10 +1,16 @@
import { describe, expect, it } from 'vitest';
import { defaultProject } from '../src/lib/defaults';
import { flooringCuts, groupFlooringCuts } from '../src/lib/cut-list';
import { defaultProject, rectangle } from '../src/lib/defaults';
import {
flooringCuts,
groupFlooringCuts,
groupSkirtingCuts,
skirtingCuts,
} from '../src/lib/cut-list';
import { planFlooring } from '../src/lib/flooring';
import { planSkirting } from '../src/lib/skirting';
import type { FlooringPiece, FlooringPlan } from '../src/lib/types';
function piece(id: string, changes: Partial<FlooringPiece> = {}): FlooringPiece {
function piece(id: string, stockId: string, changes: Partial<FlooringPiece> = {}): FlooringPiece {
return {
id,
roomId: 'room-1',
@ -13,14 +19,11 @@ function piece(id: string, changes: Partial<FlooringPiece> = {}): FlooringPiece
polygon: [],
length: 600,
width: 190,
stockId: 'B1',
stockId,
reused: false,
cut: true,
shaped: false,
template: [],
factoryLeft: true,
factoryRight: false,
ripEdge: null,
...changes,
};
}
@ -28,8 +31,12 @@ function plan(pieces: FlooringPiece[]): FlooringPlan {
return {
pieces,
rows: [],
stocks: [],
boardCount: 0,
stocks: ['B1', 'B2', 'B3'].map((id) => ({
id,
pieceIds: pieces.filter((p) => p.stockId === id).map((p) => p.id),
remaining: 0,
})),
boardCount: 3,
cuts: 0,
wasteArea: 0,
wastePercent: 0,
@ -39,81 +46,54 @@ function plan(pieces: FlooringPiece[]): FlooringPlan {
};
}
describe('flooring cut batches', () => {
it('combines matching cuts across rows and stocks, preserving every placement exactly once', () => {
const pieces = [
piece('F1'),
piece('F2', { row: 4, stockId: 'B4', reused: true }),
piece('F3', { cut: false, length: 1200 }),
piece('F4', { cut: false, reused: true }),
piece('F5', { roomId: 'room-2' }),
];
const source = plan(pieces),
before = structuredClone(source);
describe('cuts grouped by purchased board', () => {
it('keeps different cuts and cross-room offcuts together, separating identical cuts from different boards', () => {
const source = plan([
piece('F1', 'B2'),
piece('F2', 'B1'),
piece('F3', 'B2', { roomId: 'room-2', row: 4, length: 397, reused: true }),
piece('FU1', 'B3', { length: 1200, cut: false }),
]);
const before = structuredClone(source);
const groups = groupFlooringCuts(source);
expect(groups.map((group) => group.map((p) => p.id))).toEqual([['F1', 'F2'], ['F4'], ['F5']]);
expect(
groups
.flat()
.map((p) => p.id)
.sort(),
).toEqual(
flooringCuts(source)
.map((p) => p.id)
.sort(),
);
expect(groups.map((group) => group.map((p) => p.id))).toEqual([['F2'], ['F1', 'F3']]);
expect(source).toEqual(before);
expect(groupFlooringCuts(source)).toEqual(groups);
});
it('keeps different factory ends, rip edges and thousandth-millimetre dimensions separate', () => {
it('includes ready-to-use offcuts in their original board group', () => {
const groups = groupFlooringCuts(
plan([
piece('F1'),
piece('F2', { factoryLeft: false, factoryRight: true }),
piece('F3', { ripEdge: 'top' }),
piece('F4', { ripEdge: 'bottom' }),
piece('F5', { length: 600.001 }),
piece('F6', { width: 190.001 }),
]),
plan([piece('F1', 'B1'), piece('F2', 'B1', { cut: false, reused: true })]),
);
expect(groups).toHaveLength(6);
expect(groups.map((group) => group.map((p) => p.id))).toEqual([['F1', 'F2']]);
});
it('groups shapes only when their complete measured templates match', () => {
const template = [
{ x: 0, y: 0 },
{ x: 600, y: 0 },
{ x: 600, y: 190 },
{ x: 0, y: 100 },
];
const groups = groupFlooringCuts(
plan([
piece('F1', { shaped: true, template }),
piece('F2', { shaped: true, template: structuredClone(template) }),
piece('F3', { shaped: true, template: [...template.slice(0, 3), { x: 0, y: 100.001 }] }),
piece('F4'),
]),
);
expect(groups.map((group) => group.map((p) => p.id))).toEqual([['F1', 'F2'], ['F3'], ['F4']]);
});
it('reduces the default room to a few batches while keeping all cut and offcut codes', () => {
it('includes every flooring and skirting cut once in stock order across multiple rooms', () => {
const project = defaultProject();
project.rooms.push(rectangle('room-2', 'Study', 3200, 3100));
const floor = planFlooring(project.rooms, project.flooring, project.tolerance);
const groups = groupFlooringCuts(floor);
expect(groups.length).toBeLessThanOrEqual(7);
expect(groups.length).toBeLessThan(flooringCuts(floor).length / 2);
const skirt = planSkirting(project.rooms, project.skirting, project.tolerance);
for (const [groups, pieces, stocks] of [
[groupFlooringCuts(floor), flooringCuts(floor), floor.stocks],
[groupSkirtingCuts(skirt), skirtingCuts(skirt), skirt.stocks],
] as const) {
expect(
groups
.flat()
.map((p) => p.id)
.sort(),
).toEqual(pieces.map((p) => p.id).sort());
expect(new Set(groups.flat().map((p) => p.id)).size).toBe(pieces.length);
const used = new Set(pieces.map((p) => p.stockId));
expect(groups.map((group) => group[0].stockId)).toEqual(
stocks.filter((s) => used.has(s.id)).map((s) => s.id),
);
for (const group of groups) expect(new Set(group.map((p) => p.stockId)).size).toBe(1);
}
expect(
groups
.flat()
.map((p) => p.id)
.sort(),
).toEqual(
flooringCuts(floor)
.map((p) => p.id)
.sort(),
);
expect(new Set(groups.flat().map((p) => p.id)).size).toBe(flooringCuts(floor).length);
[...groupFlooringCuts(floor), ...groupSkirtingCuts(skirt)].some(
(group) => new Set(group.map((p) => p.roomId)).size > 1,
),
).toBe(true);
});
});