better
This commit is contained in:
parent
b9485e4097
commit
0c0f08b350
45 changed files with 1565 additions and 1579 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { buildGeometry, normalizeAngle, signedArea } from './geometry';
|
||||
import { isSkirtingCut } from './cut-list';
|
||||
import { cutLength } from './format';
|
||||
import { PLANNER_LIMITS, PRECISION, PROJECT_LIMITS } from './limits';
|
||||
import type {
|
||||
Issue,
|
||||
Point,
|
||||
|
|
@ -12,7 +13,8 @@ import type {
|
|||
WallEdge,
|
||||
} from './types';
|
||||
|
||||
const EPS = 1e-6;
|
||||
const EPS = PRECISION.geometry;
|
||||
type UnpackedPiece = Omit<SkirtingPiece, 'blankLength'>;
|
||||
const rad = (degrees: number) => (degrees * Math.PI) / 180;
|
||||
type End = {
|
||||
type: SkirtingPiece['leftType'];
|
||||
|
|
@ -117,7 +119,7 @@ function corner(turn: number, thickness: number, side: 'left' | 'right', cope: b
|
|||
};
|
||||
}
|
||||
|
||||
function endCuts(piece: SkirtingPiece) {
|
||||
function endCuts(piece: UnpackedPiece) {
|
||||
return (
|
||||
Number(Math.abs(piece.leftMitre) > EPS) +
|
||||
Number(Math.abs(piece.rightMitre) > EPS) +
|
||||
|
|
@ -126,15 +128,15 @@ function endCuts(piece: SkirtingPiece) {
|
|||
);
|
||||
}
|
||||
|
||||
function blankLength(piece: SkirtingPiece, available: number) {
|
||||
function blankLength(piece: UnpackedPiece, available: number) {
|
||||
return Math.abs(available - piece.length) <= EPS ? available : cutLength(piece.length);
|
||||
}
|
||||
|
||||
function requiredCuts(piece: SkirtingPiece, available: number) {
|
||||
function requiredCuts(piece: UnpackedPiece, available: number) {
|
||||
return endCuts(piece) + Number(Math.abs(available - piece.length) > EPS);
|
||||
}
|
||||
|
||||
function consumedLength(piece: SkirtingPiece, available: number, kerf: number) {
|
||||
function consumedLength(piece: UnpackedPiece, available: number, kerf: number) {
|
||||
// Cut a square blank, then bevel within its longest-point envelope. This leaves
|
||||
// a square reusable offcut. Bevel kerfs remove material inside the blank.
|
||||
const length = blankLength(piece, available);
|
||||
|
|
@ -147,7 +149,7 @@ function makePiece(
|
|||
to: number,
|
||||
first: boolean,
|
||||
last: boolean,
|
||||
): SkirtingPiece {
|
||||
): UnpackedPiece {
|
||||
const left = first ? run.left : square(),
|
||||
right = last ? run.right : square();
|
||||
const backFrom = from + left.back,
|
||||
|
|
@ -189,16 +191,16 @@ function makePiece(
|
|||
};
|
||||
}
|
||||
|
||||
function shortestEdge(piece: SkirtingPiece) {
|
||||
const installed = piece.installedTemplate!;
|
||||
function shortestEdge(piece: UnpackedPiece) {
|
||||
const installed = piece.installedTemplate;
|
||||
return Math.min(
|
||||
installed[1].x - installed[0].x,
|
||||
installed[2].x - installed[3].x,
|
||||
piece.visibleLength!,
|
||||
piece.visibleLength,
|
||||
);
|
||||
}
|
||||
|
||||
function fits(piece: SkirtingPiece, available: number) {
|
||||
function fits(piece: UnpackedPiece, available: number) {
|
||||
// The final saw pass may extend beyond the stock's tail.
|
||||
return blankLength(piece, available) <= available + EPS;
|
||||
}
|
||||
|
|
@ -211,7 +213,7 @@ function splitCount(run: Run, target: number, minimum: number): number {
|
|||
const count = Math.max(2, Math.ceil((full.length - EPS) / target));
|
||||
const minimumSpan = count * minimum + firstAllowance(run.left) - lastAllowance(run.right);
|
||||
if (
|
||||
count > 10000 ||
|
||||
count > PLANNER_LIMITS.skirtingPieces ||
|
||||
run.to - run.from < minimumSpan - EPS ||
|
||||
firstAllowance(run.left) - Math.min(run.left.back, run.left.front) > target - minimum + EPS ||
|
||||
Math.max(run.right.back, run.right.front) - lastAllowance(run.right) > target - minimum + EPS
|
||||
|
|
@ -226,7 +228,7 @@ function splitRun(
|
|||
target: number,
|
||||
settings: SkirtingSettings,
|
||||
balanced: boolean,
|
||||
): SkirtingPiece[] | null {
|
||||
): UnpackedPiece[] | null {
|
||||
const count = splitCount(run, target, settings.minLength);
|
||||
if (!count) return null;
|
||||
if (count === 1) return [makePiece(run, run.from, run.to, true, true)];
|
||||
|
|
@ -291,8 +293,8 @@ function splitWithOffcuts(
|
|||
runs: Run[],
|
||||
target: number,
|
||||
settings: SkirtingSettings,
|
||||
): SkirtingPiece[] | null {
|
||||
const pieces: SkirtingPiece[] = [];
|
||||
): UnpackedPiece[] | null {
|
||||
const pieces: UnpackedPiece[] = [];
|
||||
const offcuts: number[] = [];
|
||||
for (const run of runs) {
|
||||
if (!splitCount(run, target, settings.minLength)) return null;
|
||||
|
|
@ -322,7 +324,7 @@ function splitWithOffcuts(
|
|||
bFits = fits(full, b);
|
||||
return aFits !== bFits ? Number(bFits) - Number(aFits) : aFits ? a - b : b - a;
|
||||
});
|
||||
let next: { piece: SkirtingPiece; to: number } | null = null;
|
||||
let next: { piece: UnpackedPiece; to: number } | null = null;
|
||||
let available = target;
|
||||
for (let index = 0; index < offcuts.length; index++) {
|
||||
next = take(offcuts[index]);
|
||||
|
|
@ -332,7 +334,7 @@ function splitWithOffcuts(
|
|||
}
|
||||
}
|
||||
next ??= take(target);
|
||||
if (!next || pieces.length >= 10000) return null;
|
||||
if (!next || pieces.length >= PLANNER_LIMITS.skirtingPieces) return null;
|
||||
pieces.push(next.piece);
|
||||
const tail = available - consumedLength(next.piece, available, settings.kerf);
|
||||
if (tail >= settings.minLength - EPS) offcuts.push(tail);
|
||||
|
|
@ -349,14 +351,14 @@ interface Packed {
|
|||
waste: number;
|
||||
}
|
||||
|
||||
/** Best-fit decreasing across all rooms, using one purchased board length. */
|
||||
/** Greedy decreasing packing, optionally reserving exact three-piece combinations. */
|
||||
function pack(
|
||||
source: SkirtingPiece[],
|
||||
source: UnpackedPiece[],
|
||||
stockLength: number,
|
||||
kerf: number,
|
||||
exactTails = false,
|
||||
): Packed {
|
||||
const pieces = source.map((piece) => ({ ...piece }));
|
||||
const pieces: SkirtingPiece[] = source.map((piece) => ({ ...piece, blankLength: 0 }));
|
||||
let pending = [...pieces].sort(
|
||||
(a, b) => b.length - a.length || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
||||
);
|
||||
|
|
@ -425,16 +427,16 @@ export function planSkirting(
|
|||
if (
|
||||
!Number.isFinite(settings.stockLength) ||
|
||||
settings.stockLength < 0.1 ||
|
||||
settings.stockLength > 1e6 ||
|
||||
settings.stockLength > PROJECT_LIMITS.maxMeasurement ||
|
||||
!Number.isFinite(settings.thickness) ||
|
||||
settings.thickness < 0 ||
|
||||
settings.thickness > 10000 ||
|
||||
settings.thickness > PROJECT_LIMITS.maxSkirtingThickness ||
|
||||
!Number.isFinite(settings.minLength) ||
|
||||
settings.minLength < 0.1 ||
|
||||
settings.minLength > 1e6 ||
|
||||
settings.minLength > PROJECT_LIMITS.maxMeasurement ||
|
||||
!Number.isFinite(settings.kerf) ||
|
||||
settings.kerf < 0 ||
|
||||
settings.kerf > 10000
|
||||
settings.kerf > PROJECT_LIMITS.maxSkirtingKerf
|
||||
) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
|
|
@ -445,7 +447,7 @@ export function planSkirting(
|
|||
}
|
||||
const stockLength = settings.stockLength;
|
||||
let runs: Run[] = [];
|
||||
const fallbackSplits = new Map<Run, SkirtingPiece[]>();
|
||||
const fallbackSplits = new Map<Run, UnpackedPiece[]>();
|
||||
rooms.forEach((room, roomIndex) => {
|
||||
const geometry = buildGeometry(room, tolerance);
|
||||
if (!geometry.valid) {
|
||||
|
|
@ -533,7 +535,7 @@ export function planSkirting(
|
|||
|
||||
const candidates: Packed[] = [];
|
||||
const prefixes = new Map(runs.map((run) => [run.roomId, run.prefix]));
|
||||
const addCandidate = (source: SkirtingPiece[]) => {
|
||||
const addCandidate = (source: UnpackedPiece[]) => {
|
||||
// Raw spans can fit while their displayed half-millimetre blanks do not.
|
||||
// Reject this split without losing other feasible joint arrangements.
|
||||
if (
|
||||
|
|
@ -587,7 +589,7 @@ export function planSkirting(
|
|||
issues.push({
|
||||
severity: 'info',
|
||||
message:
|
||||
'Deterministic stock packing shares offcuts across rooms. It compares full-stock, balanced and offcut-led joins, then prefers less waste and fewer cuts; it is a heuristic, not a guaranteed global optimum. Cut square blanks to the longest-point lengths, then prepare their mitres and cope marked ends. Each cope counts one additional shaping operation; profile-specific relief passes are not estimated. Crosscut kerf is allowed between blanks; bevel kerfs stay inside the blanks. Waste is unused stock length including crosscut kerf.',
|
||||
'Deterministic stock packing shares offcuts across rooms. It compares full-stock, balanced and offcut-led joins, prioritising fewer cuts and then less waste. Exact combinations can save a trimming pass. This is a heuristic, not a guaranteed global optimum. Cut square blanks to the longest-point lengths, then prepare their mitres and cope marked ends. Each cope counts one additional shaping operation; profile-specific relief passes are not estimated. Crosscut kerf is allowed between blanks; bevel kerfs stay inside the blanks. Waste is unused stock length including crosscut kerf.',
|
||||
});
|
||||
const plan = { ...best, stockCount: best.stocks.length, issues };
|
||||
const cutNumbers = new Map<string, number>();
|
||||
|
|
|
|||
Loading…
Reference in a new issue