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+ }
0 commit comments