UNPKG

@chrisheninger/saxi

Version:

Drive the AxiDraw pen plotter

176 lines 6.31 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const vec_1 = require("./vec"); /** Format a smallish duration in 2h30m15s form */ 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(""); } exports.formatDuration = formatDuration; /** 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; } } } return [{ x: minX, y: minY }, { x: maxX, y: maxY }]; } /** * 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 = vec_1.vadd(targetMin, vec_1.vmul(vec_1.vsub(targetMax, targetMin), 0.5)); const offset = vec_1.vsub(targetCenter, vec_1.vmul(vec_1.vsub(max, min), scale * 0.5)); return pointLists.map((pl) => pl.map((p) => vec_1.vadd(vec_1.vmul(vec_1.vsub(p, min), scale), offset))); } /** Scale a drawing to fill a piece of paper, with the given size and margins. */ function scaleToPaper(pointLists, paperSize, marginMm) { return scaleToFit(pointLists, { x: marginMm, y: marginMm }, vec_1.vsub(paperSize.size, { x: marginMm, y: marginMm })); } exports.scaleToPaper = scaleToPaper; /** * 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 = vec_1.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]; var u1 = -Infinity; var 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 vec_1.vadd(a, vec_1.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. */ function cropToMargins(pointLists, paperSize, marginMm) { const pageAabb = [{ x: 0, y: 0 }, paperSize.size]; const margin = { x: marginMm, y: marginMm }; const insetAabb = [vec_1.vadd(pageAabb[0], margin), vec_1.vsub(pageAabb[1], margin)]; const truncatedPointLists = []; for (const pointList of pointLists) { for (const croppedLine of cropLineToAabb(pointList, insetAabb)) { truncatedPointLists.push(croppedLine); } } return truncatedPointLists; } exports.cropToMargins = cropToMargins; 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 (vec_1.vlen2(vec_1.vsub(p, dedupedPoints[dedupedPoints.length - 1])) > epsilon2) { dedupedPoints.push(p); } } return dedupedPoints; } exports.dedupPoints = dedupPoints; //# sourceMappingURL=util.js.map