UNPKG

@bitbybit-dev/base

Version:

Bit By Bit Developers Base CAD Library to Program Geometry

247 lines (246 loc) 12.3 kB
/** * Contains various mesh helper methods that are not necessarily present in higher level CAD kernels that bitbybit is using. */ export class MeshBitByBit { constructor(vector, polyline) { this.vector = vector; this.polyline = polyline; } /** * Calculates signed distance from a point to a plane (positive=above plane, negative=below). * Example: point=[0,5,0], plane={normal:[0,1,0], d:0} → 5 (point is 5 units above XZ plane) * @param inputs a point and a plane * @returns signed distance * @group base * @shortname signed dist to plane * @drawable false */ signedDistanceToPlane(inputs) { return this.vector.dot({ first: inputs.plane.normal, second: inputs.point }) - inputs.plane.d; } /** * Calculates plane equation from triangle vertices (normal vector and distance from origin). * Returns undefined if triangle is degenerate (zero area, collinear points). * Example: triangle=[[0,0,0], [1,0,0], [0,1,0]] → {normal:[0,0,1], d:0} (XY plane) * @param inputs triangle and tolerance * @returns triangle plane * @group traingle * @shortname triangle plane * @drawable false */ calculateTrianglePlane(inputs) { const EPSILON_SQ = Math.pow((inputs.tolerance || 1e-7), 2); const edge1 = this.vector.sub({ first: inputs.triangle[1], second: inputs.triangle[0] }); const edge2 = this.vector.sub({ first: inputs.triangle[2], second: inputs.triangle[0] }); const normal = this.vector.cross({ first: edge1, second: edge2 }); if (this.vector.lengthSq({ vector: normal }) < EPSILON_SQ) { return undefined; // Degenerate triangle } // Defensive copy if normalize modifies in-place const normalizedNormal = this.vector.normalized({ vector: normal }); const d = this.vector.dot({ first: normalizedNormal, second: inputs.triangle[0] }); return { normal: normalizedNormal, d: d }; } /** * Calculates intersection segment of two triangles (line segment where they cross). * Returns undefined if triangles don't intersect, are parallel, or are coplanar. * Example: triangle1=[[0,0,0], [2,0,0], [1,2,0]], triangle2=[[1,-1,1], [1,1,1], [1,1,-1]] → [[1,0,0], [1,1,0]] * @param inputs first triangle, second triangle, and tolerance * @returns intersection segment or undefined if no intersection * @group traingle * @shortname triangle-triangle int * @drawable false */ triangleTriangleIntersection(inputs) { const t1 = inputs.triangle1; const t2 = inputs.triangle2; const EPSILON = inputs.tolerance || 1e-7; const p1 = t1[0], p2 = t1[1], p3 = t1[2]; const q1 = t2[0], q2 = t2[1], q3 = t2[2]; const plane1 = this.calculateTrianglePlane({ triangle: t1, tolerance: EPSILON }); const plane2 = this.calculateTrianglePlane({ triangle: t2, tolerance: EPSILON }); if (!plane1 || !plane2) return undefined; const distQ_Plane1 = [ this.signedDistanceToPlane({ point: q1, plane: plane1 }), this.signedDistanceToPlane({ point: q2, plane: plane1 }), this.signedDistanceToPlane({ point: q3, plane: plane1 }), ]; if ((distQ_Plane1[0] > EPSILON && distQ_Plane1[1] > EPSILON && distQ_Plane1[2] > EPSILON) || (distQ_Plane1[0] < -EPSILON && distQ_Plane1[1] < -EPSILON && distQ_Plane1[2] < -EPSILON)) { return undefined; } const distP_Plane2 = [ this.signedDistanceToPlane({ point: p1, plane: plane2 }), this.signedDistanceToPlane({ point: p2, plane: plane2 }), this.signedDistanceToPlane({ point: p3, plane: plane2 }), ]; if ((distP_Plane2[0] > EPSILON && distP_Plane2[1] > EPSILON && distP_Plane2[2] > EPSILON) || (distP_Plane2[0] < -EPSILON && distP_Plane2[1] < -EPSILON && distP_Plane2[2] < -EPSILON)) { return undefined; } const allDistPZero = distP_Plane2.every(d => Math.abs(d) < EPSILON); const allDistQZero = distQ_Plane1.every(d => Math.abs(d) < EPSILON); if (allDistPZero && allDistQZero) { return undefined; // Explicitly not handling coplanar intersection areas } const lineDir = this.vector.cross({ first: plane1.normal, second: plane2.normal }); const det = this.vector.dot({ first: lineDir, second: lineDir }); // det = |lineDir|^2 if (det < EPSILON * EPSILON) { return undefined; // Planes parallel, no line intersection (coplanar case handled above) } // --- Calculate Interval Projections --- // Store the 3D points that define the intervals on the line const t1_intersection_points_3d = []; const t2_intersection_points_3d = []; const edges1 = [[p1, p2], [p2, p3], [p3, p1]]; const dists1 = distP_Plane2; for (let i = 0; i < 3; ++i) { const u = edges1[i][0]; const v = edges1[i][1]; const du = dists1[i]; const dv = dists1[(i + 1) % 3]; if (Math.abs(du) < EPSILON) t1_intersection_points_3d.push(u); // Start vertex is on plane2 // Removed the redundant check for dv here, handled by next edge start if ((du * dv) < 0 && Math.abs(du - dv) > EPSILON) { // Edge crosses plane2 const t = du / (du - dv); t1_intersection_points_3d.push(this.computeIntersectionPoint(u, v, t)); } } const edges2 = [[q1, q2], [q2, q3], [q3, q1]]; const dists2 = distQ_Plane1; for (let i = 0; i < 3; ++i) { const u = edges2[i][0]; const v = edges2[i][1]; const du = dists2[i]; const dv = dists2[(i + 1) % 3]; if (Math.abs(du) < EPSILON) t2_intersection_points_3d.push(u); // Start vertex is on plane1 // Removed redundant check for dv if ((du * dv) < 0 && Math.abs(du - dv) > EPSILON) { // Edge crosses plane1 const t = du / (du - dv); t2_intersection_points_3d.push(this.computeIntersectionPoint(u, v, t)); } } // We expect exactly two points for each triangle in the standard piercing case. // Handle potential duplicates or edge cases if more points are generated (e.g., edge lies on plane) // A simple check for the common case: if (t1_intersection_points_3d.length < 2 || t2_intersection_points_3d.length < 2) { // This can happen if triangles touch at a vertex or edge without crossing planes, // or due to numerical precision near edges/vertices. return undefined; // Treat touch as no intersection segment } // Calculate a robust origin ON the intersection line const n1 = plane1.normal; const n2 = plane2.normal; const d1 = plane1.d; const d2 = plane2.d; // Point P = ( (d1 * N2 - d2 * N1) x D ) / (D dot D) const term1 = this.vector.mul({ vector: n2, scalar: d1 }); const term2 = this.vector.mul({ vector: n1, scalar: d2 }); const termSub = this.vector.sub({ first: term1, second: term2 }); const crossTerm = this.vector.cross({ first: termSub, second: lineDir }); const lineOrigin = this.vector.mul({ vector: crossTerm, scalar: 1.0 / det }); // Project the 3D intersection points onto the lineDir, relative to lineOrigin const t1_params = t1_intersection_points_3d.map(p => this.vector.dot({ first: this.vector.sub({ first: p, second: lineOrigin }), second: lineDir })); const t2_params = t2_intersection_points_3d.map(p => this.vector.dot({ first: this.vector.sub({ first: p, second: lineOrigin }), second: lineDir })); // Find the intervals const t1Interval = [Math.min(...t1_params), Math.max(...t1_params)]; const t2Interval = [Math.min(...t2_params), Math.max(...t2_params)]; // Find the overlap of the two intervals const intersectionMinParam = Math.max(t1Interval[0], t2Interval[0]); const intersectionMaxParam = Math.min(t1Interval[1], t2Interval[1]); // Check if the overlap is valid if (intersectionMinParam < intersectionMaxParam - (EPSILON * det)) { // Let's use scaled epsilon for robustness against small det values. // Convert the final parameters back to 3D points using the lineOrigin // P = lineOrigin + dir * (param / det) const point1 = this.vector.add({ first: lineOrigin, second: this.vector.mul({ vector: lineDir, scalar: intersectionMinParam / det }) }); const point2 = this.vector.add({ first: lineOrigin, second: this.vector.mul({ vector: lineDir, scalar: intersectionMaxParam / det }) }); // Check if the resulting segment has non-zero length const segVec = this.vector.sub({ first: point1, second: point2 }); if (this.vector.lengthSq({ vector: segVec }) > EPSILON * EPSILON) { return [point1, point2]; } else { return undefined; // Degenerate segment } } else { return undefined; // Intervals do not overlap } } /** * Calculates all intersection segments between two triangle meshes (pairwise triangle tests). * Returns array of line segments where mesh surfaces intersect. * Example: cube mesh intersecting with sphere mesh → multiple segments forming intersection curve * @param inputs first mesh, second mesh, and tolerance * @returns array of intersection segments * @group mesh * @shortname mesh-mesh int segments * @drawable false */ meshMeshIntersectionSegments(inputs) { const mesh1 = inputs.mesh1; const mesh2 = inputs.mesh2; const intersectionSegments = []; for (let i = 0; i < mesh1.length; ++i) { for (let j = 0; j < mesh2.length; ++j) { const triangle1 = mesh1[i]; const triangle2 = mesh2[j]; const segment = this.triangleTriangleIntersection({ triangle1, triangle2, tolerance: inputs.tolerance }); if (segment) { intersectionSegments.push(segment); } } } return intersectionSegments; } /** * Calculates intersection polylines between two meshes by sorting segments into connected paths. * Segments are joined end-to-end to form continuous or closed curves. * Example: cube-sphere intersection → closed polyline loops where surfaces meet * @param inputs first mesh, second mesh, and tolerance * @returns array of intersection polylines * @group mesh * @shortname mesh-mesh int polylines * @drawable true */ meshMeshIntersectionPolylines(inputs) { const segments = this.meshMeshIntersectionSegments(inputs); return this.polyline.sortSegmentsIntoPolylines({ segments, tolerance: inputs.tolerance }); } /** * Calculates intersection points between two meshes as point arrays (one array per polyline). * Closed polylines have first point duplicated at end. * Example: cube-sphere intersection → arrays of points defining intersection curves * @param inputs first mesh, second mesh, and tolerance * @returns array of intersection points * @group mesh * @shortname mesh-mesh int points * @drawable false */ meshMeshIntersectionPoints(inputs) { const polylines = this.meshMeshIntersectionPolylines(inputs); return polylines.map(polyline => { if (polyline.isClosed) { return [...polyline.points, polyline.points[0]]; } else { return polyline.points; } }); } computeIntersectionPoint(u, v, t) { return this.vector.add({ first: u, second: this.vector.mul({ vector: this.vector.sub({ first: v, second: u }), scalar: t }) }); } }