Skip to content

Commit ad12453

Browse files
committed
feat: Add Ellipse class and implement intersection logic with Circle, Line, and Rectangle
1 parent cbd9efb commit ad12453

File tree

4 files changed

+341
-0
lines changed

4 files changed

+341
-0
lines changed

src/core/math/Circle.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Shape } from "./Shape";
22
import { Vector } from "./Vector";
33
import { Line } from "./Line";
4+
import { Ellipse } from "./Ellipse";
45
import { Rectangle } from "./Rectangle";
56
import { Polygon } from "./Polygon";
67

@@ -39,6 +40,8 @@ export class Circle implements Shape {
3940
return this.intersectsWithLine(other);
4041
} else if (other instanceof Circle) {
4142
return this.intersectsWithCircle(other);
43+
} else if (other instanceof Ellipse) {
44+
return other.intersects(this);
4245
} else if (other instanceof Rectangle) {
4346
return other.intersects(this);
4447
} else if (other instanceof Polygon) {

src/core/math/Ellipse.ts

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
import { Shape } from "./Shape";
2+
import { Vector } from "./Vector";
3+
import { Line } from "./Line";
4+
import { Rectangle } from "./Rectangle";
5+
import { Circle } from "./Circle";
6+
import { Polygon } from "./Polygon";
7+
8+
export class Ellipse implements Shape {
9+
private readonly center: Vector;
10+
private readonly radiusX: number;
11+
private readonly radiusY: number;
12+
private readonly rotation: number; // in degrees
13+
14+
constructor(x: number, y: number, radiusX: number, radiusY: number, rotation: number = 0) {
15+
this.center = new Vector(x, y);
16+
this.radiusX = radiusX;
17+
this.radiusY = radiusY;
18+
this.rotation = rotation;
19+
}
20+
21+
public static fromBounds(x: number, y: number, width: number, height: number, rotation: number = 0): Ellipse {
22+
return new Ellipse(x + width / 2, y + height / 2, width / 2, height / 2, rotation);
23+
}
24+
25+
public contains(point: Vector): boolean {
26+
// Transform point to ellipse's local coordinate system
27+
const localPoint = this.transformToLocal(point);
28+
29+
// Standard ellipse equation: (x²/a²) + (y²/b²) <= 1
30+
const normalized = (localPoint.x * localPoint.x) / (this.radiusX * this.radiusX) +
31+
(localPoint.y * localPoint.y) / (this.radiusY * this.radiusY);
32+
33+
return normalized <= 1;
34+
}
35+
36+
public intersects(other: Shape): boolean {
37+
if (other instanceof Line) {
38+
return this.intersectsWithLine(other);
39+
} else if (other instanceof Circle) {
40+
return this.intersectsWithCircle(other);
41+
} else if (other instanceof Rectangle) {
42+
return this.intersectsWithRectangle(other);
43+
} else if (other instanceof Ellipse) {
44+
return this.intersectsWithEllipse(other);
45+
} else if (other instanceof Polygon) {
46+
return other.intersects(this);
47+
}
48+
49+
return false;
50+
}
51+
52+
private intersectsWithLine(line: Line): boolean {
53+
const start = line.getStart();
54+
const end = line.getEnd();
55+
56+
// Check if endpoints are inside
57+
if (this.contains(start) || this.contains(end)) {
58+
return true;
59+
}
60+
61+
// Transform line to ellipse's local coordinate system
62+
const localStart = this.transformToLocal(start);
63+
const localEnd = this.transformToLocal(end);
64+
65+
// Line direction in local space
66+
const dx = localEnd.x - localStart.x;
67+
const dy = localEnd.y - localStart.y;
68+
69+
// Quadratic equation coefficients for line-ellipse intersection
70+
const a = (dx * dx) / (this.radiusX * this.radiusX) +
71+
(dy * dy) / (this.radiusY * this.radiusY);
72+
73+
const b = 2 * ((localStart.x * dx) / (this.radiusX * this.radiusX) +
74+
(localStart.y * dy) / (this.radiusY * this.radiusY));
75+
76+
const c = (localStart.x * localStart.x) / (this.radiusX * this.radiusX) +
77+
(localStart.y * localStart.y) / (this.radiusY * this.radiusY) - 1;
78+
79+
const discriminant = b * b - 4 * a * c;
80+
81+
if (discriminant < 0) {
82+
return false; // No intersection
83+
}
84+
85+
// Check if intersection points are on the line segment
86+
const sqrt = Math.sqrt(discriminant);
87+
const t1 = (-b - sqrt) / (2 * a);
88+
const t2 = (-b + sqrt) / (2 * a);
89+
90+
return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1) || (t1 < 0 && t2 > 1);
91+
}
92+
93+
private intersectsWithCircle(circle: Circle): boolean {
94+
// Approximate by checking if circle center is close enough to ellipse
95+
const circleCenter = circle.getPosition();
96+
const circleRadius = circle.getRadius();
97+
98+
// Check if circle center is inside expanded ellipse
99+
const expandedEllipse = new Ellipse(
100+
this.center.x,
101+
this.center.y,
102+
this.radiusX + circleRadius,
103+
this.radiusY + circleRadius,
104+
this.rotation
105+
);
106+
107+
if (expandedEllipse.contains(circleCenter)) {
108+
return true;
109+
}
110+
111+
// Check if any point on circle boundary intersects
112+
// Sample points on circle perimeter
113+
const samples = 16;
114+
for (let i = 0; i < samples; i++) {
115+
const angle = (360 / samples) * i;
116+
const point = circle.getBorderPoint(angle);
117+
if (this.contains(point)) {
118+
return true;
119+
}
120+
}
121+
122+
return false;
123+
}
124+
125+
private intersectsWithRectangle(rectangle: Rectangle): boolean {
126+
// Check if any corner is inside the ellipse
127+
const corners = rectangle.getCorners();
128+
if (this.contains(corners.topLeft) ||
129+
this.contains(corners.topRight) ||
130+
this.contains(corners.bottomLeft) ||
131+
this.contains(corners.bottomRight)) {
132+
return true;
133+
}
134+
135+
// Check if any side intersects the ellipse
136+
const sides = rectangle.getSides();
137+
if (this.intersects(sides.top) ||
138+
this.intersects(sides.right) ||
139+
this.intersects(sides.bottom) ||
140+
this.intersects(sides.left)) {
141+
return true;
142+
}
143+
144+
// Check if ellipse is completely inside rectangle
145+
return rectangle.contains(this.center);
146+
}
147+
148+
private intersectsWithEllipse(other: Ellipse): boolean {
149+
// Simplified approach: check if centers are close enough
150+
// and sample points on both ellipses
151+
const distance = this.center.distanceBetween(other.center);
152+
const maxDistance = Math.max(this.radiusX, this.radiusY) +
153+
Math.max(other.radiusX, other.radiusY);
154+
155+
if (distance > maxDistance) {
156+
return false;
157+
}
158+
159+
// Sample points on both ellipses
160+
const samples = 16;
161+
for (let i = 0; i < samples; i++) {
162+
const angle = (360 / samples) * i;
163+
164+
const point1 = this.getBorderPoint(angle);
165+
if (other.contains(point1)) {
166+
return true;
167+
}
168+
169+
const point2 = other.getBorderPoint(angle);
170+
if (this.contains(point2)) {
171+
return true;
172+
}
173+
}
174+
175+
return false;
176+
}
177+
178+
public getBorderPoint(angle: number): Vector {
179+
// Get point on ellipse boundary at given angle
180+
const radians = (angle * Math.PI) / 180;
181+
182+
// Parametric equation for ellipse
183+
const localX = this.radiusX * Math.cos(radians);
184+
const localY = this.radiusY * Math.sin(radians);
185+
186+
// Transform back to world space
187+
return this.transformToWorld(new Vector(localX, localY));
188+
}
189+
190+
public getBounds(): Rectangle {
191+
if (this.rotation === 0) {
192+
// Simple case: axis-aligned ellipse
193+
return new Rectangle(
194+
this.center.x - this.radiusX,
195+
this.center.y - this.radiusY,
196+
this.radiusX * 2,
197+
this.radiusY * 2
198+
);
199+
}
200+
201+
// For rotated ellipse, find the bounding box by sampling extreme points
202+
const samples = 32;
203+
let minX = Infinity;
204+
let minY = Infinity;
205+
let maxX = -Infinity;
206+
let maxY = -Infinity;
207+
208+
for (let i = 0; i < samples; i++) {
209+
const angle = (360 / samples) * i;
210+
const point = this.getBorderPoint(angle);
211+
212+
minX = Math.min(minX, point.x);
213+
minY = Math.min(minY, point.y);
214+
maxX = Math.max(maxX, point.x);
215+
maxY = Math.max(maxY, point.y);
216+
}
217+
218+
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
219+
}
220+
221+
public getArea(): number {
222+
return Math.PI * this.radiusX * this.radiusY;
223+
}
224+
225+
public getPerimeter(): number {
226+
// Ramanujan's approximation for ellipse perimeter
227+
const a = this.radiusX;
228+
const b = this.radiusY;
229+
const h = Math.pow(a - b, 2) / Math.pow(a + b, 2);
230+
231+
return Math.PI * (a + b) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)));
232+
}
233+
234+
public getCenter(): Vector {
235+
return this.center;
236+
}
237+
238+
public getRadiusX(): number {
239+
return this.radiusX;
240+
}
241+
242+
public getRadiusY(): number {
243+
return this.radiusY;
244+
}
245+
246+
public getDiameterX(): number {
247+
return this.radiusX * 2;
248+
}
249+
250+
public getDiameterY(): number {
251+
return this.radiusY * 2;
252+
}
253+
254+
public getRotation(): number {
255+
return this.rotation;
256+
}
257+
258+
public getFoci(): { focus1: Vector; focus2: Vector } {
259+
// Calculate foci for the ellipse
260+
const c = Math.sqrt(Math.abs(this.radiusX * this.radiusX - this.radiusY * this.radiusY));
261+
262+
let focus1Local: Vector;
263+
let focus2Local: Vector;
264+
265+
if (this.radiusX > this.radiusY) {
266+
focus1Local = new Vector(-c, 0);
267+
focus2Local = new Vector(c, 0);
268+
} else {
269+
focus1Local = new Vector(0, -c);
270+
focus2Local = new Vector(0, c);
271+
}
272+
273+
return {
274+
focus1: this.transformToWorld(focus1Local),
275+
focus2: this.transformToWorld(focus2Local)
276+
};
277+
}
278+
279+
public getEccentricity(): number {
280+
const a = Math.max(this.radiusX, this.radiusY);
281+
const b = Math.min(this.radiusX, this.radiusY);
282+
283+
return Math.sqrt(1 - (b * b) / (a * a));
284+
}
285+
286+
private transformToLocal(point: Vector): Vector {
287+
// Translate to origin
288+
const translated = new Vector(
289+
point.x - this.center.x,
290+
point.y - this.center.y
291+
);
292+
293+
if (this.rotation === 0) {
294+
return translated;
295+
}
296+
297+
// Rotate by negative rotation angle
298+
const radians = (-this.rotation * Math.PI) / 180;
299+
const cos = Math.cos(radians);
300+
const sin = Math.sin(radians);
301+
302+
return new Vector(
303+
translated.x * cos - translated.y * sin,
304+
translated.x * sin + translated.y * cos
305+
);
306+
}
307+
308+
private transformToWorld(localPoint: Vector): Vector {
309+
if (this.rotation === 0) {
310+
return new Vector(
311+
localPoint.x + this.center.x,
312+
localPoint.y + this.center.y
313+
);
314+
}
315+
316+
// Rotate by rotation angle
317+
const radians = (this.rotation * Math.PI) / 180;
318+
const cos = Math.cos(radians);
319+
const sin = Math.sin(radians);
320+
321+
const rotated = new Vector(
322+
localPoint.x * cos - localPoint.y * sin,
323+
localPoint.x * sin + localPoint.y * cos
324+
);
325+
326+
// Translate to center
327+
return new Vector(
328+
rotated.x + this.center.x,
329+
rotated.y + this.center.y
330+
);
331+
}
332+
}

