@bitbybit-dev/base
Version:
Bit By Bit Developers Base CAD Library to Program Geometry
1,000 lines (999 loc) • 42.6 kB
JavaScript
/**
* 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;
}
}