1+ import { Vector } from "./Vector" ;
2+ import { Line } from "./Line" ;
3+ import { Circle } from "./Circle" ;
4+ import { Rectangle } from "./Rectangle" ;
5+ import { Ellipse } from "./Ellipse" ;
6+ import { Polygon } from "./Polygon" ;
7+ import { Shape } from "./Shape" ;
8+
9+ /**
10+ * Represents the result of a ray intersection test
11+ */
12+ export interface RaycastHit {
13+ /** The point where the ray hit */
14+ point : Vector ;
15+ /** Distance from ray origin to hit point */
16+ distance : number ;
17+ /** Normal vector at the hit point (perpendicular to surface) */
18+ normal : Vector ;
19+ /** The shape that was hit */
20+ shape ?: Shape ;
21+ /** Parameter t where ray hit (distance along ray direction) */
22+ t : number ;
23+ }
24+
25+ /**
26+ * Represents a 2D ray with an origin and direction.
27+ * Useful for raycasting, line-of-sight checks, click detection, projectiles, etc.
28+ */
29+ export class Ray {
30+ private readonly origin : Vector ;
31+ private readonly direction : Vector ;
32+
33+ constructor ( origin : Vector , direction : Vector ) {
34+ this . origin = origin ;
35+ this . direction = direction . normalize ( ) ; // Always normalized
36+ }
37+
38+ /**
39+ * Creates a ray from origin pointing towards a target point
40+ */
41+ public static fromPoints ( origin : Vector , target : Vector ) : Ray {
42+ const direction = target . subtract ( origin ) ;
43+ return new Ray ( origin , direction ) ;
44+ }
45+
46+ /**
47+ * Creates a ray from origin with an angle in degrees
48+ */
49+ public static fromAngle ( origin : Vector , angle : number ) : Ray {
50+ const direction = Vector . ofAngle ( angle ) ;
51+ return new Ray ( origin , direction ) ;
52+ }
53+
54+ /**
55+ * Gets a point along the ray at distance t
56+ */
57+ public getPoint ( t : number ) : Vector {
58+ return this . origin . add ( this . direction . multiply ( t ) ) ;
59+ }
60+
61+ /**
62+ * Casts the ray against a line segment
63+ */
64+ public castLine ( line : Line ) : RaycastHit | null {
65+ const start = line . getStart ( ) ;
66+ const end = line . getEnd ( ) ;
67+
68+ const lineDir = end . subtract ( start ) ;
69+ const v1 = this . origin . subtract ( start ) ;
70+ const v2 = new Vector ( - this . direction . y , this . direction . x ) ;
71+ const v3 = new Vector ( - lineDir . y , lineDir . x ) ;
72+
73+ const dot = lineDir . dot ( v2 ) ;
74+ if ( Math . abs ( dot ) < 0.000001 ) {
75+ return null ; // Parallel
76+ }
77+
78+ const t1 = lineDir . perpDot ( v1 ) / dot ;
79+ const t2 = v1 . dot ( v2 ) / dot ;
80+
81+ if ( t1 >= 0 && t2 >= 0 && t2 <= 1 ) {
82+ const point = this . getPoint ( t1 ) ;
83+ const distance = this . origin . distanceBetween ( point ) ;
84+
85+ // Normal is perpendicular to line direction
86+ const normal = new Vector ( - lineDir . y , lineDir . x ) . normalize ( ) ;
87+
88+ return {
89+ point,
90+ distance,
91+ normal,
92+ t : t1
93+ } ;
94+ }
95+
96+ return null ;
97+ }
98+
99+ /**
100+ * Casts the ray against a circle
101+ */
102+ public castCircle ( circle : Circle ) : RaycastHit | null {
103+ const center = circle . getPosition ( ) ;
104+ const radius = circle . getRadius ( ) ;
105+
106+ const oc = this . origin . subtract ( center ) ;
107+ const a = this . direction . dot ( this . direction ) ;
108+ const b = 2 * oc . dot ( this . direction ) ;
109+ const c = oc . dot ( oc ) - radius * radius ;
110+
111+ const discriminant = b * b - 4 * a * c ;
112+
113+ if ( discriminant < 0 ) {
114+ return null ; // No intersection
115+ }
116+
117+ const sqrt = Math . sqrt ( discriminant ) ;
118+ let t = ( - b - sqrt ) / ( 2 * a ) ;
119+
120+ // Use the closer intersection point
121+ if ( t < 0 ) {
122+ t = ( - b + sqrt ) / ( 2 * a ) ;
123+ }
124+
125+ if ( t < 0 ) {
126+ return null ; // Circle is behind ray
127+ }
128+
129+ const point = this . getPoint ( t ) ;
130+ const distance = this . origin . distanceBetween ( point ) ;
131+ const normal = point . subtract ( center ) . normalize ( ) ;
132+
133+ return {
134+ point,
135+ distance,
136+ normal,
137+ shape : circle ,
138+ t
139+ } ;
140+ }
141+
142+ /**
143+ * Casts the ray against a rectangle
144+ */
145+ public castRectangle ( rectangle : Rectangle ) : RaycastHit | null {
146+ const sides = rectangle . getSides ( ) ;
147+ const hits : RaycastHit [ ] = [ ] ;
148+
149+ // Check all four sides
150+ for ( const side of [ sides . top , sides . right , sides . bottom , sides . left ] ) {
151+ const hit = this . castLine ( side ) ;
152+ if ( hit ) {
153+ hits . push ( hit ) ;
154+ }
155+ }
156+
157+ if ( hits . length === 0 ) {
158+ return null ;
159+ }
160+
161+ // Return the closest hit
162+ hits . sort ( ( a , b ) => a . distance - b . distance ) ;
163+ return { ...hits [ 0 ] , shape : rectangle } ;
164+ }
165+
166+ /**
167+ * Casts the ray against an ellipse
168+ */
169+ public castEllipse ( ellipse : Ellipse ) : RaycastHit | null {
170+ // Transform ray to ellipse's local coordinate system
171+ const center = ellipse . getCenter ( ) ;
172+ const radiusX = ellipse . getRadiusX ( ) ;
173+ const radiusY = ellipse . getRadiusY ( ) ;
174+ const rotation = ellipse . getRotation ( ) ;
175+
176+ // Simplified approach: sample the ellipse border
177+ const samples = 64 ;
178+ let closestHit : RaycastHit | null = null ;
179+ let closestDistance = Infinity ;
180+
181+ for ( let i = 0 ; i < samples ; i ++ ) {
182+ const angle1 = ( 360 / samples ) * i ;
183+ const angle2 = ( 360 / samples ) * ( ( i + 1 ) % samples ) ;
184+
185+ const p1 = ellipse . getBorderPoint ( angle1 ) ;
186+ const p2 = ellipse . getBorderPoint ( angle2 ) ;
187+
188+ const segment = Line . ofPoints ( p1 , p2 ) ;
189+ const hit = this . castLine ( segment ) ;
190+
191+ if ( hit && hit . distance < closestDistance ) {
192+ closestDistance = hit . distance ;
193+ closestHit = hit ;
194+ }
195+ }
196+
197+ return closestHit ? { ...closestHit , shape : ellipse } : null ;
198+ }
199+
200+ /**
201+ * Casts the ray against a polygon
202+ */
203+ public castPolygon ( polygon : Polygon ) : RaycastHit | null {
204+ const sides = polygon . getSides ( ) ;
205+ const hits : RaycastHit [ ] = [ ] ;
206+
207+ for ( const side of sides ) {
208+ const hit = this . castLine ( side ) ;
209+ if ( hit ) {
210+ hits . push ( hit ) ;
211+ }
212+ }
213+
214+ if ( hits . length === 0 ) {
215+ return null ;
216+ }
217+
218+ // Return the closest hit
219+ hits . sort ( ( a , b ) => a . distance - b . distance ) ;
220+ return { ...hits [ 0 ] , shape : polygon } ;
221+ }
222+
223+ /**
224+ * Casts the ray against any shape
225+ */
226+ public cast ( shape : Shape ) : RaycastHit | null {
227+ if ( shape instanceof Line ) {
228+ return this . castLine ( shape ) ;
229+ } else if ( shape instanceof Circle ) {
230+ return this . castCircle ( shape ) ;
231+ } else if ( shape instanceof Rectangle ) {
232+ return this . castRectangle ( shape ) ;
233+ } else if ( shape instanceof Ellipse ) {
234+ return this . castEllipse ( shape ) ;
235+ } else if ( shape instanceof Polygon ) {
236+ return this . castPolygon ( shape ) ;
237+ }
238+
239+ return null ;
240+ }
241+
242+ /**
243+ * Casts the ray against multiple shapes and returns the closest hit
244+ */
245+ public castMultiple ( shapes : Shape [ ] ) : RaycastHit | null {
246+ const hits : RaycastHit [ ] = [ ] ;
247+
248+ for ( const shape of shapes ) {
249+ const hit = this . cast ( shape ) ;
250+ if ( hit ) {
251+ hits . push ( hit ) ;
252+ }
253+ }
254+
255+ if ( hits . length === 0 ) {
256+ return null ;
257+ }
258+
259+ // Return the closest hit
260+ hits . sort ( ( a , b ) => a . distance - b . distance ) ;
261+ return hits [ 0 ] ;
262+ }
263+
264+ /**
265+ * Casts the ray against multiple shapes and returns all hits sorted by distance
266+ */
267+ public castAll ( shapes : Shape [ ] ) : RaycastHit [ ] {
268+ const hits : RaycastHit [ ] = [ ] ;
269+
270+ for ( const shape of shapes ) {
271+ const hit = this . cast ( shape ) ;
272+ if ( hit ) {
273+ hits . push ( hit ) ;
274+ }
275+ }
276+
277+ hits . sort ( ( a , b ) => a . distance - b . distance ) ;
278+ return hits ;
279+ }
280+
281+ /**
282+ * Checks if the ray intersects any shape within maxDistance
283+ */
284+ public intersects ( shape : Shape , maxDistance ?: number ) : boolean {
285+ const hit = this . cast ( shape ) ;
286+
287+ if ( ! hit ) {
288+ return false ;
289+ }
290+
291+ if ( maxDistance !== undefined ) {
292+ return hit . distance <= maxDistance ;
293+ }
294+
295+ return true ;
296+ }
297+
298+ /**
299+ * Reflects the ray off a surface normal
300+ */
301+ public reflect ( normal : Vector ) : Ray {
302+ // R = D - 2(D·N)N
303+ const dotProduct = this . direction . dot ( normal ) ;
304+ const reflection = this . direction . subtract ( normal . multiply ( 2 * dotProduct ) ) ;
305+
306+ return new Ray ( this . origin , reflection ) ;
307+ }
308+
309+ /**
310+ * Creates a ray reflected off a hit point
311+ */
312+ public reflectFromHit ( hit : RaycastHit ) : Ray {
313+ const reflection = this . direction . subtract ( hit . normal . multiply ( 2 * this . direction . dot ( hit . normal ) ) ) ;
314+ return new Ray ( hit . point , reflection ) ;
315+ }
316+
317+ /**
318+ * Gets the origin of the ray
319+ */
320+ public getOrigin ( ) : Vector {
321+ return this . origin ;
322+ }
323+
324+ /**
325+ * Gets the direction of the ray (normalized)
326+ */
327+ public getDirection ( ) : Vector {
328+ return this . direction ;
329+ }
330+
331+ /**
332+ * Gets the angle of the ray in degrees
333+ */
334+ public getAngle ( ) : number {
335+ return this . direction . heading ( ) ;
336+ }
337+
338+ /**
339+ * Creates a line segment from the ray with a maximum length
340+ */
341+ public toLine ( maxLength : number = 1000 ) : Line {
342+ const end = this . getPoint ( maxLength ) ;
343+ return Line . ofPoints ( this . origin , end ) ;
344+ }
345+
346+ /**
347+ * Checks if a point is on the ray (within tolerance)
348+ */
349+ public containsPoint ( point : Vector , tolerance : number = 0.001 ) : boolean {
350+ const toPoint = point . subtract ( this . origin ) ;
351+ const projection = toPoint . dot ( this . direction ) ;
352+
353+ if ( projection < 0 ) {
354+ return false ; // Point is behind the ray
355+ }
356+
357+ const closestPoint = this . getPoint ( projection ) ;
358+ const distance = point . distanceBetween ( closestPoint ) ;
359+
360+ return distance <= tolerance ;
361+ }
362+
363+ /**
364+ * Gets the distance from a point to the ray
365+ */
366+ public distanceToPoint ( point : Vector ) : number {
367+ const toPoint = point . subtract ( this . origin ) ;
368+ const projection = toPoint . dot ( this . direction ) ;
369+
370+ if ( projection < 0 ) {
371+ return this . origin . distanceBetween ( point ) ;
372+ }
373+
374+ const closestPoint = this . getPoint ( projection ) ;
375+ return point . distanceBetween ( closestPoint ) ;
376+ }
377+
378+ /**
379+ * Gets the closest point on the ray to a given point
380+ */
381+ public getClosestPoint ( point : Vector ) : Vector {
382+ const toPoint = point . subtract ( this . origin ) ;
383+ const projection = toPoint . dot ( this . direction ) ;
384+
385+ if ( projection < 0 ) {
386+ return this . origin ;
387+ }
388+
389+ return this . getPoint ( projection ) ;
390+ }
391+ }
0 commit comments