Skip to main content

lunar_math/
types.rs

1//! built-in engine types: transform, color, rect
2//!
3//! these are the common types used across all 2D game code.
4
5use crate::Vec2;
6use bevy_ecs::prelude::Component;
7
8/// 2D transform component: position, rotation, scale.
9///
10/// this is the primary way to represent an entity's placement in the world.
11/// it supports translation (x, y), rotation (radians), and scale (x, y).
12/// for depth sorting, assign a layer via `lunar::layers` constants on your [`Sprite`].
13///
14/// # builder pattern
15///
16/// transforms can be constructed fluently:
17///
18/// ```ignore
19/// let transform = Transform::from_xy(100.0, 200.0)
20///     .with_rotation(std::f32::consts::PI / 4.0)
21///     .with_scale(Vec2::new(2.0, 2.0));
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Component)]
24pub struct Transform {
25	/// x, y position
26	pub translation: Vec2,
27	/// rotation in radians
28	pub rotation: f32,
29	/// x, y scale
30	pub scale: Vec2,
31}
32
33impl Transform {
34	/// create a transform from a 2D translation.
35	/// rotation defaults to 0, scale to (1, 1).
36	#[must_use]
37	pub const fn from_translation(translation: Vec2) -> Self {
38		Self {
39			translation,
40			rotation: 0.0,
41			scale: Vec2::ONE,
42		}
43	}
44
45	/// create a transform from x, y coordinates.
46	/// shorthand for [`from_translation`](Transform::from_translation).
47	#[must_use]
48	pub const fn from_xy(x: f32, y: f32) -> Self {
49		Self::from_translation(Vec2::new(x, y))
50	}
51
52	/// set the rotation in radians.
53	/// returns self for builder-style chaining.
54	#[must_use]
55	pub const fn with_rotation(mut self, rotation: f32) -> Self {
56		self.rotation = rotation;
57		self
58	}
59
60	/// set the scale.
61	/// returns self for builder-style chaining.
62	#[must_use]
63	pub const fn with_scale(mut self, scale: Vec2) -> Self {
64		self.scale = scale;
65		self
66	}
67}
68
69impl Default for Transform {
70	fn default() -> Self {
71		Self {
72			translation: Vec2::ZERO,
73			rotation: 0.0,
74			scale: Vec2::ONE,
75		}
76	}
77}
78
79/// local transform: position, rotation, and scale relative to the parent entity.
80///
81/// when an entity has no parent, this is equivalent to world space.
82/// used in entity hierarchies for parent-child transform propagation.
83#[derive(Debug, Clone, Copy, PartialEq, Component)]
84pub struct LocalTransform {
85	/// x, y position relative to parent
86	pub translation: Vec2,
87	/// rotation in radians relative to parent
88	pub rotation: f32,
89	/// x, y scale relative to parent
90	pub scale: Vec2,
91}
92
93impl LocalTransform {
94	/// create a local transform from a 2D translation.
95	#[must_use]
96	pub const fn from_translation(translation: Vec2) -> Self {
97		Self {
98			translation,
99			rotation: 0.0,
100			scale: Vec2::ONE,
101		}
102	}
103
104	/// create a local transform from x, y coordinates.
105	#[must_use]
106	pub const fn from_xy(x: f32, y: f32) -> Self {
107		Self::from_translation(Vec2::new(x, y))
108	}
109
110	/// set the rotation in radians.
111	#[must_use]
112	pub const fn with_rotation(mut self, rotation: f32) -> Self {
113		self.rotation = rotation;
114		self
115	}
116
117	/// set the scale.
118	#[must_use]
119	pub const fn with_scale(mut self, scale: Vec2) -> Self {
120		self.scale = scale;
121		self
122	}
123}
124
125impl Default for LocalTransform {
126	fn default() -> Self {
127		Self {
128			translation: Vec2::ZERO,
129			rotation: 0.0,
130			scale: Vec2::ONE,
131		}
132	}
133}
134
135/// world transform: absolute position, rotation, and scale in world space.
136///
137/// this component is computed automatically from [`LocalTransform`] and
138/// parent hierarchy. do not modify directly — use [`LocalTransform`] instead.
139#[derive(Debug, Clone, Copy, PartialEq, Component)]
140pub struct WorldTransform {
141	/// absolute x, y position
142	pub translation: Vec2,
143	/// absolute rotation in radians
144	pub rotation: f32,
145	/// absolute scale
146	pub scale: Vec2,
147}
148
149impl WorldTransform {
150	/// create a world transform at the origin.
151	#[must_use]
152	pub const fn new() -> Self {
153		Self {
154			translation: Vec2::ZERO,
155			rotation: 0.0,
156			scale: Vec2::ONE,
157		}
158	}
159
160	/// create a world transform from x, y coordinates.
161	#[must_use]
162	pub const fn from_xy(x: f32, y: f32) -> Self {
163		Self {
164			translation: Vec2::new(x, y),
165			rotation: 0.0,
166			scale: Vec2::ONE,
167		}
168	}
169}
170
171impl Default for WorldTransform {
172	fn default() -> Self {
173		Self::new()
174	}
175}
176
177/// RGBA color type.
178///
179/// all channels are normalized to the range 0.0 - 1.0.
180/// common colors are provided as associated constants.
181///
182/// # example
183///
184/// ```ignore
185/// let red = Color::rgb(1.0, 0.0, 0.0);
186/// let semi_transparent = Color::rgba(1.0, 1.0, 1.0, 0.5);
187/// ```
188#[derive(Debug, Clone, Copy, PartialEq)]
189pub struct Color {
190	/// red channel (0.0 - 1.0)
191	pub r: f32,
192	/// green channel (0.0 - 1.0)
193	pub g: f32,
194	/// blue channel (0.0 - 1.0)
195	pub b: f32,
196	/// alpha channel (0.0 - 1.0)
197	pub a: f32,
198}
199
200impl Color {
201	/// pure black (0, 0, 0, 1).
202	pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
203	/// pure white (1, 1, 1, 1).
204	pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
205	/// pure red (1, 0, 0, 1).
206	pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
207	/// pure green (0, 1, 0, 1).
208	pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
209	/// pure blue (0, 0, 1, 1).
210	pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
211	/// fully transparent black (0, 0, 0, 0).
212	pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
213
214	/// create an RGB color with full opacity (alpha = 1.0).
215	#[must_use]
216	pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
217		Self { r, g, b, a: 1.0 }
218	}
219
220	/// create an RGBA color with explicit alpha.
221	#[must_use]
222	pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
223		Self { r, g, b, a }
224	}
225}
226
227impl Default for Color {
228	fn default() -> Self {
229		Self::WHITE
230	}
231}
232
233/// 2D rectangle: position + size.
234///
235/// represents a bounding box with top-left corner at (x, y)
236/// and dimensions (w, h). useful for collision detection and UI layout.
237///
238/// # example
239///
240/// ```ignore
241/// let rect = Rect::new(0.0, 0.0, 100.0, 50.0);
242/// if rect.contains(mouse_pos) {
243///     // clicked!
244/// }
245/// ```
246#[derive(Debug, Clone, Copy, PartialEq)]
247pub struct Rect {
248	/// x coordinate of top-left corner
249	pub x: f32,
250	/// y coordinate of top-left corner
251	pub y: f32,
252	/// width
253	pub w: f32,
254	/// height
255	pub h: f32,
256}
257
258impl Rect {
259	/// create a new rectangle from top-left corner and size.
260	#[must_use]
261	pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
262		Self { x, y, w, h }
263	}
264
265	/// create a rectangle from center point and half-size extents.
266	#[must_use]
267	pub fn from_center(center: Vec2, half_size: Vec2) -> Self {
268		Self {
269			x: center.x - half_size.x,
270			y: center.y - half_size.y,
271			w: half_size.x * 2.0,
272			h: half_size.y * 2.0,
273		}
274	}
275
276	/// check if a point lies inside this rectangle.
277	///
278	/// inclusive of all edges (touching counts as inside). contrast with [`intersects`](Self::intersects),
279	/// which is exclusive (touching edges do not count as overlap).
280	#[must_use]
281	pub fn contains(&self, point: Vec2) -> bool {
282		point.x >= self.x
283			&& point.x <= self.x + self.w
284			&& point.y >= self.y
285			&& point.y <= self.y + self.h
286	}
287
288	/// check if this rectangle overlaps another.
289	///
290	/// exclusive of touching edges (two rects that share only an edge do not overlap). contrast with
291	/// [`contains`](Self::contains), which is inclusive.
292	#[must_use]
293	pub fn intersects(&self, other: &Self) -> bool {
294		self.x < other.x + other.w
295			&& self.x + self.w > other.x
296			&& self.y < other.y + other.h
297			&& self.y + self.h > other.y
298	}
299
300	/// get the center point of this rectangle.
301	#[must_use]
302	pub fn center(&self) -> Vec2 {
303		Vec2::new(self.x + self.w / 2.0, self.y + self.h / 2.0)
304	}
305
306	/// get the top-left corner position.
307	#[must_use]
308	pub const fn top_left(&self) -> Vec2 {
309		Vec2::new(self.x, self.y)
310	}
311
312	/// get the bottom-right corner position.
313	#[must_use]
314	pub const fn bottom_right(&self) -> Vec2 {
315		Vec2::new(self.x + self.w, self.y + self.h)
316	}
317
318	/// expand or shrink the rect by the given deltas on all sides.
319	pub fn inflate(&mut self, dx: f32, dy: f32) {
320		self.x -= dx;
321		self.y -= dy;
322		self.w = dx.mul_add(2.0, self.w);
323		self.h = dy.mul_add(2.0, self.h);
324	}
325
326	/// constrain this rect to lie fully within another rect.
327	/// clamps both position and size so the right/bottom edges don't exceed the boundary.
328	pub fn clamp(&mut self, within: &Self) {
329		let x2 = (self.x + self.w).min(within.x + within.w);
330		let y2 = (self.y + self.h).min(within.y + within.h);
331		self.x = self.x.max(within.x);
332		self.y = self.y.max(within.y);
333		self.w = (x2 - self.x).max(0.0);
334		self.h = (y2 - self.y).max(0.0);
335	}
336
337	/// alias for [`Rect::contains`] — point collision check.
338	#[must_use]
339	pub fn collide_point(&self, point: Vec2) -> bool {
340		self.contains(point)
341	}
342
343	/// alias for [`Rect::intersects`] — rect collision check.
344	#[must_use]
345	pub fn collide_rect(&self, other: &Self) -> bool {
346		self.intersects(other)
347	}
348
349	/// return the smallest rect that contains both this rect and another.
350	#[must_use]
351	pub fn union(&self, other: &Self) -> Self {
352		let x = self.x.min(other.x);
353		let y = self.y.min(other.y);
354		let right = (self.x + self.w).max(other.x + other.w);
355		let bottom = (self.y + self.h).max(other.y + other.h);
356		Self {
357			x,
358			y,
359			w: right - x,
360			h: bottom - y,
361		}
362	}
363}
364
365#[cfg(test)]
366mod color_tests {
367	use super::*;
368
369	#[test]
370	fn rgb_constructs_with_full_opacity() {
371		let c = Color::rgb(0.5, 0.3, 0.8);
372		assert_eq!(c.r, 0.5);
373		assert_eq!(c.g, 0.3);
374		assert_eq!(c.b, 0.8);
375		assert_eq!(c.a, 1.0);
376	}
377
378	#[test]
379	fn rgba_constructs_with_explicit_alpha() {
380		let c = Color::rgba(1.0, 0.0, 0.0, 0.5);
381		assert_eq!(c.a, 0.5);
382	}
383
384	#[test]
385	fn constants_have_expected_values() {
386		assert_eq!(Color::BLACK, Color::rgb(0.0, 0.0, 0.0));
387		assert_eq!(Color::WHITE, Color::rgb(1.0, 1.0, 1.0));
388		assert_eq!(Color::RED, Color::rgb(1.0, 0.0, 0.0));
389		assert_eq!(Color::GREEN, Color::rgb(0.0, 1.0, 0.0));
390		assert_eq!(Color::BLUE, Color::rgb(0.0, 0.0, 1.0));
391		assert_eq!(Color::TRANSPARENT, Color::rgba(0.0, 0.0, 0.0, 0.0));
392	}
393
394	#[test]
395	fn default_is_white() {
396		assert_eq!(Color::default(), Color::WHITE);
397	}
398}
399
400#[cfg(test)]
401mod rect_tests {
402	use super::*;
403
404	#[test]
405	fn new_creates_rect() {
406		let r = Rect::new(10.0, 20.0, 100.0, 50.0);
407		assert_eq!(r.x, 10.0);
408		assert_eq!(r.y, 20.0);
409		assert_eq!(r.w, 100.0);
410		assert_eq!(r.h, 50.0);
411	}
412
413	#[test]
414	fn from_center_derives_correct_corners() {
415		let r = Rect::from_center(Vec2::new(50.0, 30.0), Vec2::new(40.0, 20.0));
416		assert_eq!(r.x, 10.0);
417		assert_eq!(r.y, 10.0);
418		assert_eq!(r.w, 80.0);
419		assert_eq!(r.h, 40.0);
420	}
421
422	#[test]
423	fn contains_point_inside() {
424		let r = Rect::new(0.0, 0.0, 100.0, 100.0);
425		assert!(r.contains(Vec2::new(50.0, 50.0)));
426		assert!(r.contains(Vec2::new(0.0, 0.0)));
427		assert!(r.contains(Vec2::new(100.0, 100.0)));
428	}
429
430	#[test]
431	fn contains_point_outside() {
432		let r = Rect::new(0.0, 0.0, 100.0, 100.0);
433		assert!(!r.contains(Vec2::new(-1.0, 50.0)));
434		assert!(!r.contains(Vec2::new(50.0, 101.0)));
435	}
436
437	#[test]
438	fn intersects_overlapping() {
439		let a = Rect::new(0.0, 0.0, 50.0, 50.0);
440		let b = Rect::new(25.0, 25.0, 50.0, 50.0);
441		assert!(a.intersects(&b));
442		assert!(b.intersects(&a));
443	}
444
445	#[test]
446	fn intersects_non_overlapping() {
447		let a = Rect::new(0.0, 0.0, 10.0, 10.0);
448		let b = Rect::new(20.0, 20.0, 10.0, 10.0);
449		assert!(!a.intersects(&b));
450	}
451
452	#[test]
453	fn center_is_midpoint() {
454		let r = Rect::new(10.0, 20.0, 100.0, 50.0);
455		let c = r.center();
456		assert_eq!(c.x, 60.0);
457		assert_eq!(c.y, 45.0);
458	}
459
460	#[test]
461	fn inflate_expands_on_all_sides() {
462		let mut r = Rect::new(10.0, 10.0, 20.0, 20.0);
463		r.inflate(5.0, 10.0);
464		assert_eq!(r.x, 5.0);
465		assert_eq!(r.y, 0.0);
466		assert_eq!(r.w, 30.0);
467		assert_eq!(r.h, 40.0);
468	}
469
470	#[test]
471	fn clamp_constrains_within_boundary() {
472		let mut r = Rect::new(-10.0, -10.0, 100.0, 100.0);
473		let boundary = Rect::new(0.0, 0.0, 50.0, 50.0);
474		r.clamp(&boundary);
475		assert_eq!(r.x, 0.0);
476		assert_eq!(r.y, 0.0);
477		assert_eq!(r.w, 50.0);
478		assert_eq!(r.h, 50.0);
479	}
480
481	#[test]
482	fn clamp_does_not_expand() {
483		let mut r = Rect::new(10.0, 10.0, 20.0, 20.0);
484		let boundary = Rect::new(0.0, 0.0, 100.0, 100.0);
485		r.clamp(&boundary);
486		assert_eq!(r, Rect::new(10.0, 10.0, 20.0, 20.0));
487	}
488
489	#[test]
490	fn collide_point_delegates_to_contains() {
491		let r = Rect::new(0.0, 0.0, 10.0, 10.0);
492		assert!(r.collide_point(Vec2::new(5.0, 5.0)));
493		assert!(!r.collide_point(Vec2::new(15.0, 5.0)));
494	}
495
496	#[test]
497	fn collide_rect_delegates_to_intersects() {
498		let a = Rect::new(0.0, 0.0, 10.0, 10.0);
499		assert!(a.collide_rect(&Rect::new(5.0, 5.0, 10.0, 10.0)));
500		assert!(!a.collide_rect(&Rect::new(20.0, 20.0, 10.0, 10.0)));
501	}
502
503	#[test]
504	fn union_encloses_both_rects() {
505		let a = Rect::new(0.0, 0.0, 10.0, 10.0);
506		let b = Rect::new(20.0, 20.0, 10.0, 10.0);
507		let u = a.union(&b);
508		assert_eq!(u, Rect::new(0.0, 0.0, 30.0, 30.0));
509	}
510
511	#[test]
512	fn union_with_contained_rect() {
513		let a = Rect::new(0.0, 0.0, 100.0, 100.0);
514		let b = Rect::new(10.0, 10.0, 20.0, 20.0);
515		assert_eq!(a.union(&b), a);
516	}
517
518	#[test]
519	fn top_left_returns_corner() {
520		let r = Rect::new(5.0, 10.0, 50.0, 30.0);
521		assert_eq!(r.top_left(), Vec2::new(5.0, 10.0));
522	}
523
524	#[test]
525	fn bottom_right_returns_corner() {
526		let r = Rect::new(5.0, 10.0, 50.0, 30.0);
527		assert_eq!(r.bottom_right(), Vec2::new(55.0, 40.0));
528	}
529}
530
531#[cfg(test)]
532mod transform_tests {
533	use super::*;
534
535	#[test]
536	fn default_transform_is_at_origin() {
537		let t = Transform::default();
538		assert_eq!(t.translation, Vec2::ZERO);
539		assert_eq!(t.rotation, 0.0);
540		assert_eq!(t.scale, Vec2::ONE);
541	}
542
543	#[test]
544	fn from_translation_sets_position() {
545		let t = Transform::from_translation(Vec2::new(100.0, 200.0));
546		assert_eq!(t.translation.x, 100.0);
547		assert_eq!(t.translation.y, 200.0);
548		assert_eq!(t.rotation, 0.0);
549		assert_eq!(t.scale, Vec2::ONE);
550	}
551
552	#[test]
553	fn from_xy_shorthand() {
554		let t = Transform::from_xy(50.0, 75.0);
555		assert_eq!(t.translation.x, 50.0);
556		assert_eq!(t.translation.y, 75.0);
557	}
558
559	#[test]
560	fn with_rotation_chain() {
561		let t = Transform::from_xy(0.0, 0.0).with_rotation(1.5);
562		assert_eq!(t.rotation, 1.5);
563	}
564
565	#[test]
566	fn with_scale_chain() {
567		let t = Transform::from_xy(0.0, 0.0).with_scale(Vec2::new(2.0, 3.0));
568		assert_eq!(t.scale, Vec2::new(2.0, 3.0));
569	}
570
571	#[test]
572	fn builder_chaining() {
573		let t = Transform::from_xy(10.0, 20.0)
574			.with_rotation(0.5)
575			.with_scale(Vec2::splat(2.0));
576		assert_eq!(t.translation.x, 10.0);
577		assert_eq!(t.translation.y, 20.0);
578		assert_eq!(t.rotation, 0.5);
579		assert_eq!(t.scale, Vec2::splat(2.0));
580	}
581
582	#[test]
583	fn local_transform_default() {
584		let t = LocalTransform::default();
585		assert_eq!(t.translation, Vec2::ZERO);
586		assert_eq!(t.rotation, 0.0);
587		assert_eq!(t.scale, Vec2::ONE);
588	}
589
590	#[test]
591	fn world_transform_new_is_at_origin() {
592		let t = WorldTransform::new();
593		assert_eq!(t.translation, Vec2::ZERO);
594		assert_eq!(t.rotation, 0.0);
595		assert_eq!(t.scale, Vec2::ONE);
596	}
597
598	#[test]
599	fn world_transform_from_xy() {
600		let t = WorldTransform::from_xy(30.0, 40.0);
601		assert_eq!(t.translation.x, 30.0);
602		assert_eq!(t.translation.y, 40.0);
603	}
604}