This commit is contained in:
Andras Schmelczer 2026-09-11 17:57:03 +01:00
commit 7959fe85cc
29 changed files with 1679 additions and 498 deletions

View file

@ -5,6 +5,7 @@ import { planFlooring } from '../src/lib/flooring';
import { buildGeometry, insetPolygon, signedArea } from '../src/lib/geometry';
import { fireplace, rectangle } from '../src/lib/defaults';
import { flooringCuts } from '../src/lib/cut-list';
import { cutLength } from '../src/lib/format';
import type { FlooringPlan, FlooringSettings, Point, Project, Room } from '../src/lib/types';
const settings: FlooringSettings = {
@ -369,4 +370,106 @@ describe('laminate planning', () => {
});
});
it('temporary F8 audit',()=>{const project=f8Fixture.project as Project;const plan=planFlooring(project.rooms,project.flooring,project.tolerance);const atCorner=plan.pieces.filter(p=>p.polygon.some(v=>Math.hypot(v.x-7607.513,v.y-2160.340)<0.01));throw new Error(JSON.stringify({atCorner,first:plan.rows[0],last:plan.rows.at(-1),boards:plan.boardCount,cuts:plan.cuts,area:plan.coveredArea,insetArea:insetPolygon(buildGeometry(project.rooms[0],project.tolerance).vertices,10).reduce((a,p)=>a+Math.abs(signedArea(p)),0)}));});
describe('angled room cut allocation', () => {
const project = f8Fixture.project as Project;
const plan = planFlooring(project.rooms, project.flooring, project.tolerance);
it('aligns a row seam with the inward corner that previously left F8 a thin toe', () => {
expect(errors(plan)).toEqual([]);
expect(project.rooms[0].walls).toHaveLength(13);
expect(project.rooms[0].walls.flatMap((wall) => wall.doors)).toHaveLength(2);
const corner = { x: 7607.513, y: 2160.34 };
const adjoining = plan.pieces.filter((piece) =>
piece.polygon.some((point) => Math.hypot(point.x - corner.x, point.y - corner.y) < 0.01),
);
expect(adjoining.length).toBeGreaterThanOrEqual(2);
expect(new Set(adjoining.map((piece) => piece.row)).size).toBe(2);
for (const piece of adjoining) {
// Boards run north/south: an x-boundary is the uncut factory long-edge seam.
const xs = piece.polygon.map((point) => point.x);
expect(
Math.min(Math.abs(Math.min(...xs) - corner.x), Math.abs(Math.max(...xs) - corner.x)),
).toBeLessThan(0.01);
const turns = piece.polygon.map((point, index, polygon) => {
const previous = polygon[(index + polygon.length - 1) % polygon.length];
const next = polygon[(index + 1) % polygon.length];
return (
(point.x - previous.x) * (next.y - point.y) - (point.y - previous.y) * (next.x - point.x)
);
});
expect(turns.every((turn) => turn >= -0.01) || turns.every((turn) => turn <= 0.01)).toBe(
true,
);
}
expectCoverage(plan, project.rooms[0], project.flooring.gap);
expectOffsets(plan, project.flooring);
expectStockFits(plan, project.flooring);
expect(plan).toEqual(planFlooring(project.rooms, project.flooring, project.tolerance));
});
it('fits all displayed half-millimetre blanks and their kerfs on each purchased board', () => {
expect(errors(plan)).toEqual([]);
expect(plan.pieces.some((piece) => cutLength(piece.length) > piece.length + 0.01)).toBe(true);
for (const stock of plan.stocks) {
const pieces = stock.pieceIds.map((id) => plan.pieces.find((piece) => piece.id === id)!);
const rounded = pieces.reduce((sum, piece) => sum + cutLength(piece.length), 0);
expect(
rounded + (pieces.length - 1) * project.flooring.kerf,
`${stock.id}: ${pieces.map((piece) => `${piece.id}=${cutLength(piece.length)}`).join(', ')}`,
).toBeLessThanOrEqual(project.flooring.length + 0.001);
}
});
it('preserves whole factory boards whose purchased length is not a half millimetre', () => {
const s = {
...settings,
length: 1200.2,
minOffset: 0,
maxOffset: 1200.2,
pattern: 'random' as const,
};
const r = room(1220.2, 220);
const fitted = planFlooring([r], s, 1);
expect(errors(fitted)).toEqual([]);
expect(fitted.boardCount).toBe(1);
expect(fitted.cuts).toBe(0);
expect(fitted.pieces[0].length).toBe(1200.2);
expect(fitted.pieces[0].blankLength).toBe(1200.2);
expect(fitted.stocks[0].remaining).toBe(0);
expectCoverage(fitted, r, s.gap);
});
it('uses an exact-fit fractional offcut without pretending it needs a new cut', () => {
const s = { ...settings, length: 1200.2, pattern: 'random' as const };
const fitted = planFlooring([room(620, 220, 'first'), room(617.2, 220, 'second')], s, 1);
expect(errors(fitted)).toEqual([]);
expect(fitted.boardCount).toBe(1);
expect(fitted.cuts).toBe(1);
expect(fitted.pieces.map((piece) => piece.length)).toEqual([600, 597.2]);
expect(fitted.pieces.map((piece) => piece.blankLength)).toEqual([600, 597.2]);
expect(fitted.pieces[1].reused).toBe(true);
expect(fitted.pieces[1].cut).toBe(false);
expect(fitted.stocks[0].remaining).toBe(0);
expectStockFits(fitted, s);
});
it('keeps a finishing cut visible when its rounded blank consumes a complete board', () => {
const s = { ...settings, pattern: 'random' as const };
const fitted = planFlooring([room(1219.6, 220)], s, 1);
expect(errors(fitted)).toEqual([]);
expect(fitted.boardCount).toBe(1);
expect(fitted.pieces[0].length).toBe(1199.6);
expect(fitted.pieces[0].cut).toBe(true);
expect(fitted.cuts).toBe(1);
expect(fitted.stocks[0].remaining).toBe(0);
});
it('caps a rounded rip blank at the available fractional factory width', () => {
const s = { ...settings, width: 200.2, pattern: 'random' as const };
const fitted = planFlooring([room(1220, 220.1)], s, 1);
expect(errors(fitted)).toEqual([]);
expect(fitted.boardCount).toBe(1);
expect(fitted.pieces[0].width).toBe(200.1);
expect(fitted.pieces[0].blankWidth).toBe(200.2);
expect(fitted.pieces[0].cut).toBe(true);
});
});