1use crate::Vec2;
6use bevy_ecs::prelude::Component;
7
8#[derive(Debug, Clone, Copy, PartialEq, Component)]
24pub struct Transform {
25 pub translation: Vec2,
27 pub rotation: f32,
29 pub scale: Vec2,
31}
32
33impl Transform {
34 #[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 #[must_use]
48 pub const fn from_xy(x: f32, y: f32) -> Self {
49 Self::from_translation(Vec2::new(x, y))
50 }
51
52 #[must_use]
55 pub const fn with_rotation(mut self, rotation: f32) -> Self {
56 self.rotation = rotation;
57 self
58 }
59
60 #[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#[derive(Debug, Clone, Copy, PartialEq, Component)]
84pub struct LocalTransform {
85 pub translation: Vec2,
87 pub rotation: f32,
89 pub scale: Vec2,
91}
92
93impl LocalTransform {
94 #[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 #[must_use]
106 pub const fn from_xy(x: f32, y: f32) -> Self {
107 Self::from_translation(Vec2::new(x, y))
108 }
109
110 #[must_use]
112 pub const fn with_rotation(mut self, rotation: f32) -> Self {
113 self.rotation = rotation;
114 self
115 }
116
117 #[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#[derive(Debug, Clone, Copy, PartialEq, Component)]
140pub struct WorldTransform {
141 pub translation: Vec2,
143 pub rotation: f32,
145 pub scale: Vec2,
147}
148
149impl WorldTransform {
150 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq)]
189pub struct Color {
190 pub r: f32,
192 pub g: f32,
194 pub b: f32,
196 pub a: f32,
198}
199
200impl Color {
201 pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
203 pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
205 pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
207 pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
209 pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
211 pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
213
214 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq)]
247pub struct Rect {
248 pub x: f32,
250 pub y: f32,
252 pub w: f32,
254 pub h: f32,
256}
257
258impl Rect {
259 #[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 #[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 #[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 #[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 #[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 #[must_use]
308 pub const fn top_left(&self) -> Vec2 {
309 Vec2::new(self.x, self.y)
310 }
311
312 #[must_use]
314 pub const fn bottom_right(&self) -> Vec2 {
315 Vec2::new(self.x + self.w, self.y + self.h)
316 }
317
318 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 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 #[must_use]
339 pub fn collide_point(&self, point: Vec2) -> bool {
340 self.contains(point)
341 }
342
343 #[must_use]
345 pub fn collide_rect(&self, other: &Self) -> bool {
346 self.intersects(other)
347 }
348
349 #[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}