src/core/math/Line.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { toDegrees } from "core/utils/Maths";
22
import { Shape } from "./Shape";
33
import { Vector } from "./Vector";
44
import { Circle } from "./Circle";
5+
import { Ellipse } from "./Ellipse";
56
import { Rectangle } from "./Rectangle";
67
import { Polygon } from "./Polygon";
78

@@ -37,6 +38,8 @@ export class Line implements Shape {
3738
return this.intersectsWithLine(other);
3839
} else if (other instanceof Circle) {
3940
return other.intersects(this);
41+
} else if (other instanceof Ellipse) {
42+
return other.intersects(this);
4043
} else if (other instanceof Rectangle) {
4144
return other.intersects(this);
4245
} else if (other instanceof Polygon) {

src/core/math/Rectangle.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Shape } from "./Shape";
33
import { Vector } from "./Vector";
44
import { Line } from "./Line";
55
import { Circle } from "./Circle";
6+
import { Ellipse } from "./Ellipse";
67
import { Polygon } from "./Polygon";
78

89
interface RectangleCorners {
@@ -39,6 +40,8 @@ export class Rectangle implements Shape {
3940
return this.intersectsWithLine(other);
4041
} else if (other instanceof Circle) {
4142
return this.intersectsWithCircle(other);
43+
} else if (other instanceof Ellipse) {
44+
return other.intersects(this);
4245
} else if (other instanceof Rectangle) {
4346
return this.intersectsWithRectangle(other);
4447
} else if (other instanceof Polygon) {

0 commit comments

Comments
 (0)