add jsdoc for geo point/line/slope

This commit is contained in:
Stewart Allen 2025-12-16 07:21:47 -05:00
commit d1e3b11ec3
4 changed files with 482 additions and 51 deletions

View file

@ -167,7 +167,7 @@ function inCloseRange(val, min, max, precisiom) {
* @returns {number}
*/
function sqr(v) {
return v * v
return v * v;
}
/**

View file

@ -1,6 +1,16 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* Represents a line segment defined by two points
* @class
*/
export class Line {
/**
* Create a line segment between two points
* @param {Point} p1 - First endpoint
* @param {Point} p2 - Second endpoint
* @param {string} [key] - Optional unique identifier (auto-generated from point keys if not provided)
*/
constructor(p1, p2, key) {
if (!key) key = [p1.key, p2.key].join(';');
this.p1 = p1;
@ -8,21 +18,35 @@ export class Line {
this.key = key;
}
/**
* Calculate length of line segment in 2D
* @returns {number} Euclidean distance between endpoints
*/
length() {
return Math.sqrt(this.length2());
}
/**
* @returns {number} square of length
* Calculate squared length of line segment
* Faster than length() - use when only comparing lengths
* @returns {number} Squared distance between endpoints
*/
length2() {
return this.p1.distToSq2D(this.p2);
}
/**
* Calculate slope of line segment
* @returns {Slope} Slope object representing direction from p1 to p2
*/
slope() {
return base.newSlope(this.p1.slopeTo(this.p2));
}
/**
* Reverse direction of line by swapping endpoints (mutates in place)
* @returns {Line} This line (for chaining)
*/
reverse() {
let t = this.p1;
this.p1 = this.p2;
@ -30,10 +54,20 @@ export class Line {
return this;
}
/**
* Calculate midpoint of line segment
* @returns {Point} Point at center of line
*/
midpoint() {
return this.p1.midPointTo(this.p2);
}
/**
* Check if this line is collinear with another line
* Uses cross product to test if slopes are parallel within tolerance
* @param {Line} line - Line to test against
* @returns {boolean} True if lines are collinear (parallel with same or opposite direction)
*/
isCollinear(line) {
let p1 = this.p1,
p2 = this.p2,
@ -47,10 +81,25 @@ export class Line {
}
}
/**
* Create a new Line instance
* @param {Point} p1 - First endpoint
* @param {Point} p2 - Second endpoint
* @param {string} [key] - Optional unique identifier
* @returns {Line} New line segment
*/
export function newLine(p1, p2, key) {
return new Line(p1, p2, key);
}
/**
* Create a line with endpoints ordered by their keys (lexicographic)
* Ensures consistent line representation regardless of point order
* @param {Point} p1 - First point
* @param {Point} p2 - Second point
* @param {string} [key] - Optional unique identifier
* @returns {Line} New line with points ordered by key
*/
export function newOrderedLine(p1, p2, key) {
return p1.key < p2.key ? newLine(p1,p2,key) : newLine(p2,p1,key);
}

View file

@ -6,7 +6,19 @@ import { newSlope } from './slope.js';
const { Vector3 } = THREE;
/**
* Represents a 3D point with x, y, z coordinates
* Also supports optional 'a' property for rotary axis operations
* @class
*/
class Point {
/**
* Create a new point
* @param {number} [x=0] - X coordinate
* @param {number} [y=0] - Y coordinate
* @param {number} [z=0] - Z coordinate
* @param {string} [key] - Optional cached key for comparison
*/
constructor(x = 0, y = 0, z = 0, key) {
this.x = x;
this.y = y;
@ -16,6 +28,11 @@ class Point {
}
}
/**
* Get unique string key for this point based on coordinates
* Cached for performance in point comparison and hashing
* @returns {string} Unique identifier string
*/
get key() {
if (this._key) {
return this._key;
@ -27,6 +44,10 @@ class Point {
].join('');
}
/**
* Convert point to Clipper library format (scaled integers)
* @returns {{X: number, Y: number}} Clipper point object
*/
toClipper() {
return {
X: (this.x * config.clipper) | 0,
@ -34,14 +55,29 @@ class Point {
};
}
/**
* Convert point to array format
* @returns {number[]} [x, y, z] array
*/
toArray() {
return [ this.x, this.y, this.z ];
}
/**
* Convert to Three.js Vector3 object
* @returns {THREE.Vector3} Three.js vector
*/
toVector3() {
return new Vector3(this.x, this.y, this.z);
}
/**
* Set all coordinates and invalidate cached key
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {number} z - Z coordinate
* @returns {Point} This point (for chaining)
*/
set(x, y, z) {
this.x = x;
this.y = y;
@ -50,26 +86,50 @@ class Point {
return this;
}
/**
* Set X coordinate
* @param {number} x - X coordinate
* @returns {Point} This point (for chaining)
*/
setX(x) {
this.x = x;
return this;
}
/**
* Set Y coordinate
* @param {number} y - Y coordinate
* @returns {Point} This point (for chaining)
*/
setY(y) {
this.y = y;
return this;
}
/**
* Set Z coordinate
* @param {number} z - Z coordinate
* @returns {Point} This point (for chaining)
*/
setZ(z) {
this.z = z;
return this;
}
/**
* Set A (rotary axis) coordinate for 4-axis machining
* @param {number} a - A axis angle in degrees
* @returns {Point} This point (for chaining)
*/
setA(a) {
this.a = a;
return this;
}
/**
* Swap X and Z coordinates in place
* @returns {Point} This point (for chaining)
*/
swapXZ() {
let p = this,
t = p.x;
@ -78,6 +138,10 @@ class Point {
return this;
}
/**
* Swap Y and Z coordinates in place
* @returns {Point} This point (for chaining)
*/
swapYZ() {
let p = this,
t = p.y;
@ -86,6 +150,13 @@ class Point {
return this;
}
/**
* Scale coordinates by specified factors
* @param {number} [x=1] - X scale factor
* @param {number} [y=1] - Y scale factor
* @param {number} [z=1] - Z scale factor
* @returns {Point} This point (for chaining)
*/
scale(x = 1, y = 1, z = 1) {
this.x *= x;
this.y *= y;
@ -93,6 +164,11 @@ class Point {
return this;
}
/**
* Create new point with rounded coordinates
* @param {number} [precision] - Number of decimal places
* @returns {Point} New rounded point
*/
round(precision) {
return newPoint(
this.x.round(precision),
@ -100,20 +176,38 @@ class Point {
this.z.round(precision));
}
/**
* Associate a facet (triangle) with this point for mesh operations
* @param {Object} facet - Triangle/facet object
* @returns {Point} This point (for chaining)
*/
addFacet(facet) {
if (!this.group) this.group = [];
this.group.push(facet);
return this;
}
/**
* Invalidate cached key, forcing recalculation on next access
* Call after modifying x, y, or z coordinates
*/
rekey() {
this._key = undefined;
}
/**
* Get string representation of point (same as key)
* @returns {string} String representation
*/
toString() {
return this.key;
}
/**
* Create deep copy of this point
* @param {string[]} [keys] - Optional property names to copy to cloned point
* @returns {Point} New point with same coordinates
*/
clone(keys) {
let p = newPoint(this.x, this.y, this.z, this._key);
if (this.a !== undefined) {
@ -127,20 +221,41 @@ class Point {
return p;
}
// annotate instance with other field data (see clone())
/**
* Annotate instance with additional field data (see clone())
* @param {Object} [obj={}] - Object with properties to add to this point
* @returns {Point} This point (for chaining)
*/
annotate(obj = {}) {
Object.assign(this, obj);
return this;
}
/**
* Create slope object from this point to another
* @param {Point} p - Target point
* @returns {Slope} Slope object representing direction
*/
slopeTo(p) {
return newSlope(this, p);
}
/**
* Create line segment from this point to another
* @param {Point} p - Endpoint
* @param {string} [k] - Optional key for line
* @returns {Line} Line segment
*/
lineTo(p, k) {
return newLine(this, p, k);
}
/**
* Check if point is near another within specified distance
* @param {Point} p - Point to compare
* @param {number} dist - Distance threshold
* @returns {boolean} True if within distance in both x and y
*/
isNear(p, dist) {
return util.isCloseTo(this.x, p.x, dist) && util.isCloseTo(this.y, p.y, dist);
}
@ -157,6 +272,13 @@ class Point {
return Math.sqrt(this.distToLineSq(p1, p2));
}
/**
* Calculate 3D distance from this point to infinite line defined by two points
* Uses cross product method for true 3D distance
* @param {Point} lp1 - First point defining the line
* @param {Point} lp2 - Second point defining the line
* @returns {number} Perpendicular distance to line
*/
distToLine3D(lp1, lp2) {
// Convert points to vectors
const p0 = [this.x, this.y, this.z];
@ -189,19 +311,25 @@ class Point {
}
/**
* used exclusively in new fill code. output does not agree with
* old distToLine, but is the only method that seems to work for
* fill. using new distToLine as a global replacement breaks support
* offset clipping. both need to be investigated and a single line
* normal distance needs to be formulated to replace both functions.
* Alternative distance-to-line calculation used exclusively in new fill code
* NOTE: Output does not agree with distToLine(). This is the only method that works
* for fill, but using it as global replacement breaks support offset clipping.
* Both need investigation to formulate a single unified line normal distance function.
* @param {Point} p1 - First point of line segment
* @param {Point} p2 - Second point of line segment
* @returns {number} Distance to line
*/
distToLineNew(p1, p2) {
return p2l(this, p1, p2);
}
/**
* return square of distance to line connecting points p1, p2
* distance is calculated on the perpendicular (normal) to line
* Return square of distance to line segment connecting points p1, p2
* Distance calculated on perpendicular (normal) to line
* Handles endpoints: if perpendicular falls outside segment, returns distance to nearest endpoint
* @param {Point} p1 - First point of line segment
* @param {Point} p2 - Second point of line segment
* @returns {number} Squared distance to line segment
*/
distToLineSq(p1, p2) {
let p = this,
@ -215,6 +343,14 @@ class Point {
return util.distSqv2(p.x, p.y, p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y));
}
/**
* Check if point is within squared distance to line segment with optimized early exit
* Uses bounding box check for fast rejection before calculating actual distance
* @param {Point} p1 - First point of line segment
* @param {Point} p2 - Second point of line segment
* @param {number} dist2 - Squared distance threshold
* @returns {boolean} True if within squared distance
*/
withinDist2(p1, p2, dist2) {
let ll2 = p1.distToSq2D(p2),
dp1 = this.distToSq2D(p1),
@ -235,10 +371,20 @@ class Point {
return this.distToLineSq(p1, p2) < dist2;
}
/**
* Calculate midpoint between this point and another in 2D (preserves this.z)
* @param {Point} p2 - Target point
* @returns {Point} New midpoint with averaged x,y and this.z
*/
midPointTo(p2) {
return newPoint((this.x + p2.x) / 2, (this.y + p2.y) / 2, this.z);
}
/**
* Calculate midpoint between this point and another in 3D
* @param {Point} p2 - Target point
* @returns {Point} New midpoint with averaged x,y,z
*/
midPointTo3D(p2) {
return newPoint(
(this.x + p2.x) / 2,
@ -248,7 +394,10 @@ class Point {
}
/**
* non-scale corrected version of follow()
* Project point along slope by multiplier (non-scale corrected version of follow())
* @param {Slope} slope - Direction slope
* @param {number} mult - Distance multiplier
* @returns {Point} New projected point
*/
projectOnSlope(slope, mult) {
return newPoint(
@ -257,13 +406,22 @@ class Point {
this.z);
}
/**
* Follow direction toward target point by scaled distance
* @param {Point} point - Target point
* @param {number} mult - Distance multiplier
* @returns {Point} New point along line to target
*/
followTo(point, mult) {
return this.follow(this.slopeTo(point), mult);
}
/**
* return a point along the line this from point to p2 offset
* by a distance. positive distances are closer to this point.
* Return point along line from this to p2, offset backward by distance
* Positive distances move closer to this point (back toward source)
* @param {Point} p2 - Target point
* @param {number} dist - Offset distance (positive = toward this point)
* @returns {Point} Offset point
*/
offsetPointFrom(p2, dist) {
let p1 = this,
@ -276,8 +434,11 @@ class Point {
}
/**
* return a point along the line this from point to p2 offset
* by a distance. positive distances are farther from this point.
* Return point along line from this to p2, offset forward by distance
* Positive distances move farther from this point (toward p2 and beyond)
* @param {Point} p2 - Target point
* @param {number} dist - Offset distance (positive = away from this point)
* @returns {Point} Offset point
*/
offsetPointTo(p2, dist) {
let p1 = this,
@ -293,6 +454,14 @@ class Point {
return newPoint(p1.x + ox, p1.y + oy, p2.z, key.NONE);
}
/**
* Create offset line segment perpendicular to line from this to p2
* Returns line shifted by offset distance perpendicular to original direction
* Stores original points in np1.op and np2.op properties
* @param {Point} p2 - Line endpoint
* @param {number} offset - Perpendicular offset distance
* @returns {Line} Offset line segment
*/
offsetLineTo(p2, offset) {
let p1 = this,
dx = p2.x - p1.x,
@ -307,13 +476,22 @@ class Point {
return newLine(np1, np2, key.NONE);
}
/**
* Create new point offset by specified amounts in each dimension
* @param {number} x - X offset
* @param {number} y - Y offset
* @param {number} z - Z offset
* @returns {Point} New offset point
*/
offset(x, y, z) {
return newPoint(this.x + x, this.y + y, this.z + z);
}
/**
* checks if a point is inside of a polygon
* does not check children/holes
* Check if point is inside polygon using ray casting algorithm
* Does not check children/holes - use isInPolygon() for full polygon test
* @param {Polygon} poly - Polygon to test
* @returns {boolean} True if inside polygon boundary
*/
inPolygon(poly) {
if (!poly.bounds.containsXY(this.x, this.y)) return false;
@ -335,8 +513,10 @@ class Point {
}
/**
* returns true if the point is inside of a polygon but
* not inside any of it's children
* Check if point is inside polygon but not inside any of its holes
* Handles arrays of polygons and considers points near edges as inside
* @param {Polygon|Polygon[]} poly - Polygon(s) to test
* @returns {boolean} True if inside outer polygon but not in holes
*/
isInPolygon(poly) {
let point = this,
@ -358,8 +538,10 @@ class Point {
}
/**
* returns true if the point is inside of a polygon but
* not inside any of it's children
* Stricter version of isInPolygon - does not consider near-edge points as inside
* Point must be truly inside outer polygon and not in any holes
* @param {Polygon|Polygon[]} poly - Polygon(s) to test
* @returns {boolean} True if strictly inside polygon
*/
isInPolygonOnly(poly) {
let point = this,
@ -383,8 +565,11 @@ class Point {
}
/**
* checks if point is near polygon edge. distance is squared.
* @param {boolean} [inner] process inner polygons
* Check if point is near any edge of polygon within squared distance threshold
* @param {Polygon} poly - Polygon to test
* @param {number} dist2 - Squared distance threshold
* @param {boolean} [inner] - If true, also process inner polygons (holes)
* @returns {boolean} True if within distance of any edge
*/
nearPolygon(poly, dist2, inner) {
// throw new Error("nearPolygon");
@ -402,15 +587,23 @@ class Point {
}
/**
* returns true if point will not be trimmed later
* Check if point will not be trimmed in later offset operations
* Used in offset path generation to filter points
* @param {Polygon} poly - Polygon boundary
* @param {number} offset - Offset distance (positive=outward, negative=inward)
* @param {number} mindist2 - Minimum squared distance from edge
* @returns {boolean} True if point is safe from trimming
*/
insideOffset(poly, offset, mindist2) {
return this.inPolygon(poly) === (offset > 0) && !this.nearPolygon(poly, mindist2);
}
/**
* returns a new point following given slope for given distance
* same as projectOnSlope() but scaled
* Create new point following given slope for given distance (scale-corrected)
* Same as projectOnSlope() but normalizes slope to unit vector first
* @param {Slope} slope - Direction slope
* @param {number} distance - Distance to travel
* @returns {Point} New point along slope
*/
follow(slope, distance) {
let ls = distance / Math.sqrt(slope.dx * slope.dx + slope.dy * slope.dy);
@ -418,8 +611,11 @@ class Point {
}
/**
* for point, return intersecting point on z to next point if points
* are on either size of z
* Calculate intersection point on plane at height z between this point and p
* Used for slicing operations - finds where line crosses Z plane
* @param {Point} p - Second point
* @param {number} z - Z height of intersection plane
* @returns {Point} Intersection point at height z
*/
intersectZ(p, z) {
let dx = p.x - this.x,
@ -429,44 +625,60 @@ class Point {
return newPoint(this.x + dx * pct, this.y + dy * pct, this.z + dz * pct);
}
/**
* Test exact equality in 2D (x and y coordinates only)
* @param {Point} p - Point to compare
* @returns {boolean} True if x and y are exactly equal
*/
isEqual2D(p) {
return this === p || (this.x === p.x && this.y === p.y);
}
/**
* returns true if points are close enough to be considered equivalent
* Check if points are close enough to be considered equivalent in 2D
* Uses precision_merge_sq threshold for tolerance
* @param {Point} p - Point to compare
* @returns {boolean} True if within merge threshold
*/
isMergable2D(p) {
return this.isEqual2D(p) || (this.distToSq2D(p) < config.precision_merge_sq);
}
/**
* compares 3D point
* Test exact equality in 3D (x, y, and z coordinates)
* @param {Point} p - Point to compare
* @returns {boolean} True if all coordinates are exactly equal
*/
isEqual(p) {
return this === p || (this.x === p.x && this.y === p.y && this.z === p.z);
}
/**
* returns true if points are close enough to be considered equivalent
* Check if points are close enough to be considered equivalent in 3D
* Uses precision_merge_sq threshold for tolerance
* @param {Point} p - Point to compare
* @returns {boolean} True if within merge threshold
*/
isMergable3D(p) {
return this.isEqual(p) || (this.distToSq3D(p) < config.precision_merge_sq);
}
/**
* return true if point is inside 2D square size dist*2 around p
* Check if point is inside 2D bounding box centered on p
* @param {Point} p - Center point
* @param {number} dist - Half-width of box (distance from center to edge)
* @returns {boolean} True if inside box
*/
isInBox(p, dist) {
return Math.abs(this.x - p.x) < dist && Math.abs(this.y - p.y) < dist;
}
/**
* return min distance from point to a polygon
* stops searching if any point is closer than threshold
*
* @param {Polygon} poly
* @param {number} [threshold] stop looking if under threshold
* Calculate minimum distance from point to any segment of polygon
* Early exit optimization: stops searching if distance falls below threshold
* @param {Polygon} poly - Polygon with segments to measure
* @param {number} [threshold] - Stop searching if distance drops below this value
* @returns {number} Minimum distance to any polygon segment
*/
distToPolySegments(poly, threshold) {
let point = this,
@ -481,8 +693,11 @@ class Point {
}
/**
* @param {Polygon} poly
* @param {number} [threshold] stop looking if under threshold
* Calculate minimum distance from point to any vertex of polygon
* Early exit optimization: stops searching if distance falls below threshold
* @param {Polygon} poly - Polygon with vertices to measure
* @param {number} [threshold] - Stop searching if distance drops below this value
* @returns {number} Minimum distance to any polygon vertex
*/
distToPolyPoints(poly, threshold) {
let point = this,
@ -495,7 +710,11 @@ class Point {
}
/**
* @returns {Point} nearest point (less than max) from array to this point
* Find nearest point from array within maximum distance
* Skips this point and deleted points (p.del flag)
* @param {Point[]} points - Array of candidate points
* @param {number} max - Maximum squared distance to consider
* @returns {Point|null} Nearest point within max distance, or null if none found
*/
nearestTo(points, max) {
if (!max) throw "missing max";
@ -515,8 +734,10 @@ class Point {
}
/**
* @param {Point[]} points
* @return {number} average square dist to cloud of points
* Calculate average squared distance to cloud of points
* Excludes this point from calculation
* @param {Point[]} points - Point cloud
* @returns {number} Average squared distance
*/
averageDistTo(points) {
let sum = 0.0,
@ -532,7 +753,9 @@ class Point {
}
/**
* dist to point in 2D
* Calculate Euclidean distance to point in 2D
* @param {Point} p - Target point
* @returns {number} Distance
*/
distTo2D(p) {
let dx = this.x - p.x,
@ -541,7 +764,10 @@ class Point {
}
/**
* square of distance in 2D
* Calculate squared distance to point in 2D
* Faster than distTo2D() - use when only comparing distances
* @param {Point} p - Target point
* @returns {number} Squared distance
*/
distToSq2D(p) {
let dx = this.x - p.x,
@ -549,6 +775,11 @@ class Point {
return dx * dx + dy * dy;
}
/**
* Calculate Euclidean distance to point in 3D
* @param {Point} p - Target point
* @returns {number} Distance
*/
distTo3D(p) {
let dx = this.x - p.x,
dy = this.y - p.y,
@ -557,7 +788,10 @@ class Point {
}
/**
* square of distance in 3D
* Calculate squared distance to point in 3D
* Faster than distTo3D() - use when only comparing distances
* @param {Point} p - Target point
* @returns {number} Squared distance
*/
distToSq3D(p) {
let dx = this.x - p.x,
@ -567,7 +801,11 @@ class Point {
}
/**
* returns true if point is inside triangle described by three points
* Check if point is inside triangle using barycentric coordinate test
* @param {Point} a - Triangle vertex A
* @param {Point} b - Triangle vertex B
* @param {Point} c - Triangle vertex C
* @returns {boolean} True if inside triangle
*/
inTriangle(a, b, c) {
let as_x = this.x - a.x,
@ -579,22 +817,39 @@ class Point {
}
/**
* returns true if point is on a line described by two points.
* test sum of distances p1->this + this->p2 ~= p1->p2 whens
* slopes from p1->this same as this->p2
* Check if point lies on line segment within precision threshold
* Uses perpendicular distance test with precision_point_on_line tolerance
* @param {Point} p1 - Line start point
* @param {Point} p2 - Line end point
* @returns {boolean} True if on line within tolerance
*/
onLine(p1, p2) {
return this.distToLine(p1, p2) < config.precision_point_on_line;
}
/**
* Create new point by adding delta vector
* @param {Point} delta - Vector to add
* @returns {Point} New point (this + delta)
*/
add(delta) {
return newPoint(this.x + delta.x, this.y + delta.y, this.z + delta.z);
}
/**
* Create new point by subtracting delta vector
* @param {Point} delta - Vector to subtract
* @returns {Point} New point (this - delta)
*/
sub(delta) {
return newPoint(this.x - delta.x, this.y - delta.y, this.z - delta.z);
}
/**
* Move this point by delta vector (mutates in place)
* @param {Point} delta - Vector to add
* @returns {Point} This point (for chaining)
*/
move(delta) {
this.x += delta.x;
this.y += delta.y;
@ -602,7 +857,10 @@ class Point {
return this;
}
// radians rotatition in XY around origin
/**
* Rotate point in XY plane around origin (mutates in place)
* @param {number} angle - Rotation angle in radians
*/
rotate(angle) {
const { x, y } = this;
this.x = x * Math.cos(angle) - y * Math.sin(angle);
@ -610,14 +868,34 @@ class Point {
}
}
/**
* Calculate dot product of two 2D vectors
* @private
* @param {Object} u - First vector with x,y properties
* @param {Object} v - Second vector with x,y properties
* @returns {number} Dot product u·v
*/
function dot(u, v) {
return u.x * v.x + u.y * v.y;
}
/**
* Calculate magnitude (length) of 2D vector
* @private
* @param {Object} v - Vector with x,y properties
* @returns {number} Vector magnitude
*/
function norm(v) {
return Math.sqrt(dot(v, v));
}
/**
* Calculate Euclidean distance between two 2D vectors
* @private
* @param {Object} u - First vector with x,y properties
* @param {Object} v - Second vector with x,y properties
* @returns {number} Distance
*/
function d(u, v) {
return norm({
x: u.x - v.x,
@ -625,6 +903,15 @@ function d(u, v) {
});
}
/**
* Calculate distance from point to line (used by distToLineNew)
* Projects point onto infinite line and returns distance to projection
* @private
* @param {Point} p - Point to measure from
* @param {Point} l1 - First point defining line
* @param {Point} l2 - Second point defining line
* @returns {number} Perpendicular distance from point to line
*/
function p2l(p, l1, l2) {
let v = {
x: l2.x - l1.x,
@ -648,10 +935,24 @@ function p2l(p, l1, l2) {
return d(p, pb);
}
/**
* Create a new Point instance
* @param {number} [x=0] - X coordinate
* @param {number} [y=0] - Y coordinate
* @param {number} [z=0] - Z coordinate
* @param {string} [key] - Optional cached key for comparison
* @returns {Point} New point instance
*/
function newPoint(x, y, z, key) {
return new Point(x, y, z, key);
}
/**
* Convert Clipper library point to Point instance
* @param {Object} cp - Clipper point with X,Y integer properties (scaled by config.clipper)
* @param {number} z - Z coordinate for resulting point
* @returns {Point} New point with coordinates scaled back from Clipper format
*/
function pointFromClipper(cp, z) {
return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z);
}

View file

@ -8,21 +8,48 @@ const ABS = Math.abs,
DEG2RAD = Math.PI / 180,
RAD2DEG = 180 / Math.PI;
/**
* Represents a 2D directional slope (vector direction)
* Stores both dx/dy components and computed angle in degrees
* @class
*/
export class Slope {
/**
* Create slope from two points or explicit dx/dy values
* @param {Point} [p1] - First point (if creating from points)
* @param {Point} [p2] - Second point (if creating from points)
* @param {number} [dx] - X component if not using points
* @param {number} [dy] - Y component if not using points
*/
constructor(p1, p2, dx, dy) {
this.dx = p1 && p2 ? p2.x - p1.x : dx;
this.dy = p1 && p2 ? p2.y - p1.y : dy;
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
}
/**
* Get string representation of slope
* @returns {string} "dx,dy,angle" format
*/
toString() {
return [this.dx, this.dy, this.angle].join(',');
}
/**
* Create deep copy of slope
* @returns {Slope} New slope with same dx, dy
*/
clone() {
return new Slope(null, null, this.dx, this.dy);
}
/**
* Check if slopes are equivalent within precision tolerance
* Special handling for near-vertical and near-horizontal slopes
* Uses adaptive precision based on slope magnitude
* @param {Slope} s - Slope to compare
* @returns {boolean} True if slopes are effectively the same
*/
isSame(s) {
// if very close to vertical or horizontal, they're the same
if (ABS(this.dx) <= config.precision_merge && ABS(s.dx) <= config.precision_merge) return true;
@ -32,6 +59,11 @@ export class Slope {
return angleWithinDelta(this.angle, s.angle, prec || config.precision_slope);
}
/**
* Rotate slope 90 degrees counter-clockwise to get perpendicular (mutates in place)
* Converts slope to its normal (perpendicular) direction
* @returns {Slope} This slope (for chaining)
*/
normal() {
let t = this.dx;
this.dx = -this.dy;
@ -40,6 +72,11 @@ export class Slope {
return this;
}
/**
* Reverse slope direction by 180 degrees (mutates in place)
* Negates both dx and dy components
* @returns {Slope} This slope (for chaining)
*/
invert() {
this.dx = -this.dx;
this.dy = -this.dy;
@ -47,6 +84,11 @@ export class Slope {
return this;
}
/**
* Normalize slope so largest component is 1 (mutates in place)
* Maintains direction while scaling to unit-like form
* @returns {Slope} This slope (for chaining)
*/
toUnit() {
let max = Math.max(ABS(this.dx), ABS(this.dy));
this.dx = this.dx / max;
@ -54,12 +96,24 @@ export class Slope {
return this;
}
/**
* Scale slope by factor (mutates in place)
* @param {number} f - Scale factor
* @returns {Slope} This slope (for chaining)
*/
factor(f) {
this.dx *= f;
this.dy *= f;
return this;
}
/**
* Calculate angular difference to another slope
* Result normalized to [-180, 180] range
* @param {Slope} s2 - Target slope
* @param {boolean} [sign] - If true, return signed difference; if false, return absolute difference
* @returns {number} Angle difference in degrees
*/
angleDiff(s2,sign) {
const n1 = this.angle;
const n2 = s2.angle;
@ -71,20 +125,47 @@ export class Slope {
}
/**
* returns true if the difference between a & b is less than v
* Check if difference between two values is less than threshold
* @private
* @param {number} a - First value
* @param {number} b - Second value
* @param {number} v - Threshold
* @returns {boolean} True if |a-b| < v
*/
function minDeltaABS(a,b,v) {
return ABS(a-b) < v;
}
/**
* Check if two angles are within delta degrees of each other
* Handles wraparound at 0/360 degree boundary
* @private
* @param {number} a1 - First angle in degrees
* @param {number} a2 - Second angle in degrees
* @param {number} delta - Maximum difference in degrees
* @returns {boolean} True if angles are within delta
*/
function angleWithinDelta(a1, a2, delta) {
return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta);
}
/**
* Create new Slope from two points or explicit dx/dy values
* @param {Point} [p1] - First point (if creating from points)
* @param {Point} [p2] - Second point (if creating from points)
* @param {number} [dx] - X component if not using points
* @param {number} [dy] - Y component if not using points
* @returns {Slope} New slope instance
*/
export function newSlope(p1, p2, dx, dy) {
return new Slope(p1, p2, dx, dy);
}
/**
* Create slope from angle in degrees
* @param {number} angle - Angle in degrees (0 = East, 90 = North, etc.)
* @returns {Slope} New slope with direction from angle
*/
export function newSlopeFromAngle(angle) {
return newSlope(0,0,
Math.cos(angle * DEG2RAD),