UNPKG

@bitbybit-dev/base

Version:

Bit By Bit Developers Base CAD Library to Program Geometry

200 lines (199 loc) 8.35 kB
export class GeometryHelper { constructor() { /** * Calculates the nesting depth of an array recursively. * Example: [1,2,3] → 1, [[1,2],[3,4]] → 2, [[[1]]] → 3 */ this.getArrayDepth = (value) => { return Array.isArray(value) ? 1 + Math.max(...value.map(this.getArrayDepth)) : 0; }; } /** * Applies one or more 4×4 transformation matrices to a list of points sequentially. * Each transformation is applied in order (composition of transformations). * Example: points=[[0,0,0], [1,0,0]] with translation [5,0,0] → [[5,0,0], [6,0,0]] */ transformControlPoints(transformation, transformedControlPoints) { const transformationArrays = this.getFlatTransformations(transformation); transformationArrays.forEach(transform => { transformedControlPoints = this.transformPointsByMatrixArray(transformedControlPoints, transform); }); return transformedControlPoints; } /** * Flattens nested transformation arrays into a single-level array of transformation matrices. * Handles both 2D arrays (single transform list) and 3D arrays (nested transform lists). * Example: [[[matrix1, matrix2]], [[matrix3]]] → [matrix1, matrix2, matrix3] */ getFlatTransformations(transformation) { let transformationArrays = []; if (this.getArrayDepth(transformation) === 3) { transformation.forEach(transform => { transformationArrays.push(...transform); }); } else { transformationArrays = transformation; } return transformationArrays; } /** * Applies a single 4×4 transformation matrix (as flat 16-element array) to multiple points. * Example: points=[[0,0,0], [1,0,0]] with translation matrix → transformed points */ transformPointsByMatrixArray(points, transform) { return this.transformPointsCoordinates(points, transform); } /** * Transforms multiple points using a transformation matrix (maps each point through the matrix). * Example: points=[[1,0,0], [0,1,0]] with 90° rotation → [[0,1,0], [-1,0,0]] */ transformPointsCoordinates(points, transform) { const transformedPoints = []; for (const pt of points) { const transformedVector = this.transformCoordinates(pt[0], pt[1], pt[2], transform); transformedPoints.push(transformedVector); } return transformedPoints; } /** * Removes all duplicate vectors from a list (works with arbitrary-length numeric vectors). * Compares vectors using tolerance for floating-point equality. * Example: [[1,2], [3,4], [1,2], [5,6]] with tolerance=1e-7 → [[1,2], [3,4], [5,6]] */ removeAllDuplicateVectors(vectors, tolerance = 1e-7) { const cleanVectors = []; vectors.forEach(vector => { // when there are no vectors in cleanVectors array that match the current vector, push it in. if (!cleanVectors.some(s => this.vectorsTheSame(vector, s, tolerance))) { cleanVectors.push(vector); } }); return cleanVectors; } /** * Removes consecutive duplicate vectors from a list (keeps only first occurrence in each sequence). * Optionally checks and removes duplicate if first and last vectors match. * Example: [[1,2], [1,2], [3,4], [3,4], [5,6]] → [[1,2], [3,4], [5,6]] */ removeConsecutiveVectorDuplicates(vectors, checkFirstAndLast = true, tolerance = 1e-7) { const vectorsRemaining = []; if (vectors.length > 1) { for (let i = 1; i < vectors.length; i++) { const currentVector = vectors[i]; const previousVector = vectors[i - 1]; if (!this.vectorsTheSame(currentVector, previousVector, tolerance)) { vectorsRemaining.push(previousVector); } if (i === vectors.length - 1) { vectorsRemaining.push(currentVector); } } if (checkFirstAndLast) { const firstVector = vectorsRemaining[0]; const lastVector = vectorsRemaining[vectorsRemaining.length - 1]; if (this.vectorsTheSame(firstVector, lastVector, tolerance)) { vectorsRemaining.pop(); } } } else if (vectors.length === 1) { vectorsRemaining.push(...vectors); } return vectorsRemaining; } /** * Compares two vectors for approximate equality using tolerance (element-wise comparison). * Returns false if vectors have different lengths. * Example: [1.0000001, 2.0], [1.0, 2.0] with tolerance=1e-6 → true */ vectorsTheSame(vec1, vec2, tolerance) { let result = false; if (vec1.length !== vec2.length) { return result; } else { result = true; for (let i = 0; i < vec1.length; i++) { if (!this.approxEq(vec1[i], vec2[i], tolerance)) { result = false; break; } } } return result; } /** * Checks if two numbers are approximately equal within a tolerance. * Example: 1.0000001, 1.0 with tolerance=1e-6 → true, 1.001, 1.0 with tolerance=1e-6 → false */ approxEq(num1, num2, tolerance) { const res = Math.abs(num1 - num2) < tolerance; return res; } /** * Removes consecutive duplicate points from a list (specialized for 3D/2D points). * Optionally checks and removes duplicate if first and last points match (for closed loops). * Example: [[0,0,0], [0,0,0], [1,0,0], [1,0,0]] → [[0,0,0], [1,0,0]] */ removeConsecutivePointDuplicates(points, checkFirstAndLast = true, tolerance = 1e-7) { const pointsRemaining = []; if (points.length > 1) { for (let i = 1; i < points.length; i++) { const currentPoint = points[i]; const previousPoint = points[i - 1]; if (!this.arePointsTheSame(currentPoint, previousPoint, tolerance)) { pointsRemaining.push(previousPoint); } if (i === points.length - 1) { pointsRemaining.push(currentPoint); } } if (checkFirstAndLast) { const firstPoint = pointsRemaining[0]; const lastPoint = pointsRemaining[pointsRemaining.length - 1]; if (this.arePointsTheSame(firstPoint, lastPoint, tolerance)) { pointsRemaining.pop(); } } } else if (points.length === 1) { pointsRemaining.push(...points); } return pointsRemaining; } /** * Checks if two points are approximately equal using tolerance (supports 2D and 3D points). * Example: [1.0000001, 2.0, 3.0], [1.0, 2.0, 3.0] with tolerance=1e-6 → true */ arePointsTheSame(pointA, pointB, tolerance) { let result = false; if (pointA.length === 2 && pointB.length === 2) { if (this.approxEq(pointA[0], pointB[0], tolerance) && this.approxEq(pointA[1], pointB[1], tolerance)) { result = true; } } else if (pointA.length === 3 && pointB.length === 3) { if (this.approxEq(pointA[0], pointB[0], tolerance) && this.approxEq(pointA[1], pointB[1], tolerance) && this.approxEq(pointA[2], pointB[2], tolerance)) { result = true; } } return result; } transformCoordinates(x, y, z, transformation) { const m = transformation; const rx = x * m[0] + y * m[4] + z * m[8] + m[12]; const ry = x * m[1] + y * m[5] + z * m[9] + m[13]; const rz = x * m[2] + y * m[6] + z * m[10] + m[14]; const rw = 1 / (x * m[3] + y * m[7] + z * m[11] + m[15]); const newx = rx * rw; const newy = ry * rw; const newz = rz * rw; return [newx, newy, newz]; } }