UNPKG

@bitbybit-dev/base

Version:

Bit By Bit Developers Base CAD Library to Program Geometry

1,000 lines (999 loc) 42.6 kB
/** * Contains various methods for points. Point in bitbybit is simply an array containing 3 numbers for [x, y, z]. * Because of this form Point can be interchanged with Vector, which also is an array in [x, y, z] form. * When creating 2D points, z coordinate is simply set to 0 - [x, y, 0]. */ export class Point { constructor(geometryHelper, transforms, vector, lists) { this.geometryHelper = geometryHelper; this.transforms = transforms; this.vector = vector; this.lists = lists; } /** * Applies transformation matrix to a single point (rotates, scales, or translates). * Example: point=[0,0,0] with translation [5,5,0] → [5,5,0] * @param inputs Contains a point and the transformations to apply * @returns Transformed point * @group transforms * @shortname transform point * @drawable true */ transformPoint(inputs) { const transformation = inputs.transformation; let transformedControlPoints = [inputs.point]; transformedControlPoints = this.geometryHelper.transformControlPoints(transformation, transformedControlPoints); return transformedControlPoints[0]; } /** * Applies same transformation matrix to multiple points (batch transform). * Example: 5 points with rotation 90° → all 5 points rotated together * @param inputs Contains points and the transformations to apply * @returns Transformed points * @group transforms * @shortname transform points * @drawable true */ transformPoints(inputs) { return this.geometryHelper.transformControlPoints(inputs.transformation, inputs.points); } /** * Applies different transformation matrices to corresponding points (one transform per point). * Arrays must have equal length. * Example: 3 points with 3 different translations → each point moved independently * @param inputs Contains points and the transformations to apply * @returns Transformed points * @group transforms * @shortname transforms for points * @drawable true */ transformsForPoints(inputs) { if (inputs.points.length !== inputs.transformation.length) { throw new Error("You must provide equal nr of points and transformations"); } return inputs.points.map((pt, index) => { return this.geometryHelper.transformControlPoints(inputs.transformation[index], [pt])[0]; }); } /** * Moves multiple points by a translation vector (same offset for all points). * Example: points=[[0,0,0], [1,0,0]], translation=[5,5,0] → [[5,5,0], [6,5,0]] * @param inputs Contains points and the translation vector * @returns Translated points * @group transforms * @shortname translate points * @drawable true */ translatePoints(inputs) { const translationTransform = this.transforms.translationXYZ({ translation: inputs.translation }); return this.geometryHelper.transformControlPoints(translationTransform, inputs.points); } /** * Moves multiple points by corresponding translation vectors (one vector per point). * Arrays must have equal length. * Example: 3 points with 3 different vectors → each point moved by its corresponding vector * @param inputs Contains points and the translation vector * @returns Translated points * @group transforms * @shortname translate points with vectors * @drawable true */ translatePointsWithVectors(inputs) { if (inputs.points.length !== inputs.translations.length) { throw new Error("You must provide equal nr of points and translations"); } const translationTransforms = this.transforms.translationsXYZ({ translations: inputs.translations }); return inputs.points.map((pt, index) => { return this.geometryHelper.transformControlPoints(translationTransforms[index], [pt])[0]; }); } /** * Moves multiple points by separate X, Y, Z values (convenience method for translation). * Example: points=[[0,0,0]], x=10, y=5, z=0 → [[10,5,0]] * @param inputs Contains points and the translation in x y and z * @returns Translated points * @group transforms * @shortname translate xyz points * @drawable true */ translateXYZPoints(inputs) { const translationTransform = this.transforms.translationXYZ({ translation: [inputs.x, inputs.y, inputs.z] }); return this.geometryHelper.transformControlPoints(translationTransform, inputs.points); } /** * Scales multiple points around a center point with different factors per axis. * Example: points=[[10,0,0]], center=[5,0,0], scaleXyz=[2,1,1] → [[15,0,0]] (doubles X distance from center) * @param inputs Contains points, center point and scale factors * @returns Scaled points * @group transforms * @shortname scale points on center * @drawable true */ scalePointsCenterXYZ(inputs) { const scaleTransforms = this.transforms.scaleCenterXYZ({ center: inputs.center, scaleXyz: inputs.scaleXyz }); return this.geometryHelper.transformControlPoints(scaleTransforms, inputs.points); } /** * Stretches multiple points along a direction from a center point (directional scaling). * Example: points=[[10,0,0]], center=[0,0,0], direction=[1,0,0], scale=2 → [[20,0,0]] * @param inputs Contains points, center point, direction and scale factor * @returns Stretched points * @group transforms * @shortname stretch points dir from center * @drawable true */ stretchPointsDirFromCenter(inputs) { const stretchTransforms = this.transforms.stretchDirFromCenter({ center: inputs.center, scale: inputs.scale, direction: inputs.direction }); return this.geometryHelper.transformControlPoints(stretchTransforms, inputs.points); } /** * Rotates multiple points around a center point along a custom axis. * Example: points=[[10,0,0]], center=[0,0,0], axis=[0,1,0], angle=90° → [[0,0,-10]] * @param inputs Contains points, axis, center point and angle of rotation * @returns Rotated points * @group transforms * @shortname rotate points center axis * @drawable true */ rotatePointsCenterAxis(inputs) { const rotationTransforms = this.transforms.rotationCenterAxis({ center: inputs.center, axis: inputs.axis, angle: inputs.angle }); return this.geometryHelper.transformControlPoints(rotationTransforms, inputs.points); } /** * Calculates axis-aligned bounding box containing all points (min, max, center, width, height, length). * Example: points=[[0,0,0], [10,5,3]] → {min:[0,0,0], max:[10,5,3], center:[5,2.5,1.5], width:10, height:5, length:3} * @param inputs Points * @returns Bounding box of points * @group extract * @shortname bounding box pts * @drawable true */ boundingBoxOfPoints(inputs) { const xVals = []; const yVals = []; const zVals = []; inputs.points.forEach(pt => { xVals.push(pt[0]); yVals.push(pt[1]); zVals.push(pt[2]); }); const min = [Math.min(...xVals), Math.min(...yVals), Math.min(...zVals)]; const max = [Math.max(...xVals), Math.max(...yVals), Math.max(...zVals)]; const center = [ (min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2, ]; const width = max[0] - min[0]; const height = max[1] - min[1]; const length = max[2] - min[2]; return { min, max, center, width, height, length, }; } /** * Calculates distance to the nearest point in a collection. * Example: point=[0,0,0], points=[[5,0,0], [10,0,0], [3,0,0]] → 3 (distance to [3,0,0]) * @param inputs Point from which to measure and points to measure the distance against * @returns Distance to closest point * @group extract * @shortname distance to closest pt * @drawable false */ closestPointFromPointsDistance(inputs) { return this.closestPointFromPointData(inputs).distance; } /** * Finds array index of the nearest point in a collection (1-based index, not 0-based). * Example: point=[0,0,0], points=[[5,0,0], [10,0,0], [3,0,0]] → 3 (index of [3,0,0]) * @param inputs Point from which to find the index in a collection of points * @returns Closest point index * @group extract * @shortname index of closest pt * @drawable false */ closestPointFromPointsIndex(inputs) { return this.closestPointFromPointData(inputs).index; } /** * Finds the nearest point in a collection to a reference point. * Example: point=[0,0,0], points=[[5,0,0], [10,0,0], [3,0,0]] → [3,0,0] * @param inputs Point and points collection to find the closest point in * @returns Closest point * @group extract * @shortname closest pt * @drawable true */ closestPointFromPoints(inputs) { return this.closestPointFromPointData(inputs).point; } /** * Calculates Euclidean distance between two points. * Example: start=[0,0,0], end=[3,4,0] → 5 (using Pythagorean theorem: √(3²+4²)) * @param inputs Coordinates of start and end points * @returns Distance * @group measure * @shortname distance * @drawable false */ distance(inputs) { const x = inputs.endPoint[0] - inputs.startPoint[0]; const y = inputs.endPoint[1] - inputs.startPoint[1]; const z = inputs.endPoint[2] - inputs.startPoint[2]; return Math.sqrt(x * x + y * y + z * z); } /** * Calculates distances from a start point to multiple end points. * Example: start=[0,0,0], endPoints=[[3,0,0], [0,4,0], [5,0,0]] → [3, 4, 5] * @param inputs Coordinates of start and end points * @returns Distances * @group measure * @shortname distances to points * @drawable false */ distancesToPoints(inputs) { return inputs.endPoints.map(pt => { return this.distance({ startPoint: inputs.startPoint, endPoint: pt }); }); } /** * Duplicates a point N times (creates array with N copies of the same point). * Example: point=[5,5,0], amountOfPoints=3 → [[5,5,0], [5,5,0], [5,5,0]] * @param inputs The point to be multiplied and the amount of points to create * @returns Distance * @group transforms * @shortname multiply point * @drawable true */ multiplyPoint(inputs) { const points = []; for (let i = 0; i < inputs.amountOfPoints; i++) { points.push([inputs.point[0], inputs.point[1], inputs.point[2]]); } return points; } /** * Extracts X coordinate from a point. * Example: point=[5,10,3] → 5 * @param inputs The point * @returns X coordinate * @group get * @shortname x coord * @drawable false */ getX(inputs) { return inputs.point[0]; } /** * Extracts Y coordinate from a point. * Example: point=[5,10,3] → 10 * @param inputs The point * @returns Y coordinate * @group get * @shortname y coord * @drawable false */ getY(inputs) { return inputs.point[1]; } /** * Extracts Z coordinate from a point. * Example: point=[5,10,3] → 3 * @param inputs The point * @returns Z coordinate * @group get * @shortname z coord * @drawable false */ getZ(inputs) { return inputs.point[2]; } /** * Calculates centroid (average position) of multiple points. * Example: points=[[0,0,0], [10,0,0], [10,10,0]] → [6.67,3.33,0] * @param inputs The points * @returns point * @group extract * @shortname average point * @drawable true */ averagePoint(inputs) { const xVals = []; const yVals = []; const zVals = []; inputs.points.forEach(pt => { xVals.push(pt[0]); yVals.push(pt[1]); zVals.push(pt[2]); }); return [ xVals.reduce((p, c) => p + c, 0) / inputs.points.length, yVals.reduce((p, c) => p + c, 0) / inputs.points.length, zVals.reduce((p, c) => p + c, 0) / inputs.points.length, ]; } /** * Creates a 3D point from X, Y, Z coordinates. * Example: x=10, y=5, z=3 → [10,5,3] * @param inputs xyz information * @returns point 3d * @group create * @shortname point xyz * @drawable true */ pointXYZ(inputs) { return [inputs.x, inputs.y, inputs.z]; } /** * Creates a 2D point from X, Y coordinates. * Example: x=10, y=5 → [10,5] * @param inputs xy information * @returns point 3d * @group create * @shortname point xy * @drawable false */ pointXY(inputs) { return [inputs.x, inputs.y]; } /** * Creates logarithmic spiral points using golden angle or custom widening factor. * Generates natural spiral patterns common in nature (sunflower, nautilus shell). * Example: numberPoints=100, radius=10, phi=1.618 → 100 points forming outward spiral * @param inputs Spiral information * @returns Specified number of points in the array along the spiral * @group create * @shortname spiral * @drawable true */ spiral(inputs) { const phi = inputs.phi; const b = Math.log(phi) / (Math.PI / inputs.widening); const spiral = []; const step = inputs.radius / inputs.numberPoints; for (let i = 0; i < inputs.radius; i += step) { const th = Math.log(i / inputs.factor) / b; const x = i * Math.cos(th); const y = i * Math.sin(th); spiral.push([x ? x : 0, y ? y : 0, 0]); } return spiral; } /** * Creates hexagonal grid center points on XY plane (honeycomb pattern). * Grid size controlled by number of hexagons, not width/height. * Example: radiusHexagon=1, nrHexagonsX=3, nrHexagonsY=3 → 9 hex centers in grid pattern * @param inputs Information about hexagon and the grid * @returns Points in the array on the grid * @group create * @shortname hex grid * @drawable true */ hexGrid(inputs) { const xLength = Math.sqrt(Math.pow(inputs.radiusHexagon, 2) - Math.pow(inputs.radiusHexagon / 2, 2)); const points = []; for (let ix = 0; ix < inputs.nrHexagonsX; ix++) { const coordX = ix * xLength * 2; for (let iy = 0; iy < inputs.nrHexagonsY; iy++) { const coordY = (inputs.radiusHexagon + inputs.radiusHexagon / 2) * iy; const adjustX = coordX + (iy % 2 === 0 ? 0 : xLength); points.push([adjustX, coordY, 0]); } } if (inputs.orientOnCenter) { const compensateX = points[points.length - 1][0] / 2; const compensateY = points[points.length - 1][1] / 2; points.forEach((p, index) => { points[index] = [p[0] - compensateX, p[1] - compensateY, 0]; }); } if (inputs.pointsOnGround) { points.forEach((p, index) => { points[index] = [p[0], 0, p[1]]; }); } return points; } /** * Creates hexagonal grid scaled to fit within specified width/height bounds (auto-calculates hex size). * Returns center points and hex vertices. Supports pointy-top or flat-top orientation. * Example: width=10, height=10, nrHexagonsInHeight=3 → hex grid filling 10×10 area with 3 rows * @param inputs Information about the desired grid dimensions and hexagon counts. * @returns An object containing the array of center points and an array of hexagon vertex arrays. * @group create * @shortname scaled hex grid to fit * @drawable false */ hexGridScaledToFit(inputs) { var _a, _b, _c, _d; let width = inputs.width; let height = inputs.height; let nrHexagonsInHeight = inputs.nrHexagonsInHeight; let nrHexagonsInWidth = inputs.nrHexagonsInWidth; let extendTop = (_a = inputs.extendTop) !== null && _a !== void 0 ? _a : false; let extendBottom = (_b = inputs.extendBottom) !== null && _b !== void 0 ? _b : false; let extendLeft = (_c = inputs.extendLeft) !== null && _c !== void 0 ? _c : false; let extendRight = (_d = inputs.extendRight) !== null && _d !== void 0 ? _d : false; const { flatTop = false, centerGrid = false, pointsOnGround = false } = inputs; // we flip the width and height if the hexagons are flat-topped and will then rotate resuls afterwards as default // computes pointy-top hexagons if (flatTop) { const oldWidth = width; width = inputs.height; height = oldWidth; const oldNrHexagonsInWidth = nrHexagonsInWidth; nrHexagonsInWidth = nrHexagonsInHeight; nrHexagonsInHeight = oldNrHexagonsInWidth; const extendTopOld = extendTop; const extendBottomOld = extendBottom; const extendLeftOld = extendLeft; const extendRightOld = extendRight; extendTop = extendLeftOld; extendBottom = extendRightOld; extendLeft = extendBottomOld; extendRight = extendTopOld; } // --- Input Validation --- if (width <= 0 || height <= 0 || nrHexagonsInWidth < 1 || nrHexagonsInHeight < 1) { console.warn("Hex grid dimensions and counts must be positive."); return { centers: [], hexagons: [], shortestDistEdge: undefined, longestDistEdge: undefined, maxFilletRadius: undefined }; } // --- Generate Unscaled Regular Grid Centers (Radius = 1) --- // Use the *existing* hexGrid function, ensuring it doesn't center or project yet. const BASE_RADIUS = 1.0; const unscaledCenters = this.hexGrid({ radiusHexagon: BASE_RADIUS, nrHexagonsX: nrHexagonsInWidth, nrHexagonsY: nrHexagonsInHeight, orientOnCenter: false, pointsOnGround: false // Keep on XY plane for now }); if (unscaledCenters.length === 0) { return { centers: [], hexagons: [], shortestDistEdge: undefined, longestDistEdge: undefined, maxFilletRadius: undefined }; // Return empty if base grid failed } // --- Generate Unscaled Regular Hexagon Vertices (Radius = 1) --- const unscaledHexagons = unscaledCenters.map(center => this.getRegularHexagonVertices(center, BASE_RADIUS)); // --- Determine Dimensions of the Unscaled Grid Bounding Box --- let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (const hex of unscaledHexagons) { for (const vertex of hex) { if (vertex[0] < minX) minX = vertex[0]; if (vertex[0] > maxX) maxX = vertex[0]; if (vertex[1] < minY) minY = vertex[1]; if (vertex[1] > maxY) maxY = vertex[1]; } } const unscaledWidth = maxX - minX; const unscaledHeight = maxY - minY; // --- Step 4: Calculate Scaling Factors --- // Handle potential zero dimensions if only 1 hex (W/H would be based on hex size) const scaleX = (unscaledWidth > 1e-9) ? width / unscaledWidth : 1; const scaleY = (unscaledHeight > 1e-9) ? height / unscaledHeight : 1; // If unscaled W/H is 0 (e.g., 1x1 grid), scale=1 means the final hex will have // width/height derived from its regular R=1 shape, not fitting totalW/H. // This might need adjustment if a single hex *must* fill the total W/H. // For now, assume nrU/nrV > 1 or accept R=1 size for single hex. // --- Scale Centers and Vertices --- // Scale relative to the min corner of the unscaled grid (minX, minY) let scaledCenters = unscaledCenters.map(p => [ (p[0] - minX) * scaleX, (p[1] - minY) * scaleY, 0 // Keep Z=0 for now ]); let scaledHexagons = unscaledHexagons.map(hex => hex.map(v => [ (v[0] - minX) * scaleX, (v[1] - minY) * scaleY, 0 // Keep Z=0 for now ])); let shortestDistEdge = Infinity; let longestDistEdge = -Infinity; let maxFilletRadius = 0; // --- Calculate Shortes/Longest & Extensions --- if (scaledHexagons.length !== 0) { const firstHex = scaledHexagons[0]; maxFilletRadius = this.safestPointsMaxFilletHalfLine({ points: firstHex, checkLastWithFirst: true, tolerance: 1e-7 }); // Calculate the shortest and longest edge distances firstHex.forEach((pt, index) => { const nextPt = firstHex[(index + 1) % firstHex.length]; const dist = this.distance({ startPoint: pt, endPoint: nextPt }); if (dist < shortestDistEdge) { shortestDistEdge = dist; } if (dist > longestDistEdge) { longestDistEdge = dist; } }); if (extendTop || extendBottom || extendLeft || extendRight) { const pt1Pointy = firstHex[0]; const pt2Pointy = firstHex[1]; const cellHeight = pt1Pointy[1] - pt2Pointy[1]; const cellWidth = pt2Pointy[0] - pt1Pointy[0]; if (extendTop && !extendBottom) { const transform = { center: [0, 0, 0], direction: [0, 1, 0], scale: height / (height - cellHeight), }; scaledHexagons = scaledHexagons.map(hex => { transform.points = hex; return this.stretchPointsDirFromCenter(transform); }); transform.points = scaledCenters; scaledCenters = this.stretchPointsDirFromCenter(transform); } if (extendBottom && !extendTop) { const transform = { center: [0, height, 0], direction: [0, -1, 0], scale: height / (height - cellHeight), }; scaledHexagons = scaledHexagons.map(hex => { transform.points = hex; return this.stretchPointsDirFromCenter(transform); }); transform.points = scaledCenters; scaledCenters = this.stretchPointsDirFromCenter(transform); } if (extendTop && extendBottom) { const transform = { center: [0, height / 2, 0], direction: [0, 1, 0], scale: height / (height - cellHeight * 2), }; scaledHexagons = scaledHexagons.map(hex => { transform.points = hex; return this.stretchPointsDirFromCenter(transform); }); transform.points = scaledCenters; scaledCenters = this.stretchPointsDirFromCenter(transform); } if (extendLeft && !extendRight) { const transform = { center: [width, 0, 0], direction: [1, 0, 0], scale: width / (width - cellWidth), }; scaledHexagons = scaledHexagons.map(hex => { transform.points = hex; return this.stretchPointsDirFromCenter(transform); }); transform.points = scaledCenters; scaledCenters = this.stretchPointsDirFromCenter(transform); } if (extendRight && !extendLeft) { const transform = { center: [0, 0, 0], direction: [1, 0, 0], scale: width / (width - cellWidth), }; scaledHexagons = scaledHexagons.map(hex => { transform.points = hex; return this.stretchPointsDirFromCenter(transform); }); transform.points = scaledCenters; scaledCenters = this.stretchPointsDirFromCenter(transform); } if (extendLeft && extendRight) { const transform = { center: [width / 2, 0, 0], direction: [1, 0, 0], scale: width / (width - cellWidth * 2), }; scaledHexagons = scaledHexagons.map(hex => { transform.points = hex; return this.stretchPointsDirFromCenter(transform); }); transform.points = scaledCenters; scaledCenters = this.stretchPointsDirFromCenter(transform); } } } if (flatTop) { // width and height are swapped scaledCenters = this.rotatePointsCenterAxis({ points: scaledCenters, center: [width / 2, height / 2, 0], axis: [0, 0, 1], angle: 90 }); scaledHexagons = scaledHexagons.map(hex => { return this.rotatePointsCenterAxis({ points: hex, center: [width / 2, height / 2, 0], axis: [0, 0, 1], angle: 90 }); }); // translate to new center const vecTranslation = this.vector.sub({ first: [height / 2, width / 2, 0], second: [width / 2, height / 2, 0] }); scaledCenters = this.translatePoints({ points: scaledCenters, translation: vecTranslation }); scaledHexagons = scaledHexagons.map(hex => { return this.translatePoints({ points: hex, translation: vecTranslation }); }); } // --- Apply Optional Centering --- // Center the scaled grid (currently starting at [0,0]) around [0,0] if (centerGrid) { let shiftX = width / 2; let shiftY = height / 2; if (flatTop) { shiftX = height / 2; shiftY = width / 2; } for (let i = 0; i < scaledCenters.length; i++) { scaledCenters[i][0] -= shiftX; scaledCenters[i][1] -= shiftY; } for (let i = 0; i < scaledHexagons.length; i++) { for (let j = 0; j < scaledHexagons[i].length; j++) { scaledHexagons[i][j][0] -= shiftX; scaledHexagons[i][j][1] -= shiftY; } } } // --- Apply Optional Ground Projection --- if (pointsOnGround) { for (let i = 0; i < scaledCenters.length; i++) { scaledCenters[i] = [scaledCenters[i][0], 0, scaledCenters[i][1]]; } for (let i = 0; i < scaledHexagons.length; i++) { for (let j = 0; j < scaledHexagons[i].length; j++) { scaledHexagons[i][j] = [scaledHexagons[i][j][0], 0, scaledHexagons[i][j][1]]; } } } // We need to adjust orders to be column first and then row first if we choose flat top if (flatTop) { const grouped = this.lists.groupNth({ list: scaledHexagons.reverse(), nrElements: inputs.nrHexagonsInWidth, keepRemainder: true, }); const res = this.lists.flipLists({ list: grouped }); res.forEach(s => s.reverse()); scaledHexagons = res.flat(); const groupedCenters = this.lists.groupNth({ list: scaledCenters.reverse(), nrElements: inputs.nrHexagonsInWidth, keepRemainder: true, }); const resCenters = this.lists.flipLists({ list: groupedCenters }); resCenters.forEach(s => s.reverse()); scaledCenters = resCenters.flat(); } // --- Return Result --- return { centers: scaledCenters, hexagons: scaledHexagons, shortestDistEdge, longestDistEdge, maxFilletRadius }; } /** * Calculates the maximum possible fillet radius at a corner formed by two line segments * sharing an endpoint (C), such that the fillet arc is tangent to both segments * and lies entirely within them. * @param inputs three points and the tolerance * @returns the maximum fillet radius * @group fillet * @shortname max fillet radius * @drawable false */ maxFilletRadius(inputs) { const { start: p1, center: p2, end: c, tolerance = 1e-7 } = inputs; const v1 = this.vector.sub({ first: p1, second: c }); const v2 = this.vector.sub({ first: p2, second: c }); const len1 = this.vector.length({ vector: v1 }); const len2 = this.vector.length({ vector: v2 }); if (len1 < tolerance || len2 < tolerance) { return 0; } const normV1 = this.vector.normalized({ vector: v1 }); const normV2 = this.vector.normalized({ vector: v2 }); if (!normV1 || !normV2) { return 0; } // Calculate the cosine of the angle between the vectors // Clamp to [-1, 1] to avoid potential domain errors with acos due to floating point inaccuracies const cosAlpha = Math.max(-1.0, Math.min(1.0, this.vector.dot({ first: normV1, second: normV2 }))); // Check for collinearity // If vectors point in the same direction (angle ~ 0), no fillet if (cosAlpha > 1.0 - tolerance) { return 0; } // If vectors point in opposite directions (angle ~ 180 deg), no corner for a fillet if (cosAlpha < -1.0 + tolerance) { return 0; } // Calculate the angle alpha (0 < alpha < PI) const alpha = Math.acos(cosAlpha); // Calculate tan(alpha / 2) // alpha/2 is between 0 and PI/2, so tan is positive and non-zero const tanHalfAlpha = Math.tan(alpha / 2.0); // If tanHalfAlpha is extremely small (alpha near 0, shouldn't happen due to collinearity check), return 0 if (tanHalfAlpha < tolerance) { return 0; } // The distance 'd' from corner C to the tangent point must be less than or equal to the segment lengths. // d = r / tan(alpha/2) <= min(len1, len2) // r <= min(len1, len2) * tan(alpha/2) const maxRadius = Math.min(len1, len2) * tanHalfAlpha; return maxRadius; } /** * Calculates the maximum possible fillet radius at a corner C, such that the fillet arc * is tangent to both segments (P1-C, P2-C) and the tangent points lie within * the first half of each segment (measured from C). * @param inputs three points and the tolerance * @returns the maximum fillet radius * @group fillet * @shortname max fillet radius half line * @drawable false */ maxFilletRadiusHalfLine(inputs) { const { start: p1, center: p2, end: c, tolerance = 1e-7 } = inputs; const v1 = this.vector.sub({ first: p1, second: c }); const v2 = this.vector.sub({ first: p2, second: c }); const len1 = this.vector.length({ vector: v1 }); const len2 = this.vector.length({ vector: v2 }); if (len1 < tolerance || len2 < tolerance) { return 0; } const normV1 = this.vector.normalized({ vector: v1 }); const normV2 = this.vector.normalized({ vector: v2 }); if (!normV1 || !normV2) { return 0; } const cosAlpha = Math.max(-1.0, Math.min(1.0, this.vector.dot({ first: normV1, second: normV2 }))); if (cosAlpha > 1.0 - tolerance || cosAlpha < -1.0 + tolerance) { return 0; // Collinear } const alpha = Math.acos(cosAlpha); const tanHalfAlpha = Math.tan(alpha / 2.0); if (tanHalfAlpha < tolerance) { return 0; } // The distance 'd' from corner C to the tangent point must be less than or equal // to HALF the length of each segment. // d = r / tan(alpha/2) <= min(len1 / 2, len2 / 2) // r <= min(len1 / 2, len2 / 2) * tan(alpha/2) const maxRadius = Math.min(len1 / 2.0, len2 / 2.0) * tanHalfAlpha; return maxRadius; } /** * Calculates the maximum possible fillet radius at each corner of a polyline formed by * formed by a series of points. The fillet radius is calculated for each internal * corner and optionally for the closing corners if the polyline is closed. * @param inputs Points, checkLastWithFirst flag, and tolerance * @returns Array of maximum fillet radii for each corner * @group fillet * @shortname max fillets half line * @drawable false */ maxFilletsHalfLine(inputs) { const { points, checkLastWithFirst = false, tolerance = 1e-7 } = inputs; const n = points.length; const results = []; // Need at least 3 points to form a corner if (n < 3) { return results; } // 1. Calculate fillets for internal corners (P[1] to P[n-2]) for (let i = 1; i < n - 1; i++) { const p_prev = points[i - 1]; const p_corner = points[i]; const p_next = points[i + 1]; // Map geometric points to the DTO structure used by calculateMaxFilletRadiusHalfLine // DTO: { start: P_prev, center: P_next, end: P_corner, tolerance } const cornerInput = { start: p_prev, center: p_next, end: p_corner, tolerance: tolerance }; results.push(this.maxFilletRadiusHalfLine(cornerInput)); } // 2. Calculate fillets for closing corners if it's a closed polyline if (checkLastWithFirst && n >= 3) { // Corner at P[0] (formed by P[n-1]-P[0] and P[1]-P[0]) const p_prev_start = points[n - 1]; // Previous point is the last point const p_corner_start = points[0]; const p_next_start = points[1]; const startCornerInput = { start: p_prev_start, center: p_next_start, end: p_corner_start, tolerance: tolerance }; results.push(this.maxFilletRadiusHalfLine(startCornerInput)); // Corner at P[n-1] (formed by P[n-2]-P[n-1] and P[0]-P[n-1]) const p_prev_end = points[n - 2]; const p_corner_end = points[n - 1]; const p_next_end = points[0]; // Next point wraps around to the first point const endCornerInput = { start: p_prev_end, center: p_next_end, end: p_corner_end, tolerance: tolerance }; results.push(this.maxFilletRadiusHalfLine(endCornerInput)); } return results; } /** * Calculates the single safest maximum fillet radius that can be applied * uniformly to all corners of collection of points, based on the 'half-line' constraint. * This is determined by finding the minimum of the maximum possible fillet * radii calculated for each individual corner. * @param inputs Defines the points, whether it's closed, and an optional tolerance. * @returns The smallest value from the results of pointsMaxFilletsHalfLine. * Returns 0 if the polyline has fewer than 3 points or if any * calculated maximum radius is 0. * @group fillet * @shortname safest fillet radii points * @drawable false */ safestPointsMaxFilletHalfLine(inputs) { const allMaxRadii = this.maxFilletsHalfLine(inputs); if (allMaxRadii.length === 0) { // No corners, or fewer than 3 points. No fillet possible. return 0; } // Find the minimum radius among all calculated maximums. // If any corner calculation resulted in 0, the safest radius is 0. const safestRadius = Math.min(...allMaxRadii); // Ensure we don't return a negative radius if Math.min had weird input (shouldn't happen here) return Math.max(0, safestRadius); } /** * Removes consecutive duplicate points from array within tolerance. * Example: [[0,0,0], [0,0,0], [1,0,0], [1,0,0], [2,0,0]] → [[0,0,0], [1,0,0], [2,0,0]] * @param inputs points, tolerance and check first and last * @returns Points in the array without consecutive duplicates * @group clean * @shortname remove duplicates * @drawable true */ removeConsecutiveDuplicates(inputs) { return this.geometryHelper.removeConsecutivePointDuplicates(inputs.points, inputs.checkFirstAndLast, inputs.tolerance); } /** * Calculates normal vector from three points using cross product (perpendicular to plane). * Example: p1=[0,0,0], p2=[1,0,0], p3=[0,1,0] → [0,0,1] (pointing up from XY plane) * @param inputs Three points and the reverse normal flag * @returns Normal vector * @group create * @shortname normal from 3 points * @drawable true */ normalFromThreePoints(inputs) { const p1 = inputs.point1; const p2 = inputs.point2; const p3 = inputs.point3; if (!p1 || !p2 || !p3 || p1.length !== 3 || p2.length !== 3 || p3.length !== 3) { throw new Error("All points must be arrays of 3 numbers [x, y, z]"); } // Calculate vector A = p2 - p1 const ax = p2[0] - p1[0]; const ay = p2[1] - p1[1]; const az = p2[2] - p1[2]; // Calculate vector B = p3 - p1 const bx = p3[0] - p1[0]; const by = p3[1] - p1[1]; const bz = p3[2] - p1[2]; // Calculate the cross product N = A x B let nx = (ay * bz) - (az * by); let ny = (az * bx) - (ax * bz); let nz = (ax * by) - (ay * bx); // Check for collinear points (resulting in a zero vector) // A zero vector indicates the points don't form a unique plane. // You might want to handle this case depending on your application. if (nx === 0 && ny === 0 && nz === 0) { console.warn("Points are collinear or coincident; cannot calculate a unique normal."); return undefined; // Or return [0, 0, 0] if that's acceptable } if (inputs.reverseNormal) { nx = -nx; ny = -ny; nz = -nz; } return this.vector.normalized({ vector: [nx, ny, nz] }); } closestPointFromPointData(inputs) { let distance = Number.MAX_SAFE_INTEGER; let closestPointIndex; let point; for (let i = 0; i < inputs.points.length; i++) { const pt = inputs.points[i]; const currentDist = this.distance({ startPoint: inputs.point, endPoint: pt }); if (currentDist < distance) { distance = currentDist; closestPointIndex = i; point = pt; } } return { index: closestPointIndex + 1, distance, point }; } /** * Checks if two points are approximately equal within tolerance (distance-based comparison). * Example: point1=[1.0000001, 2.0, 3.0], point2=[1.0, 2.0, 3.0], tolerance=1e-6 → true * @param inputs Two points and the tolerance * @returns true if the points are almost equal * @group measure * @shortname two points almost equal * @drawable false */ twoPointsAlmostEqual(inputs) { const p1 = inputs.point1; const p2 = inputs.point2; const dist = this.distance({ startPoint: p1, endPoint: p2 }); return dist < inputs.tolerance; } /** * Sorts points lexicographically (by X, then Y, then Z coordinates). * Example: [[5,0,0], [1,0,0], [3,0,0]] → [[1,0,0], [3,0,0], [5,0,0]] * @param inputs points * @returns sorted points * @group sort * @shortname sort points * @drawable true */ sortPoints(inputs) { return [...inputs.points].sort((a, b) => { if (a[0] !== b[0]) return a[0] - b[0]; if (a[1] !== b[1]) return a[1] - b[1]; return a[2] - b[2]; }); } /** * Calculates the 6 vertices of a regular flat-top hexagon. * @param center The center point [x, y, z]. * @param radius The radius (distance from center to vertex). * @returns An array of 6 Point3 vertices in counter-clockwise order. */ getRegularHexagonVertices(center, radius) { const vertices = []; const cx = center[0]; const cy = center[1]; const cz = center[2]; const angleStep = Math.PI / 3; for (let i = 0; i < 6; i++) { const angle = angleStep * i; vertices.push([ cx + radius * Math.sin(angle), cy + radius * Math.cos(angle), cz // Maintain original Z ]); } return vertices; } }