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 { newPoint } from './point.js';
/**
* Represents an axis-aligned 2D bounding box
* Used for spatial queries and geometric containment tests
* @class
*/
export class Bounds {
/**
* Create new bounding box initialized to inverted extremes
* (min values at max, max values at min) ready for point addition
*/
constructor() {
this.minx = 10e7;
this.miny = 10e7;
@ -11,6 +20,14 @@ export class Bounds {
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) {
this.minx = minx;
this.miny = miny;
@ -19,6 +36,10 @@ export class Bounds {
return this;
}
/**
* Create deep copy of bounds
* @returns {Bounds} New bounds with same values
*/
clone() {
let b = new Bounds();
b.minx = this.minx;
@ -29,6 +50,12 @@ export class Bounds {
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) {
if (!margin) margin = config.precision_offset;
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) {
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) {
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) {
this.minx = Math.min(this.minx, p.x);
@ -70,21 +104,42 @@ export class Bounds {
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) {
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) {
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) {
return x >= this.minx - offset && x <= this.maxx + offset && y >= this.miny - offset && y <= this.maxy + offset;
}
/**
* @param {Bounds} parent
* @returns {boolean} true if fully inside parent bounds
* Check if this bounds is 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) {
return (
@ -96,9 +151,11 @@ export class Bounds {
}
/**
* @param {Bounds} b
* @param {number} precision
* @returns {boolean}
* Check if bounds overlap with another bounds
* Uses center distance comparison for efficient overlap detection
* @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) {
return (
@ -107,27 +164,52 @@ export class Bounds {
);
}
/**
* Calculate width of bounds
* @returns {number} Width (maxx - minx)
*/
width() {
return this.maxx - this.minx;
}
/**
* Calculate height of bounds
* @returns {number} Height (maxy - miny)
*/
height() {
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) {
return newPoint(this.centerx(), this.centery(), z);
}
/**
* Get X coordinate of center
* @returns {number} Center X coordinate
*/
centerx() {
return this.minx + this.width() / 2;
}
/**
* Get Y coordinate of center
* @returns {number} Center Y coordinate
*/
centery() {
return this.miny + this.height() / 2;
}
}
/**
* Create new Bounds instance
* @returns {Bounds} New bounds initialized to inverted extremes
*/
export function newBounds() {
return new Bounds();
}

View file

@ -4,10 +4,14 @@ import { base } from './base.js';
import { newPoint } from './point.js';
/**
* emit each element in an array based on
* the next closest endpoint. arrays contain
* elements with { first, last } points and
* may be open polys, unlike poly2polyEmit
* Emit each element in array based on next closest endpoint
* Uses greedy nearest-neighbor algorithm to minimize travel distance
* Arrays contain elements with { first, last } points and may be open paths
* 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) {
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
* point can be anywhere in the adjacent polygon. should be re-written
* to be more like outputOrderClosest() and have the option to account for
* depth in determining distance
* Emit polygons in optimal order based on nearest point
* Like tip2tipEmit but accepts polygons where next closest point can be anywhere
* Handles both open and closed polygons, can reverse open paths for optimal direction
* 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 = {}) {
let marker = opt.mark || 'delete';
@ -96,6 +110,13 @@ export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
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) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
@ -106,6 +127,15 @@ export function calc_normal(p1, p2) {
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) {
let dx, dy;
if (start) {
@ -118,6 +148,18 @@ export function end_vertex(n1, n2, off, start) {
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) {
let dx, dy, io, vl, q, r;
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 };
}
/**
* 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) {
let p = rec.vp.clone();
p.x += rec.dx;
@ -144,6 +192,12 @@ export function v2pl(rec) {
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) {
let p = rec.vp.clone();
p.x -= rec.dx;
@ -152,6 +206,22 @@ export function v2pr(rec) {
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) {
const absoff = Math.abs(offset);
// 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 };
}
/**
* 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) {
const { faces, normals, left, right, open } = path;
const out = [];
@ -420,8 +501,17 @@ export function pathTo3D(path, height, z) {
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) {
closed = closed !== undefined ? closed : true;
@ -516,13 +606,17 @@ export function shapeToPath(shape, points, closed) {
}
/**
* Generate a list of points approximating a circular arc.
* @param {Point} start - the starting point of the arc.
* @param {Point} end - the ending point of the arc (not included).
* @param {number} [arcdivs = 24] - the number of lines to use to represent PI radians
* @param {number} opts.radius - the radius of the arc. If undefined, will use the start and end points to infer the radius.
* @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise. generating the points.
* @return {Array<Point>} an array of points representing the arc.
* Generate array of points approximating circular arc from G-code arc commands
* Interpolates arc into line segments for rendering and toolpath generation
* Handles full circles (when start equals end) and Z-axis interpolation
* @param {Point} start - Arc starting point
* @param {Point} end - Arc ending point (not included in output)
* @param {number} [arcdivs=24] - Number of line segments per PI radians
* @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) {
let { clockwise, center, radius } = opts;
@ -597,7 +691,18 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
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 {
/**
* 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) {
this.size = size;
this.factor = Math.min(factor || 1.2, 1.1);
@ -605,6 +710,11 @@ export class FloatPacker {
this.pos = 0;
}
/**
* Push multiple float values onto array
* Automatically expands array if needed
* @param {...number} values - Float values to append
*/
push() {
const array = this.array;
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() {
if (this.pos / this.size >= 0.9) {
return this.array.subarray(0, this.pos);