import * as THREE from "three" export class L33tGeometryUtil { /** * Checks if a line intersects with a box */ lineToBox(line: THREE.Line3, box: THREE.Box3): boolean { // Check if either end of the line is inside the box if (box.containsPoint(line.start) || box.containsPoint(line.end)) { return true; } // Check intersection with each face of the box const directions = [ new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1) ]; const delta = new THREE.Vector3().subVectors(line.end, line.start); for (const dir of directions) { const planeMin = new THREE.Plane().setFromNormalAndCoplanarPoint(dir, box.min); const planeMax = new THREE.Plane().setFromNormalAndCoplanarPoint(dir.negate(), box.max); const tMin = planeMin.distanceToPoint(line.start) / delta.dot(planeMin.normal); const tMax = planeMax.distanceToPoint(line.start) / delta.dot(planeMax.normal); if (tMin >= 0 && tMin <= 1 && this.pointInBox(line.at(tMin, new THREE.Vector3()), box)) { return true; } if (tMax >= 0 && tMax <= 1 && this.pointInBox(line.at(tMax, new THREE.Vector3()), box)) { return true; } } return false; } /** * Checks if two boxes intersect and handles box containment */ boxToBox(box1: THREE.Box3, box2: THREE.Box3): { intersects: boolean; contained: boolean } { const intersects = box1.intersectsBox(box2); // Check if box2 is completely contained within box1 const contained = ( box2.min.x >= box1.min.x && box2.max.x <= box1.max.x && box2.min.y >= box1.min.y && box2.max.y <= box1.max.y && box2.min.z >= box1.min.z && box2.max.z <= box1.max.z ); return { intersects, contained }; } /** * Checks if two lines intersect */ lineToLine(line1: THREE.Line3, line2: THREE.Line3): THREE.Vector3 | false { const direction1 = new THREE.Vector3().subVectors(line1.end, line1.start); const direction2 = new THREE.Vector3().subVectors(line2.end, line2.start); const cross = direction1.cross(direction2); if (cross.lengthSq() < Number.EPSILON) { // Lines are parallel return false; } const diff = new THREE.Vector3().subVectors(line2.start, line1.start); const t = diff.cross(direction2).dot(cross) / cross.lengthSq(); const u = diff.cross(direction1).dot(cross) / cross.lengthSq(); if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { return line1.at(t, new THREE.Vector3()); } return false; } /** * Checks if a line intersects with a polygon * @param polygon Array of Vector3 points defining the polygon vertices * @param line Line3 to check intersection with * @returns boolean indicating if the line intersects with the polygon */ polygonToLine(polygon: THREE.Vector3[], line: THREE.Line3): boolean { // Check if we have at least 3 points to form a polygon if (polygon.length < 3) { throw new Error("Polygon must have 3 vertices or more"); } // Create edges from the polygon points for (let i = 0; i < polygon.length; i++) { const point1 = polygon[i]; const point2 = polygon[(i + 1) % polygon.length]; // Create an edge line const edge = new THREE.Line3(point1, point2); // Check if the lines intersect const intersection = new THREE.Vector3(); const result = this.lineLineIntersection(line, edge, intersection); if (result) { // Check if intersection point is within both line segments if (this.isPointOnLineSegment(intersection, line) && this.isPointOnLineSegment(intersection, edge)) { return true; } } } return false; } /** * Checks if two lines intersect and returns the intersection point */ private lineLineIntersection( line1: THREE.Line3, line2: THREE.Line3, intersection: THREE.Vector3 ): boolean { const p1 = line1.start; const p2 = line1.end; const p3 = line2.start; const p4 = line2.end; // Line1 represented as p1 + t(p2-p1) // Line2 represented as p3 + s(p4-p3) const denominator = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y); // Lines are parallel if (Math.abs(denominator) < Number.EPSILON) { return false; } const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denominator; const ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denominator; intersection.x = p1.x + ua * (p2.x - p1.x); intersection.y = p1.y + ua * (p2.y - p1.y); intersection.z = 0; // Assuming 2D intersection return true; } /** * Checks if a point lies on a line segment */ private isPointOnLineSegment(point: THREE.Vector3, line: THREE.Line3): boolean { const epsilon = 1e-10; // Small value for floating point comparison // Check if point is within the bounding box of the line segment const minX = Math.min(line.start.x, line.end.x) - epsilon; const maxX = Math.max(line.start.x, line.end.x) + epsilon; const minY = Math.min(line.start.y, line.end.y) - epsilon; const maxY = Math.max(line.start.y, line.end.y) + epsilon; return point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY; } polygonToPolygon(polygon1: THREE.Vector3[], polygon2: THREE.Vector3[]): boolean { // Validate input if (polygon1.length < 3 || polygon2.length < 3) { throw new Error('Both polygons must have at least 3 vertices'); } // First check: edge intersections for (let i = 0; i < polygon1.length; i++) { const i2 = (i + 1) % polygon1.length; const edge1Start = polygon1[i]; const edge1End = polygon1[i2]; for (let j = 0; j < polygon2.length; j++) { const j2 = (j + 1) % polygon2.length; const edge2Start = polygon2[j]; const edge2End = polygon2[j2]; if (this.segmentsIntersect( edge1Start.x, edge1Start.y, edge1End.x, edge1End.y, edge2Start.x, edge2Start.y, edge2End.x, edge2End.y )) { return true; } } } // Second check: one polygon inside the other return this.isPointInPolygon(polygon1[0], polygon2) || this.isPointInPolygon(polygon2[0], polygon1); } /** * Determines if two line segments intersect * Uses the orientation method: two segments intersect if they have * different orientations relative to each other */ private segmentsIntersect( x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number ): boolean { const orientation1 = this.orientation(x1, y1, x2, y2, x3, y3); const orientation2 = this.orientation(x1, y1, x2, y2, x4, y4); const orientation3 = this.orientation(x3, y3, x4, y4, x1, y1); const orientation4 = this.orientation(x3, y3, x4, y4, x2, y2); // General case: different orientations if (orientation1 !== orientation2 && orientation3 !== orientation4) { return true; } // Special cases: points are collinear if (orientation1 === 0 && this.onSegment(x1, y1, x3, y3, x2, y2)) return true; if (orientation2 === 0 && this.onSegment(x1, y1, x4, y4, x2, y2)) return true; if (orientation3 === 0 && this.onSegment(x3, y3, x1, y1, x4, y4)) return true; if (orientation4 === 0 && this.onSegment(x3, y3, x2, y2, x4, y4)) return true; return false; } /** * Determines the orientation of three points (p, q, r) * Returns: * 0 -> points are collinear * 1 -> clockwise * -1 -> counterclockwise */ private orientation( px: number, py: number, qx: number, qy: number, rx: number, ry: number ): number { const val = (qy - py) * (rx - qx) - (qx - px) * (ry - qy); if (Math.abs(val) < Number.EPSILON) return 0; return val > 0 ? 1 : -1; } /** * Checks if point q lies on segment pr */ private onSegment( px: number, py: number, qx: number, qy: number, rx: number, ry: number ): boolean { return qx <= Math.max(px, rx) && qx >= Math.min(px, rx) && qy <= Math.max(py, ry) && qy >= Math.min(py, ry); } /** * Determines if a point lies inside a polygon using ray casting algorithm */ private isPointInPolygon(point: THREE.Vector3, polygon: THREE.Vector3[]): boolean { let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x, yi = polygon[i].y; const xj = polygon[j].x, yj = polygon[j].y; const intersect = ((yi > point.y) !== (yj > point.y)) && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; } private isPointInPolygon3D(point: THREE.Vector3, polygon: THREE.Vector3[]): boolean { if (polygon.length < 3) { throw new Error('Polygon must have at least 3 vertices'); } const normal = this.calculatePolygonNormal(polygon); const projected = this.projectPointOntoPlane(point, polygon[0], normal); let windingNumber = 0; for (let i = 0; i < polygon.length; i++) { const j = (i + 1) % polygon.length; if (this.isLeft(polygon[i], polygon[j], projected) > 0) { windingNumber++; } } return windingNumber % 2 !== 0; } private isLeft(p0: THREE.Vector3, p1: THREE.Vector3, point: THREE.Vector3): number { return (p1.x - p0.x) * (point.y - p0.y) - (point.x - p0.x) * (p1.y - p0.y); } /** * Projects a line onto a plane */ private projectLineOntoPlane(line: THREE.Line3, planePoint: THREE.Vector3, planeNormal: THREE.Vector3): THREE.Line3 { const projectedStart = this.projectPointOntoPlane(line.start, planePoint, planeNormal); const projectedEnd = this.projectPointOntoPlane(line.end, planePoint, planeNormal); return new THREE.Line3(projectedStart, projectedEnd); } /** * Checks if a point lies within a box */ private pointInBox(point: THREE.Vector3, box: THREE.Box3): boolean { return ( point.x >= box.min.x && point.x <= box.max.x && point.y >= box.min.y && point.y <= box.max.y && point.z >= box.min.z && point.z <= box.max.z ); } /** * Calculates the normal vector of a polygon */ private calculatePolygonNormal(polygon: THREE.Vector3[]): THREE.Vector3 { // Use the first three vertices to calculate the normal const v1 = new THREE.Vector3().subVectors(polygon[1], polygon[0]); const v2 = new THREE.Vector3().subVectors(polygon[2], polygon[0]); return v1.cross(v2).normalize(); } /** * Projects a point onto a plane defined by a point and normal */ private projectPointOntoPlane(point: THREE.Vector3, planePoint: THREE.Vector3, planeNormal: THREE.Vector3): THREE.Vector3 { const v = new THREE.Vector3().subVectors(point, planePoint); const dist = v.dot(planeNormal); return new THREE.Vector3().subVectors(point, planeNormal.clone().multiplyScalar(dist)); } /** * Validates if a polygon has valid geometry (at least 3 non-collinear vertices) */ private validatePolygon(polygon: THREE.Vector3[]): boolean { if (polygon.length < 3) { throw new Error("Polygon must have three vertices or more ") } // Check if vertices are not collinear const v1 = new THREE.Vector3().subVectors(polygon[1], polygon[0]); const v2 = new THREE.Vector3().subVectors(polygon[2], polygon[0]); const cross = v1.cross(v2); return cross.lengthSq() > Number.EPSILON; } }