diff --git a/Geometry/FindIntersections.js b/Geometry/FindIntersections.js new file mode 100644 index 0000000000..b250187c51 --- /dev/null +++ b/Geometry/FindIntersections.js @@ -0,0 +1,199 @@ +/** + * @typedef {Object} Point + * @property {number} x - The x-coordinate of the point. + * @property {number} y - The y-coordinate of the point. + */ + +/** + * @typedef {Object} Segment + * @property {Point} start - The start point of the segment. + * @property {Point} end - The end point of the segment. + */ + +/** + * @typedef {Object} Event + * @property {Point} point - The point where the event occurs. + * @property {Array} types - Types of the event ('left', 'right', or 'intersection'). + * @property {Array} segments - Segments associated with this event. + */ + +/** + * @typedef {Object} Intersection + * @property {Segment} segment1 - First segment of the intersection. + * @property {Segment} segment2 - Second segment of the intersection. + * @property {Point} point - The point of intersection. + */ + +/** + * Creates a new point. + * @param {number} x - The x-coordinate. + * @param {number} y - The y-coordinate. + * @returns {Point} The created point. + */ +export const createPoint = (x, y) => ({ x, y }) + +/** + * Creates a new event. + * @param {Point} point - The point where the event occurs. + * @param {Array} types - Types of the event. + * @param {Array} segments - Segments associated with this event. + * @returns {Event} The created event. + */ +export const createEvent = (point, types, segments) => ({ + point, + types, + segments +}) + +/** + * Compares two points lexicographically. + * @param {Point} a - The first point. + * @param {Point} b - The second point. + * @returns {number} Negative if a < b, positive if a > b, zero if equal. + */ +export const comparePoints = (a, b) => { + if (a.x !== b.x) return a.x - b.x + return a.y - b.y +} + +/** + * Compares two segments based on their y-coordinate at the current x-coordinate of the sweep line. + * @param {Segment} a - The first segment. + * @param {Segment} b - The second segment. + * @returns {number} Negative if a < b, positive if a > b, zero if equal. + */ +export const compareSegmentsY = (a, b) => a.start.y - b.start.y + +/** + * Calculates the intersection point of two line segments. + * @param {Segment} seg1 - First segment. + * @param {Segment} seg2 - Second segment. + * @returns {Point|null} The intersection point, or null if segments are parallel. + */ +export const calculateIntersectionPoint = (seg1, seg2) => { + const x1 = seg1.start.x, + y1 = seg1.start.y + const x2 = seg1.end.x, + y2 = seg1.end.y + const x3 = seg2.start.x, + y3 = seg2.start.y + const x4 = seg2.end.x, + y4 = seg2.end.y + + const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if (Math.abs(denom) < Number.EPSILON) return null // parallel lines + + const x = + ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom + const y = + ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom + + return createPoint(x, y) +} + +/** + * Handles potential intersection between two segments. + * @param {Segment} seg1 - First segment. + * @param {Segment} seg2 - Second segment. + * @param {Array} events - Array of events to update. + * @param {Array} intersections - Array of intersections to update. + */ +export const handlePotentialIntersection = ( + seg1, + seg2, + events, + intersections +) => { + const intersectionPoint = calculateIntersectionPoint(seg1, seg2) + if (intersectionPoint) { + events.push(createEvent(intersectionPoint, ['intersection'], [seg1, seg2])) + intersections.push({ + segment1: seg1, + segment2: seg2, + point: intersectionPoint + }) + } +} + +/** + * Finds all intersections among a set of line segments using the Bentley-Ottmann algorithm. + * @param {Array} segments - Array of line segments to check for intersections. + * @returns {Array} Array of all intersections found. + */ +const findIntersections = (segments) => { + const events = [] + const intersections = [] + let sweepLineStatus = [] + + // Create initial events + segments.forEach((segment) => { + events.push(createEvent(segment.start, ['left'], [segment])) + events.push(createEvent(segment.end, ['right'], [segment])) + }) + + // Sort events + events.sort((a, b) => { + const pointCompare = comparePoints(a.point, b.point) + if (pointCompare !== 0) return pointCompare + + // If points are the same, prioritize: intersection > left > right + const typePriority = { intersection: 0, left: 1, right: 2 } + return ( + Math.min(...a.types.map((t) => typePriority[t])) - + Math.min(...b.types.map((t) => typePriority[t])) + ) + }) + + // Process events + events.forEach((event) => { + if (event.types.includes('left')) { + event.segments.forEach((segment) => { + sweepLineStatus.push(segment) + sweepLineStatus.sort(compareSegmentsY) + const index = sweepLineStatus.indexOf(segment) + const lower = index > 0 ? sweepLineStatus[index - 1] : null + const upper = + index < sweepLineStatus.length - 1 ? sweepLineStatus[index + 1] : null + if (lower) + handlePotentialIntersection(segment, lower, events, intersections) + if (upper) + handlePotentialIntersection(segment, upper, events, intersections) + }) + } + + if (event.types.includes('right')) { + event.segments.forEach((segment) => { + const index = sweepLineStatus.indexOf(segment) + const lower = index > 0 ? sweepLineStatus[index - 1] : null + const upper = + index < sweepLineStatus.length - 1 ? sweepLineStatus[index + 1] : null + sweepLineStatus = sweepLineStatus.filter((s) => s !== segment) + if (lower && upper) + handlePotentialIntersection(lower, upper, events, intersections) + }) + } + + if (event.types.includes('intersection')) { + // Re-check all pairs of segments at this x-coordinate for intersections + const segmentsAtX = sweepLineStatus.filter( + (s) => + Math.min(s.start.x, s.end.x) <= event.point.x && + Math.max(s.start.x, s.end.x) >= event.point.x + ) + for (let i = 0; i < segmentsAtX.length; i++) { + for (let j = i + 1; j < segmentsAtX.length; j++) { + handlePotentialIntersection( + segmentsAtX[i], + segmentsAtX[j], + events, + intersections + ) + } + } + } + }) + + return intersections +} + +export default findIntersections diff --git a/Geometry/Test/FindIntersections.test.js b/Geometry/Test/FindIntersections.test.js new file mode 100644 index 0000000000..e71dfcfa5f --- /dev/null +++ b/Geometry/Test/FindIntersections.test.js @@ -0,0 +1,159 @@ +import findIntersections, { + createPoint, + createEvent, + comparePoints, + compareSegmentsY, + calculateIntersectionPoint, + handlePotentialIntersection +} from '../FindIntersections' + +describe('Geometry Functions', () => { + describe('createPoint', () => { + it('should create a point with given coordinates', () => { + const point = createPoint(1, 2) + expect(point).toEqual({ x: 1, y: 2 }) + }) + }) + + describe('createEvent', () => { + it('should create an event with the given parameters', () => { + const point = createPoint(1, 2) + const event = createEvent(point, ['left'], []) + expect(event).toEqual({ + point, + types: ['left'], + segments: [] + }) + }) + }) + + describe('comparePoints', () => { + it('should return negative if first point is less than second', () => { + const a = createPoint(1, 2) + const b = createPoint(2, 3) + expect(comparePoints(a, b)).toBeLessThan(0) + }) + + it('should return positive if first point is greater than second', () => { + const a = createPoint(2, 3) + const b = createPoint(1, 2) + expect(comparePoints(a, b)).toBeGreaterThan(0) + }) + + it('should return zero if points are equal', () => { + const a = createPoint(1, 2) + const b = createPoint(1, 2) + expect(comparePoints(a, b)).toBe(0) + }) + }) + + describe('compareSegmentsY', () => { + it('should compare segments based on their start y-coordinates', () => { + const seg1 = { start: { x: 0, y: 1 }, end: { x: 1, y: 1 } } + const seg2 = { start: { x: 0, y: 2 }, end: { x: 1, y: 2 } } + expect(compareSegmentsY(seg1, seg2)).toBe(-1) + }) + + it('should return zero if segments have the same start y-coordinate', () => { + const seg1 = { start: { x: 0, y: 1 }, end: { x: 1, y: 1 } } + const seg2 = { start: { x: -1, y: 1 }, end: { x: -2, y: 1 } } + expect(compareSegmentsY(seg1, seg2)).toBe(0) + }) + + it('should return positive if first segment is greater than second', () => { + const seg1 = { start: { x: 0, y: 3 }, end: { x: 1, y: 3 } } + const seg2 = { start: { x: -1, y: 2 }, end: { x: -2, y: 2 } } + expect(compareSegmentsY(seg1, seg2)).toBe(1) + }) + }) + + describe('calculateIntersectionPoint', () => { + it('should return null for parallel segments', () => { + const seg1 = { start: createPoint(0, 0), end: createPoint(5, 5) } + const seg2 = { start: createPoint(0, 1), end: createPoint(5, 6) } + expect(calculateIntersectionPoint(seg1, seg2)).toBeNull() + }) + + it('should return intersection point for intersecting segments', () => { + const seg1 = { start: createPoint(0, 0), end: createPoint(5, 5) } + const seg2 = { start: createPoint(0, 5), end: createPoint(5, 0) } + const intersection = calculateIntersectionPoint(seg1, seg2) + expect(intersection).toEqual(createPoint(2.5, 2.5)) + }) + + it('should handle vertical and horizontal lines correctly', () => { + const seg1 = { start: createPoint(0, -1), end: createPoint(0, 1) } // vertical line + const seg2 = { start: createPoint(-1, 0), end: createPoint(1, 0) } // horizontal line + const intersection = calculateIntersectionPoint(seg1, seg2) + + expect(intersection.x).toBeCloseTo(0) // Check for close proximity to zero + expect(intersection.y).toBeCloseTo(0) // Check for close proximity to zero + }) + + it('should handle coincident segments correctly', () => { + const seg1 = { start: createPoint(0, -1), end: createPoint(0, -3) } + const seg2 = { start: createPoint(0, -3), end: createPoint(0, -4) } + expect(calculateIntersectionPoint(seg1, seg2)).toBeNull() // should not intersect as they are collinear but not overlapping + }) + + it('should handle edge cases for intersection calculations', () => { + const seg1 = { start: createPoint(-10, -10), end: createPoint(-10, -10) } + const seg2 = { start: createPoint(-10, -10), end: createPoint(-10, -10) } + + const intersection = calculateIntersectionPoint(seg1, seg2) + + expect(intersection).toBeNull() // Expect null since they are coincident lines. + }) + }) + + describe('handlePotentialIntersection', () => { + it('should push intersection event and intersection data when segments intersect', () => { + const events = [] + const intersections = [] + const seg1 = { start: createPoint(0, 0), end: createPoint(5, 5) } + const seg2 = { start: createPoint(0, 5), end: createPoint(5, -5) } + + handlePotentialIntersection(seg1, seg2, events, intersections) + + expect(events.length).toBeGreaterThan(0) + expect(intersections.length).toBeGreaterThan(0) + }) + + it('should not push anything when segments do not intersect', () => { + const events = [] + const intersections = [] + const seg1 = { start: createPoint(0, -10), end: createPoint(-10, -20) } + const seg2 = { start: createPoint(-20, -30), end: createPoint(-30, -40) } + + handlePotentialIntersection(seg1, seg2, events, intersections) + + expect(events.length).toBe(0) + expect(intersections.length).toBe(0) + }) + }) + + describe('findIntersections', () => { + it('should find intersections among segments correctly', () => { + const segments = [ + { start: createPoint(0, 0), end: createPoint(5, 5) }, + { start: createPoint(0, 5), end: createPoint(5, 0) }, + { start: createPoint(6, 6), end: createPoint(7, 7) } + ] + + const result = findIntersections(segments) + + expect(result.length).toBeGreaterThanOrEqual(1) // There should be at least one intersection + }) + + it('should return an empty array when no intersections exist', () => { + const segments = [ + { start: createPoint(10, 10), end: createPoint(20, 20) }, + { start: createPoint(30, 30), end: createPoint(40, 40) } + ] + + const result = findIntersections(segments) + + expect(result).toEqual([]) // No intersections should be found + }) + }) +})