This commit is contained in:
Andras Schmelczer 2026-09-11 22:14:24 +01:00
commit 0c0f08b350
45 changed files with 1565 additions and 1579 deletions

View file

@ -13,6 +13,7 @@ const settings: FlooringSettings = {
width: 200,
gap: 10,
pattern: 'brick',
optimisation: 'cuts',
minOffset: 300,
maxOffset: 600,
minLength: 200,
@ -68,6 +69,18 @@ function expectStockFits(plan: FlooringPlan, s: FlooringSettings) {
expect(used + Math.max(0, pieces.length - 1) * s.kerf).toBeLessThanOrEqual(s.length + 0.02);
expect(stock.remaining).toBeGreaterThanOrEqual(0);
expect(used + stock.remaining).toBeLessThanOrEqual(s.length + 0.02);
// Crosscuts cannot create factory joints: each purchased board supplies one of each end.
for (const end of ['factoryLeft', 'factoryRight'] as const) {
expect(
pieces.filter((piece) => piece[end]).length,
`${stock.id} allocates its ${end} more than once`,
).toBeLessThanOrEqual(1);
}
for (const piece of pieces.filter((piece) => piece.factoryLeft && piece.factoryRight)) {
expect(piece.length).toBeCloseTo(s.length, 3);
expect(piece.reused).toBe(false);
expect(pieces).toHaveLength(1);
}
}
}
function expectOffsets(plan: FlooringPlan, s: FlooringSettings) {
@ -137,19 +150,82 @@ describe('laminate planning', () => {
);
});
it('does not reuse a cut end where an interior board needs two factory ends', () => {
const plan = planFlooring([room()], settings, 1);
for (const row of plan.rows) {
const pieces = row.pieceIds
.map((id) => plan.pieces.find((piece) => piece.id === id)!)
.sort((a, b) => a.column - b.column);
for (const interior of pieces.slice(1, -1)) {
expect(interior.length).toBe(settings.length);
expect(interior.reused).toBe(false);
expect(interior.factoryLeft).toBe(true);
expect(interior.factoryRight).toBe(true);
}
}
describe.each([
['brick', 'cuts'],
['random', 'cuts'],
['random', 'random'],
] as const)('%s pattern, %s optimisation: factory short ends', (pattern, optimisation) => {
const s = { ...settings, pattern, optimisation };
it.each([0, 90] as const)(
'never packs three row ends onto one board, with boards running at %i degrees',
(direction) => {
const r = direction === 0 ? room(3920, 1620) : room(1620, 3920);
r.direction = direction;
const plan = planFlooring([r], s, 1);
expect(errors(plan)).toEqual([]);
expect(plan.rows).toHaveLength(8);
expect(plan.reusedCount).toBeGreaterThan(0);
for (const row of plan.rows) {
const position = (polygon: Point[]) =>
Math.min(...polygon.map((point) => (direction === 0 ? point.x : point.y)));
const pieces = row.pieceIds
.map((id) => plan.pieces.find((piece) => piece.id === id)!)
.sort((a, b) => position(a.polygon) - position(b.polygon));
expect(pieces.length).toBeGreaterThan(2);
// Derive the required joints from installed positions, independently of allocation flags.
pieces.forEach((piece, index) => {
expect(piece.factoryLeft, `${piece.id}: left joint`).toBe(index > 0);
expect(piece.factoryRight, `${piece.id}: right joint`).toBe(index < pieces.length - 1);
});
}
const shortestEnds = plan.pieces
.filter((piece) => piece.factoryLeft !== piece.factoryRight)
.map((piece) => piece.length)
.sort((a, b) => a - b)
.slice(0, 3);
expect(shortestEnds).toHaveLength(3);
// Three ends fit by length, including kerf, but cannot share only two factory joints.
expect(shortestEnds.reduce((sum, length) => sum + length, 0) + 2 * s.kerf).toBeLessThan(
s.length,
);
for (const stock of plan.stocks) expect(stock.pieceIds.length).toBeLessThanOrEqual(2);
expectStockFits(plan, s);
expectCoverage(plan, r, s.gap);
},
);
it('allows three pieces when the middle cut spans wall to wall and the outer cuts keep opposite ends', () => {
const first = room(1620, 220, 'first');
const middle = room(220, 414, 'middle');
middle.direction = 90;
const last = room(1620, 220, 'last');
const rooms = [first, middle, last];
const plan = planFlooring(rooms, s, 1);
expect(errors(plan)).toEqual([]);
const triples = plan.stocks.filter((stock) => stock.pieceIds.length === 3);
expect(triples).toHaveLength(1);
const pieces = triples[0].pieceIds.map((id) => plan.pieces.find((piece) => piece.id === id)!);
expect(
pieces.map((piece) => ({
room: piece.roomId,
length: piece.length,
left: piece.factoryLeft,
right: piece.factoryRight,
})),
).toEqual([
{ room: 'first', length: 400, left: true, right: false },
{ room: 'middle', length: 394, left: false, right: false },
{ room: 'last', length: 400, left: false, right: true },
]);
expect(plan.pieces.filter((piece) => piece.roomId === middle.id)).toEqual([pieces[1]]);
expect(pieces.map((piece) => piece.reused)).toEqual([false, true, true]);
expect(plan.boardCount).toBe(3);
expect(plan.cuts).toBe(2);
expect(triples[0].remaining).toBe(0);
expectStockFits(plan, s);
for (const r of rooms) expectCoverage(plan, r, s.gap);
});
});
it('produces exact concave fireplace templates and keeps the gap at its external corners', () => {