193 lines
6.3 KiB
TypeScript
193 lines
6.3 KiB
TypeScript
import type { Point } from './types';
|
|
import { cutMm, degree } from './format';
|
|
import { PRECISION } from './limits';
|
|
|
|
export interface PreviewBox {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
export const viewBox = (box: PreviewBox) => `${box.x} ${box.y} ${box.width} ${box.height}`;
|
|
|
|
export interface EdgeDimension {
|
|
edge: number;
|
|
length: number;
|
|
text: string;
|
|
/** Acute angle to the horizontal board edge; the diagram supplies its slope direction. */
|
|
angle?: number;
|
|
start: Point;
|
|
end: Point;
|
|
lineStart: Point;
|
|
lineEnd: Point;
|
|
normal: Point;
|
|
label: Point;
|
|
labelBox: PreviewBox;
|
|
}
|
|
|
|
const moved = (point: Point, direction: Point, distance: number): Point => ({
|
|
x: point.x + direction.x * distance,
|
|
y: point.y + direction.y * distance,
|
|
});
|
|
const overlaps = (a: PreviewBox, b: PreviewBox, gap: number) =>
|
|
a.x < b.x + b.width + gap &&
|
|
a.x + a.width + gap > b.x &&
|
|
a.y < b.y + b.height + gap &&
|
|
a.y + a.height + gap > b.y;
|
|
|
|
/** Dimensions stay outside the outline. Short adjacent edges share a lane with staggered text. */
|
|
export function dimensionedOutline(
|
|
points: Point[],
|
|
edges?: number[],
|
|
blank: { length?: number; width?: number } = {},
|
|
) {
|
|
if (points.length < 3)
|
|
return { dimensions: [] as EdgeDimension[], font: 1, box: fullPreviewBox(1, 1) };
|
|
const span = Math.max(
|
|
1,
|
|
Math.max(...points.map((point) => point.x)) - Math.min(...points.map((point) => point.x)),
|
|
Math.max(...points.map((point) => point.y)) - Math.min(...points.map((point) => point.y)),
|
|
);
|
|
const font = span * 0.065;
|
|
const orientation =
|
|
Math.sign(
|
|
points.reduce((sum, point, i) => {
|
|
const next = points[(i + 1) % points.length];
|
|
return sum + point.x * next.y - next.x * point.y;
|
|
}, 0),
|
|
) || 1;
|
|
const dimensions: EdgeDimension[] = [];
|
|
const selected = (edges ?? points.map((_, i) => i))
|
|
.map((edge) => ({
|
|
edge,
|
|
start: points[edge],
|
|
end: points[(edge + 1) % points.length],
|
|
}))
|
|
.map((edge) => ({
|
|
...edge,
|
|
length: Math.hypot(edge.end.x - edge.start.x, edge.end.y - edge.start.y),
|
|
}))
|
|
.filter((edge) => edge.length > PRECISION.geometry)
|
|
.sort((a, b) => b.length - a.length || a.edge - b.edge);
|
|
|
|
for (const edge of selected) {
|
|
const direction = {
|
|
x: (edge.end.x - edge.start.x) / edge.length,
|
|
y: (edge.end.y - edge.start.y) / edge.length,
|
|
};
|
|
const normal = { x: orientation * direction.y, y: -orientation * direction.x };
|
|
const center = { x: (edge.start.x + edge.end.x) / 2, y: (edge.start.y + edge.end.y) / 2 };
|
|
const limit =
|
|
Math.abs(direction.y) < PRECISION.geometry
|
|
? blank.length
|
|
: Math.abs(direction.x) < PRECISION.geometry
|
|
? blank.width
|
|
: undefined;
|
|
const text = cutMm(edge.length, limit);
|
|
const angle =
|
|
Math.abs(direction.x) > PRECISION.geometry && Math.abs(direction.y) > PRECISION.geometry
|
|
? (Math.atan2(Math.abs(direction.y), Math.abs(direction.x)) * 180) / Math.PI
|
|
: undefined;
|
|
const angleText = angle === undefined ? '' : degree(angle);
|
|
const width = Math.max(font * 1.2, Math.max(text.length, angleText.length) * font * 0.64),
|
|
height = font * (angle === undefined ? 1.3 : 2.5);
|
|
const outside = Math.max(
|
|
0,
|
|
...points.map((point) => (point.x - center.x) * normal.x + (point.y - center.y) * normal.y),
|
|
);
|
|
let offset = outside + font * 0.65;
|
|
let label = center,
|
|
labelBox: PreviewBox = { x: 0, y: 0, width, height };
|
|
let placed = false;
|
|
for (let lane = 0; lane < 30 && !placed; lane++) {
|
|
const base = moved(
|
|
center,
|
|
normal,
|
|
offset + (Math.abs(normal.x) * width + Math.abs(normal.y) * height) / 2 + font * 0.18,
|
|
);
|
|
for (let step = 0; step < 25; step++) {
|
|
const shift = step === 0 ? 0 : Math.ceil(step / 2) * (step % 2 ? -1 : 1) * font * 1.55;
|
|
label = moved(base, direction, shift);
|
|
labelBox = { x: label.x - width / 2, y: label.y - height / 2, width, height };
|
|
if (!dimensions.some((dimension) => overlaps(labelBox, dimension.labelBox, font * 0.25))) {
|
|
placed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!placed) offset += font * 1.8;
|
|
}
|
|
dimensions.push({
|
|
...edge,
|
|
text,
|
|
angle,
|
|
normal,
|
|
lineStart: moved(edge.start, normal, offset),
|
|
lineEnd: moved(edge.end, normal, offset),
|
|
label,
|
|
labelBox,
|
|
});
|
|
}
|
|
const xs = points.map((point) => point.x),
|
|
ys = points.map((point) => point.y);
|
|
for (const dimension of dimensions) {
|
|
xs.push(
|
|
dimension.lineStart.x,
|
|
dimension.lineEnd.x,
|
|
dimension.labelBox.x,
|
|
dimension.labelBox.x + dimension.labelBox.width,
|
|
);
|
|
ys.push(
|
|
dimension.lineStart.y,
|
|
dimension.lineEnd.y,
|
|
dimension.labelBox.y,
|
|
dimension.labelBox.y + dimension.labelBox.height,
|
|
);
|
|
}
|
|
const padding = font * 0.4;
|
|
return {
|
|
dimensions: dimensions.sort((a, b) => a.edge - b.edge),
|
|
font,
|
|
box: {
|
|
x: Math.min(...xs) - padding,
|
|
y: Math.min(...ys) - padding,
|
|
width: Math.max(...xs) - Math.min(...xs) + padding * 2,
|
|
height: Math.max(...ys) - Math.min(...ys) + padding * 2,
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Padding changes the viewport only; all vertices stay in their original millimetres. */
|
|
export function fullPreviewBox(length: number, width: number): PreviewBox {
|
|
const padding = Math.max(length, width, 1) * 0.06;
|
|
return { x: -padding, y: -padding, width: length + 2 * padding, height: width + 2 * padding };
|
|
}
|
|
|
|
/** Equal square windows enlarge both axes equally; a 45° mitre remains 45°. */
|
|
export function skirtingEndBoxes(
|
|
template: Point[],
|
|
thickness: number,
|
|
installed?: Point[],
|
|
): { left: PreviewBox; right: PreviewBox } {
|
|
const left = [
|
|
template[0].x,
|
|
template[3].x,
|
|
...(installed ? [installed[0].x, installed[3].x] : []),
|
|
];
|
|
const right = [
|
|
template[1].x,
|
|
template[2].x,
|
|
...(installed ? [installed[1].x, installed[2].x] : []),
|
|
];
|
|
const side = Math.max(
|
|
1,
|
|
thickness * 3,
|
|
(Math.max(...left) - Math.min(...left)) / 0.76,
|
|
(Math.max(...right) - Math.min(...right)) / 0.76,
|
|
);
|
|
const padding = side * 0.12;
|
|
const common = { y: (thickness - side) / 2, width: side, height: side };
|
|
return {
|
|
left: { ...common, x: Math.min(...left) - padding },
|
|
right: { ...common, x: Math.max(...right) - side + padding },
|
|
};
|
|
}
|