jsdoc geo bounds and paths

This commit is contained in:
Stewart Allen 2025-12-16 13:17:21 -05:00
commit 9f8ee3993e
2 changed files with 222 additions and 25 deletions

View file

@ -3,7 +3,16 @@
import { config, util } from './base.js'; import { config, util } from './base.js';
import { newPoint } from './point.js'; import { newPoint } from './point.js';
/**
* Represents an axis-aligned 2D bounding box
* Used for spatial queries and geometric containment tests
* @class
*/
export class Bounds { export class Bounds {
/**
* Create new bounding box initialized to inverted extremes
* (min values at max, max values at min) ready for point addition
*/
constructor() { constructor() {
this.minx = 10e7; this.minx = 10e7;
this.miny = 10e7; this.miny = 10e7;
@ -11,6 +20,14 @@ export class Bounds {
this.maxy = -10e7; this.maxy = -10e7;
} }
/**
* Set bounds to specific values
* @param {number} minx - Minimum X coordinate
* @param {number} maxx - Maximum X coordinate
* @param {number} miny - Minimum Y coordinate
* @param {number} maxy - Maximum Y coordinate
* @returns {Bounds} This bounds (for chaining)
*/
set(minx, maxx, miny, maxy) { set(minx, maxx, miny, maxy) {
this.minx = minx; this.minx = minx;
this.miny = miny; this.miny = miny;
@ -19,6 +36,10 @@ export class Bounds {
return this; return this;
} }
/**
* Create deep copy of bounds
* @returns {Bounds} New bounds with same values
*/
clone() { clone() {
let b = new Bounds(); let b = new Bounds();
b.minx = this.minx; b.minx = this.minx;
@ -29,6 +50,12 @@ export class Bounds {
return b; return b;
} }
/**
* Check if bounds are equal within margin of error
* @param {Bounds} bounds - Bounds to compare
* @param {number} [margin] - Tolerance for comparison (defaults to config.precision_offset)
* @returns {boolean} True if all bounds values are within margin
*/
equals(bounds, margin) { equals(bounds, margin) {
if (!margin) margin = config.precision_offset; if (!margin) margin = config.precision_offset;
return util.isCloseTo(this.minx, bounds.minx, margin) && return util.isCloseTo(this.minx, bounds.minx, margin) &&
@ -38,7 +65,10 @@ export class Bounds {
} }
/** /**
* @returns {Number} absolute delta in x,y coordinate space * Calculate total absolute difference in bounds coordinates
* Sum of absolute differences for all four bounds values
* @param {Bounds} bounds - Bounds to compare
* @returns {number} Absolute delta in x,y coordinate space
*/ */
delta(bounds) { delta(bounds) {
return 0 + return 0 +
@ -49,7 +79,9 @@ export class Bounds {
} }
/** /**
* @param {Bounds} b * Expand bounds to include another bounds (mutates in place)
* @param {Bounds} b - Bounds to merge
* @returns {Bounds} This bounds (for chaining)
*/ */
merge(b) { merge(b) {
this.minx = Math.min(this.minx, b.minx); this.minx = Math.min(this.minx, b.minx);
@ -60,7 +92,9 @@ export class Bounds {
} }
/** /**
* @param {Point} p * Expand bounds to include point (mutates in place)
* @param {Point} p - Point to include
* @returns {Bounds} This bounds (for chaining)
*/ */
update(p) { update(p) {
this.minx = Math.min(this.minx, p.x); this.minx = Math.min(this.minx, p.x);
@ -70,21 +104,42 @@ export class Bounds {
return this; return this;
} }
/**
* Check if this bounds fully contains another bounds
* @param {Bounds} bounds - Bounds to test
* @returns {boolean} True if bounds is fully inside this
*/
contains(bounds) { contains(bounds) {
return bounds.isNested(this); return bounds.isNested(this);
} }
/**
* Check if point with coordinates x,y is inside bounds
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @returns {boolean} True if point is inside bounds (inclusive)
*/
containsXY(x, y) { containsXY(x, y) {
return x >= this.minx && x <= this.maxx && y >= this.miny && y <= this.maxy; return x >= this.minx && x <= this.maxx && y >= this.miny && y <= this.maxy;
} }
/**
* Check if point is inside bounds expanded by offset
* Used for proximity tests with margin
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {number} offset - Expansion offset (positive = expand, negative = shrink)
* @returns {boolean} True if point is inside expanded bounds
*/
containsOffsetXY(x, y, offset) { containsOffsetXY(x, y, offset) {
return x >= this.minx - offset && x <= this.maxx + offset && y >= this.miny - offset && y <= this.maxy + offset; return x >= this.minx - offset && x <= this.maxx + offset && y >= this.miny - offset && y <= this.maxy + offset;
} }
/** /**
* @param {Bounds} parent * Check if this bounds is fully inside parent bounds
* @returns {boolean} true if fully inside parent bounds * @param {Bounds} parent - Parent bounds to test against
* @param {number} [precision=config.precision_bounds] - Tolerance for edge comparison
* @returns {boolean} True if fully inside parent bounds within precision
*/ */
isNested(parent, precision = config.precision_bounds) { isNested(parent, precision = config.precision_bounds) {
return ( return (
@ -96,9 +151,11 @@ export class Bounds {
} }
/** /**
* @param {Bounds} b * Check if bounds overlap with another bounds
* @param {number} precision * Uses center distance comparison for efficient overlap detection
* @returns {boolean} * @param {Bounds} b - Bounds to test
* @param {number} [precision=config.precision_bounds] - Tolerance for overlap test
* @returns {boolean} True if bounds overlap
*/ */
overlaps(b, precision = config.precision_bounds) { overlaps(b, precision = config.precision_bounds) {
return ( return (
@ -107,27 +164,52 @@ export class Bounds {
); );
} }
/**
* Calculate width of bounds
* @returns {number} Width (maxx - minx)
*/
width() { width() {
return this.maxx - this.minx; return this.maxx - this.minx;
} }
/**
* Calculate height of bounds
* @returns {number} Height (maxy - miny)
*/
height() { height() {
return this.maxy - this.miny; return this.maxy - this.miny;
} }
/**
* Get center point of bounds
* @param {number} [z=0] - Z coordinate for center point
* @returns {Point} Center point
*/
center(z = 0) { center(z = 0) {
return newPoint(this.centerx(), this.centery(), z); return newPoint(this.centerx(), this.centery(), z);
} }
/**
* Get X coordinate of center
* @returns {number} Center X coordinate
*/
centerx() { centerx() {
return this.minx + this.width() / 2; return this.minx + this.width() / 2;
} }
/**
* Get Y coordinate of center
* @returns {number} Center Y coordinate
*/
centery() { centery() {
return this.miny + this.height() / 2; return this.miny + this.height() / 2;
} }
} }
/**
* Create new Bounds instance
* @returns {Bounds} New bounds initialized to inverted extremes
*/
export function newBounds() { export function newBounds() {
return new Bounds(); return new Bounds();
} }

View file

@ -4,10 +4,14 @@ import { base } from './base.js';
import { newPoint } from './point.js'; import { newPoint } from './point.js';
/** /**
* emit each element in an array based on * Emit each element in array based on next closest endpoint
* the next closest endpoint. arrays contain * Uses greedy nearest-neighbor algorithm to minimize travel distance
* elements with { first, last } points and * Arrays contain elements with { first, last } points and may be open paths
* may be open polys, unlike poly2polyEmit * Elements are marked with delete flag to avoid reprocessing
* @param {Array} array - Array of elements with { first, last } Point properties
* @param {Point} startPoint - Starting point for path ordering
* @param {Function} emitter - Callback function(element, firstPoint, count)
* @returns {Point} Final endpoint after processing all elements
*/ */
export function tip2tipEmit(array, startPoint, emitter) { export function tip2tipEmit(array, startPoint, emitter) {
let mindist, dist, found, count = 0; let mindist, dist, found, count = 0;
@ -39,10 +43,20 @@ export function tip2tipEmit(array, startPoint, emitter) {
} }
/** /**
* like tip2tipEmit but accepts an array of polygons and the next closest * Emit polygons in optimal order based on nearest point
* point can be anywhere in the adjacent polygon. should be re-written * Like tip2tipEmit but accepts polygons where next closest point can be anywhere
* to be more like outputOrderClosest() and have the option to account for * Handles both open and closed polygons, can reverse open paths for optimal direction
* depth in determining distance * Optionally uses 3D distance weighted by polygon area for depth-aware ordering
* @param {Polygon[]} array - Array of polygons to process
* @param {Point} startPoint - Starting point for path ordering
* @param {Function} emitter - Callback function(polygon, startIndex, count, prevPoint) => Point
* @param {Object} [opt={}] - Options object
* @param {string} [opt.mark='delete'] - Property name to mark processed polygons
* @param {boolean} [opt.swapdir=true] - Allow reversing open paths for better direction
* @param {boolean} [opt.weight=false] - Use 3D distance weighted by area squared
* @param {boolean} [opt.term=false] - Terminate after finding first match
* @param {boolean} [opt.perm=false] - Keep markers permanent (don't undo)
* @returns {Point} Final endpoint after processing
*/ */
export function poly2polyEmit(array, startPoint, emitter, opt = {}) { export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
let marker = opt.mark || 'delete'; let marker = opt.mark || 'delete';
@ -96,6 +110,13 @@ export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
return startPoint; return startPoint;
} }
/**
* Calculate normalized perpendicular (normal) vector for line segment
* Returns unit vector perpendicular to p1->p2 (rotated 90 degrees CCW)
* @param {Point} p1 - Segment start point
* @param {Point} p2 - Segment end point
* @returns {Object} Normal data: { dx, dy, p1, p2, len }
*/
export function calc_normal(p1, p2) { export function calc_normal(p1, p2) {
let dx = p2.x - p1.x; let dx = p2.x - p1.x;
let dy = p2.y - p1.y; let dy = p2.y - p1.y;
@ -106,6 +127,15 @@ export function calc_normal(p1, p2) {
return ({ dx: dy, dy: -dx, p1, p2, len }); return ({ dx: dy, dy: -dx, p1, p2, len });
} }
/**
* Calculate vertex offset for path endpoints (start or end)
* Used for open paths where there's no adjacent segment on one side
* @param {Object} n1 - Previous segment normal
* @param {Object} n2 - Next segment normal
* @param {number} off - Offset distance
* @param {boolean} start - True if start vertex, false if end vertex
* @returns {Object} Vertex data: { dx, dy, vp }
*/
export function end_vertex(n1, n2, off, start) { export function end_vertex(n1, n2, off, start) {
let dx, dy; let dx, dy;
if (start) { if (start) {
@ -118,6 +148,18 @@ export function end_vertex(n1, n2, off, start) {
return { dx, dy, vp: n1.p2 }; return { dx, dy, vp: n1.p2 };
} }
/**
* Calculate vertex offset at junction of two line segments
* Computes miter join offset based on angle between segments
* Returns offset direction, turn direction (io), and vertex length (vl)
* @param {Object} n1 - Previous segment normal with dx, dy
* @param {Object} n2 - Next segment normal with dx, dy
* @param {number} off - Offset distance
* @param {Point} [vp] - Vertex point (defaults to n1.p2)
* @returns {Object} Vertex data: { dx, dy, vp, io, vl }
* - io: cross product (positive = right turn, negative = left turn)
* - vl: vertex length for detecting acute angles
*/
export function calc_vertex(n1, n2, off, vp) { export function calc_vertex(n1, n2, off, vp) {
let dx, dy, io, vl, q, r; let dx, dy, io, vl, q, r;
r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy); r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy);
@ -136,6 +178,12 @@ export function calc_vertex(n1, n2, off, vp) {
return { dx, dy, vp: vp || n1.p2, io, vl }; return { dx, dy, vp: vp || n1.p2, io, vl };
} }
/**
* Convert vertex record to point on left side of path
* Adds offset to original vertex position
* @param {Object} rec - Vertex record with { dx, dy, vp }
* @returns {Point} Point offset to left
*/
export function v2pl(rec) { export function v2pl(rec) {
let p = rec.vp.clone(); let p = rec.vp.clone();
p.x += rec.dx; p.x += rec.dx;
@ -144,6 +192,12 @@ export function v2pl(rec) {
return p; return p;
} }
/**
* Convert vertex record to point on right side of path
* Subtracts offset from original vertex position
* @param {Object} rec - Vertex record with { dx, dy, vp }
* @returns {Point} Point offset to right
*/
export function v2pr(rec) { export function v2pr(rec) {
let p = rec.vp.clone(); let p = rec.vp.clone();
p.x -= rec.dx; p.x -= rec.dx;
@ -152,6 +206,22 @@ export function v2pr(rec) {
return p; return p;
} }
/**
* Convert point array to offset path with left/right sides and triangle faces
* Generates parallel paths offset from center line with proper miter joints
* Handles both open and closed paths, with acute angle splitting
* Used for extruding 2D paths into 3D geometry
* @param {Point[]} points - Array of points defining centerline path
* @param {number} offset - Offset distance (positive/negative for direction)
* @param {boolean} open - True for open path, false for closed loop
* @param {number} [miter=1.5] - Miter limit ratio for splitting acute angles
* @returns {Object} Path data: { left, right, faces, normals, open }
* - left: Array of points on left side
* - right: Array of points on right side
* - faces: Array of points forming triangle mesh
* - normals: Array of normal vectors for each face vertex
* - open: Whether path is open or closed
*/
export function pointsToPath(points, offset, open, miter = 1.5) { export function pointsToPath(points, offset, open, miter = 1.5) {
const absoff = Math.abs(offset); const absoff = Math.abs(offset);
// calculate segment normals which are used to calculate vertex normals // calculate segment normals which are used to calculate vertex normals
@ -324,6 +394,17 @@ export function pointsToPath(points, offset, open, miter = 1.5) {
return { left, right, faces, normals, open }; return { left, right, faces, normals, open };
} }
/**
* Extrude 2D path to 3D geometry with specified height
* Generates vertex positions and normals for top, bottom, and side faces
* Used for creating 3D representations of 2D toolpaths
* @param {Object} path - Path object from pointsToPath with { faces, normals, left, right, open }
* @param {number} height - Half-height of extrusion (total height = 2 * height)
* @param {number} [z] - Optional Z coordinate for path (sets base Z level)
* @returns {Object} 3D geometry: { faces, normals }
* - faces: Float32Array of vertex positions (x,y,z triplets)
* - normals: Float32Array of normal vectors (x,y,z triplets)
*/
export function pathTo3D(path, height, z) { export function pathTo3D(path, height, z) {
const { faces, normals, left, right, open } = path; const { faces, normals, left, right, open } = path;
const out = []; const out = [];
@ -420,8 +501,17 @@ export function pathTo3D(path, height, z) {
return { faces: out, normals: nrm }; return { faces: out, normals: nrm };
} }
// produces indexed geometry which isn't ideal for rendering because /**
// the default threejs generated vertex normals aren't accurate * Extrude Three.js shape along point path to create tube geometry
* Produces indexed geometry (not ideal for rendering - default vertex normals aren't accurate)
* Shape profile is swept along path with rotation/translation at each point
* @param {THREE.Shape} shape - Three.js shape to extrude (profile cross-section)
* @param {Point[]} points - Array of points defining path centerline
* @param {boolean} [closed=true] - Whether path is closed loop
* @returns {Object} Indexed geometry: { index, faces }
* - index: Array of triangle indices
* - faces: Float32Array of vertex positions
*/
export function shapeToPath(shape, points, closed) { export function shapeToPath(shape, points, closed) {
closed = closed !== undefined ? closed : true; closed = closed !== undefined ? closed : true;
@ -516,13 +606,17 @@ export function shapeToPath(shape, points, closed) {
} }
/** /**
* Generate a list of points approximating a circular arc. * Generate array of points approximating circular arc from G-code arc commands
* @param {Point} start - the starting point of the arc. * Interpolates arc into line segments for rendering and toolpath generation
* @param {Point} end - the ending point of the arc (not included). * Handles full circles (when start equals end) and Z-axis interpolation
* @param {number} [arcdivs = 24] - the number of lines to use to represent PI radians * @param {Point} start - Arc starting point
* @param {number} opts.radius - the radius of the arc. If undefined, will use the start and end points to infer the radius. * @param {Point} end - Arc ending point (not included in output)
* @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise. generating the points. * @param {number} [arcdivs=24] - Number of line segments per PI radians
* @return {Array<Point>} an array of points representing the arc. * @param {Object} opts - Arc parameters
* @param {Point} opts.center - Arc center point with .r radius property
* @param {number} opts.radius - Arc radius (alternative to center)
* @param {boolean} opts.clockwise - True for clockwise (CW/G2), false for CCW (G3)
* @returns {Point[]} Array of interpolated points along arc (excluding end point)
*/ */
export function arcToPath(start, end, arcdivs = 24, opts) { export function arcToPath(start, end, arcdivs = 24, opts) {
let { clockwise, center, radius } = opts; let { clockwise, center, radius } = opts;
@ -597,7 +691,18 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
return arr return arr
} }
/**
* Dynamically growing Float32Array with efficient memory management
* Used for accumulating vertex/normal data when final size is unknown
* Automatically expands when capacity reached, optimizes final output
* @class
*/
export class FloatPacker { export class FloatPacker {
/**
* Create new float packer with initial size
* @param {number} size - Initial array size
* @param {number} [factor=1.2] - Growth factor when expanding (clamped to max 1.2)
*/
constructor(size, factor) { constructor(size, factor) {
this.size = size; this.size = size;
this.factor = Math.min(factor || 1.2, 1.1); this.factor = Math.min(factor || 1.2, 1.1);
@ -605,6 +710,11 @@ export class FloatPacker {
this.pos = 0; this.pos = 0;
} }
/**
* Push multiple float values onto array
* Automatically expands array if needed
* @param {...number} values - Float values to append
*/
push() { push() {
const array = this.array; const array = this.array;
const size = this.size; const size = this.size;
@ -621,6 +731,11 @@ export class FloatPacker {
} }
} }
/**
* Get final array trimmed to actual size
* Uses subarray (view) if >90% full, otherwise copies to save memory
* @returns {Float32Array} Array containing only pushed values
*/
finalize() { finalize() {
if (this.pos / this.size >= 0.9) { if (this.pos / this.size >= 0.9) {
return this.array.subarray(0, this.pos); return this.array.subarray(0, this.pos);