1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
use bevy::math::Mat2;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use super::Transform2D;
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
pub struct Square {
/// Offset from the `Transform` transltion component
pub offset: Vec2,
/// Square's extents
///
/// `extents = Vec2::new(half width, half height)`
pub extents: Vec2,
}
impl Square {
const NORMALS: [Vec2; 2] = [Vec2::X, Vec2::Y];
/// Constructs a new square
pub fn new(extents: Vec2) -> Self {
Square {
offset: Vec2::ZERO,
extents,
}
}
/// Constructs a new square from absolute size(ie. width and height)
pub fn size(size: Vec2) -> Self {
Square {
offset: Vec2::ZERO,
extents: size * 0.5,
}
}
/// Offset from the `Transform` transltion component
pub fn with_offset(
mut self,
offset: Vec2,
) -> Self {
self.offset = offset;
self
}
}
impl Default for Square {
/// Default square with `extents = Vec2::splat(1.0)`
fn default() -> Self {
Self::new(Vec2::splat(1.0))
}
}
impl super::SAT for Square {
fn get_normals(&self, trans: &Transform2D) -> Box<(dyn Iterator<Item = bevy::prelude::Vec2> + '_)> {
let rot = Mat2::from_angle(trans.rotation());
Box::new(Square::NORMALS.iter().map(move |n| rot * *n))
}
fn project(&self, trans: &Transform2D, normal: Vec2) -> (f32,f32) {
let rot = Mat2::from_angle(trans.rotation());
let offset = rot * self.offset;
let verts = [
Vec2::new(1.0,1.0),
Vec2::new(1.0,-1.0),
Vec2::new(-1.0,1.0),
Vec2::new(-1.0,-1.0),
];
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for v in verts {
let v = rot * (v * self.extents) + trans.translation() + offset;
let proj = v.dot(normal);
min = min.min(proj);
max = max.max(proj);
}
(min, max)
}
fn get_closest_vertex(&self, trans: &Transform2D, vertex: Vec2) -> Vec2 {
let rot = Mat2::from_angle(trans.rotation());
let offset = rot * self.offset;
let verts = [
Vec2::new(1.0,1.0),
Vec2::new(1.0,-1.0),
Vec2::new(-1.0,1.0),
Vec2::new(-1.0,-1.0),
];
let mut min_l = f32::INFINITY;
let mut closest = Vec2::ZERO;
for v in verts {
let v = rot * (v * self.extents) + trans.translation() + offset;
let l = (v - vertex).length_squared();
if l < min_l {
min_l = l;
closest = v;
}
}
closest
}
fn ray(&self, trans: &Transform2D, ro: Vec2, rc: Vec2) -> Option<f32> {
let rot = Mat2::from_angle(-trans.rotation());
// IDEA: rotate the ray (the opposite direction) and then you can do simple ray vs aabb collision
let t = rot * (trans.translation()) + self.offset; // offset should not be rotated here
let ro = rot * ro;
let rc = rot * rc;
let smin = t - self.extents;
let smax = t + self.extents;
// if one of the cast components is 0.0, make sure we are in the bounds of that axle
// Why?
// We do this explicit check because the raycast formula i used doesnt handle cases where one of the components is 0
// as it would lead to division by 0(thus errors) and the `else NAN` part will make it completly ignore the collision
// on that axle
if rc.x.abs() < f32::EPSILON && !(smin.x <= ro.x && smax.x >= ro.x) {
return None; // if it doesnt collide on the X axle terminate it early
}
if rc.y.abs() < f32::EPSILON && !(smin.y <= ro.y && smax.y >= ro.y) {
return None; // if it doesnt collide on the X axle terminate it early
}
// The if else's are to make sure we dont divide by 0.0, because if the ray is parallel to one of the axis
// it will never collide(thus division by 0.0)
let xmin = if rc.x != 0.0 { (smin.x - ro.x) / rc.x } else { f32::NAN };
let xmax = if rc.x != 0.0 { (smax.x - ro.x) / rc.x } else { f32::NAN };
let ymin = if rc.y != 0.0 { (smin.y - ro.y) / rc.y } else { f32::NAN };
let ymax = if rc.y != 0.0 { (smax.y - ro.y) / rc.y } else { f32::NAN };
let min = (xmin.min(xmax)).max(ymin.min(ymax));
let max = (xmin.max(xmax)).min(ymin.max(ymax));
if max < 0.0 || min > max || min > 1.0 {
// either the shape is entirely behind us
// or we are not colliding at all
// or the shape is too far away
None
}
else if min < 0.0 {
// we are inside the shape
Some(max)
}
else {
// normal collision(gosh that was hard)
Some(min)
}
}
}
#[cfg(test)]
mod square_tests {
use crate::prelude::SAT;
use super::*;
const EPSILON: f32 = 0.0001;
#[test]
fn square_ray() {
let s = Square {
offset: Vec2::ZERO,
extents: Vec2::splat(10.0),
};
let ts = Transform2D::new(
Vec2::ZERO,
0.0,
Vec2::splat(1.0),
);
// TEST 1 - simple collision
let r1 = Vec2::new(10.0,0.0);
let t1 = Vec2::new(-16.0,-5.0);
let c1 = s.ray(&ts, t1, r1);
assert!(c1.is_some());
// should be Vec2(6.0,0.0) so 0.6
println!("{:?}", c1);
assert!((c1.unwrap() - 0.6).abs() < EPSILON);
// TEST 2 - no collision
let r2 = Vec2::new(1.0,1.0);
let t2 = Vec2::new(100.0,100.0);
let c2 = s.ray(&ts, t2, r2);
assert!(c2.is_none());
}
}