saxi
Version:
Drive the AxiDraw pen plotter
173 lines • 6.12 kB
JavaScript
import { vadd, vlen2, vmul, vsub } from "./vec.js";
/** Format a smallish duration in 2h30m15s form */
export function formatDuration(seconds) {
const hours = Math.floor(seconds / 60 / 60);
const mins = Math.floor((seconds - hours * 60 * 60) / 60);
const secs = Math.floor(seconds - hours * 60 * 60 - mins * 60);
const parts = [
[hours, "h"],
[mins, "m"],
[secs, "s"],
];
return parts
.slice(parts.findIndex((x) => x[0] !== 0))
.map(([v, u]) => `${v}${u}`)
.join("");
}
/** Return the top-left and bottom-right corners of the bounding box containing all points in pointLists */
function extent(pointLists) {
let maxX = -Infinity;
let maxY = -Infinity;
let minX = Infinity;
let minY = Infinity;
for (const pl of pointLists) {
for (const p of pl) {
if (p.x > maxX) {
maxX = p.x;
}
if (p.y > maxY) {
maxY = p.y;
}
if (p.x < minX) {
minX = p.x;
}
if (p.y < minY) {
minY = p.y;
}
} // biome-ignore format: compactness
}
return [{ x: minX, y: minY }, { x: maxX, y: maxY }]; // biome-ignore format: compactness
}
/**
* Scale pointLists to fit within the bounding box specified by (targetMin, targetMax).
*
* Preserves aspect ratio, scaling as little as possible to completely fit within the box.
*
* Also centers the paths within the box.
*/
function scaleToFit(pointLists, targetMin, targetMax) {
const [min, max] = extent(pointLists);
const availWidthMm = targetMax.x - targetMin.x;
const availHeightMm = targetMax.y - targetMin.y;
const scaleFitX = availWidthMm / (max.x - min.x);
const scaleFitY = availHeightMm / (max.y - min.y);
const scale = Math.min(scaleFitX, scaleFitY);
const targetCenter = vadd(targetMin, vmul(vsub(targetMax, targetMin), 0.5));
const offset = vsub(targetCenter, vmul(vsub(max, min), scale * 0.5));
return pointLists.map((pl) => pl.map((p) => vadd(vmul(vsub(p, min), scale), offset)));
}
/** Scale a drawing to fill a piece of paper, with the given size and margins. */
export function scaleToPaper(pointLists, paperSize, marginMm) {
return scaleToFit(pointLists, { x: marginMm, y: marginMm }, vsub(paperSize.size, { x: marginMm, y: marginMm }));
}
/**
* Liang-Barsky algorithm for computing segment-AABB intersection.
* https://gist.github.com/ChickenProp/3194723
*/
function liangBarsky(aabb, seg) {
const [lower, upper] = aabb;
const [a, b] = seg;
const delta = vsub(b, a);
const p = [-delta.x, delta.x, -delta.y, delta.y];
const q = [a.x - lower.x, upper.x - a.x, a.y - lower.y, upper.y - a.y];
let u1 = -Infinity;
let u2 = Infinity;
for (let i = 0; i < 4; i++) {
if (p[i] === 0) {
if (q[i] < 0)
return null;
}
else {
const t = q[i] / p[i];
if (p[i] < 0 && u1 < t)
u1 = t;
else if (p[i] > 0 && u2 > t)
u2 = t;
}
}
if (u1 > u2 || u1 > 1 || u1 < 0)
return null;
return vadd(a, vmul(delta, u1));
}
/**
* Returns true if aabb contains point (edge-inclusive).
*/
function contains(aabb, point) {
const [lower, upper] = aabb;
return point.x >= lower.x && point.x <= upper.x && point.y >= lower.y && point.y <= upper.y;
}
/**
* Returns a segment that is the subset of seg which is completely contained
* within aabb, or null if seg is outside aabb.
*/
function truncate(aabb, seg) {
const [a, b] = seg;
const containsA = contains(aabb, a);
const containsB = contains(aabb, b);
if (containsA && containsB)
return seg;
if (containsA && !containsB)
return [seg[0], liangBarsky(aabb, [seg[1], seg[0]])];
if (!containsA && containsB)
return [liangBarsky(aabb, seg), seg[1]];
const forwards = liangBarsky(aabb, seg);
const backwards = liangBarsky(aabb, [seg[1], seg[0]]);
return forwards && backwards ? [forwards, backwards] : null;
}
/**
* Given a polyline, returns a list of polylines that form a subset of the
* input polyline that is completely within aabb.
*/
function cropLineToAabb(pointList, aabb) {
const truncatedPointLists = [];
let currentPointList = null;
for (let i = 1; i < pointList.length; i++) {
const [a, b] = [pointList[i - 1], pointList[i]];
const truncated = truncate(aabb, [a, b]);
if (truncated) {
if (!currentPointList) {
currentPointList = [truncated[0]];
truncatedPointLists.push(currentPointList);
}
currentPointList.push(truncated[1]);
if (truncated[1] !== b) {
// the end was truncated, record the end point and end the line
currentPointList = null;
}
}
else {
// the segment was entirely outside the aabb, end the line if there was one.
currentPointList = null;
}
}
return truncatedPointLists;
}
/**
* Crops a drawing so it is kept entirely within the given margin.
*/
export function cropToMargins(pointLists, paperSize, marginMm) {
const pageAabb = [{ x: 0, y: 0 }, paperSize.size];
const margin = { x: marginMm, y: marginMm };
const insetAabb = [vadd(pageAabb[0], margin), vsub(pageAabb[1], margin)];
const truncatedPointLists = [];
for (const pointList of pointLists) {
for (const croppedLine of cropLineToAabb(pointList, insetAabb)) {
truncatedPointLists.push(croppedLine);
}
}
return truncatedPointLists;
}
export function dedupPoints(points, epsilon) {
if (epsilon === 0) {
return points;
}
const dedupedPoints = [points[0]];
const epsilon2 = epsilon * epsilon;
for (const p of points.slice(1)) {
if (vlen2(vsub(p, dedupedPoints[dedupedPoints.length - 1])) > epsilon2) {
dedupedPoints.push(p);
}
}
return dedupedPoints;
}
//# sourceMappingURL=util.js.map