This commit is contained in:
Andras Schmelczer 2026-09-11 16:09:28 +01:00
commit d8f2407ddf
44 changed files with 10377 additions and 0 deletions

78
src/lib/cut-list.ts Normal file
View file

@ -0,0 +1,78 @@
import type {
FlooringPiece,
FlooringPlan,
FlooringSettings,
SkirtingPiece,
SkirtingPlan,
} from './types';
const sameLength = (a: number, b: number) => Math.abs(a - b) < 0.01;
/** A ready offcut still needs a placement code; only fresh, uncut whole boards are omitted. */
export const flooringCuts = (plan: FlooringPlan) =>
plan.pieces.filter((piece) => piece.cut || piece.reused);
export function isSkirtingCut(piece: SkirtingPiece, plan: SkirtingPlan): boolean {
const stock = plan.stocks.find((candidate) => candidate.id === piece.stockId);
return (
!stock ||
!sameLength(piece.length, stock.length) ||
Math.abs(piece.leftMitre) > 0.001 ||
Math.abs(piece.rightMitre) > 0.001
);
}
export const skirtingCuts = (plan: SkirtingPlan) =>
plan.pieces.filter((piece) => isSkirtingCut(piece, plan));
export function uncutFlooring(plan: FlooringPlan, settings: FlooringSettings) {
const uncut = plan.pieces.filter((piece) => !piece.cut);
const wholeBoards = uncut.filter(
(piece) => sameLength(piece.length, settings.length) && sameLength(piece.width, settings.width),
).length;
return { wholeBoards, readyOffcuts: uncut.length - wholeBoards };
}
export function uncutSkirting(plan: SkirtingPlan): { length: number; count: number }[] {
const counts = new Map<number, number>();
for (const piece of plan.pieces) {
if (!isSkirtingCut(piece, plan)) counts.set(piece.length, (counts.get(piece.length) ?? 0) + 1);
}
return [...counts].sort(([a], [b]) => a - b).map(([length, count]) => ({ length, count }));
}
/** Ready remnants need placement instructions, never another trimming instruction. */
export function readyOffcutInstructions(piece: FlooringPiece): string {
const ends =
piece.factoryLeft && piece.factoryRight
? 'both'
: piece.factoryLeft
? 'left'
: piece.factoryRight
? 'right'
: 'neither';
return `Use offcut; no new cut. Required factory short ends: ${ends}.`;
}
/** Batch identical cuts without losing any placement codes or changing factory-edge orientation. */
export function groupFlooringCuts(plan: FlooringPlan): FlooringPiece[][] {
const groups = new Map<string, FlooringPiece[]>();
const precise = (value: number) => Math.round(value * 1000);
for (const piece of flooringCuts(plan)) {
const key = JSON.stringify([
piece.roomId,
precise(piece.length),
precise(piece.width),
piece.shaped,
piece.cut,
piece.factoryLeft,
piece.factoryRight,
piece.ripEdge,
piece.shaped ? piece.template.map((point) => [precise(point.x), precise(point.y)]) : null,
]);
const group = groups.get(key);
if (group) group.push(piece);
else groups.set(key, [piece]);
}
return [...groups.values()];
}