1use glam::Vec2;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum TopologyType {
10 Euclidean,
11 Hyperbolic,
12 Spherical,
13 Toroidal,
14 Klein,
15}
16
17#[derive(Clone, Debug)]
19pub struct PortalFrame {
20 pub position: Vec2,
21 pub normal: Vec2,
22 pub width: f32,
23}
24
25impl PortalFrame {
26 pub fn new(position: Vec2, normal: Vec2, width: f32) -> Self {
27 Self {
28 position,
29 normal: normal.normalize(),
30 width,
31 }
32 }
33
34 pub fn endpoints(&self) -> (Vec2, Vec2) {
36 let tangent = Vec2::new(-self.normal.y, self.normal.x);
37 let half = tangent * (self.width / 2.0);
38 (self.position - half, self.position + half)
39 }
40
41 pub fn is_front_side(&self, point: Vec2) -> bool {
43 (point - self.position).dot(self.normal) > 0.0
44 }
45
46 pub fn signed_distance(&self, point: Vec2) -> f32 {
48 (point - self.position).dot(self.normal)
49 }
50}
51
52#[derive(Clone, Debug)]
54pub struct Portal {
55 pub entry: PortalFrame,
56 pub exit: PortalFrame,
57 pub topology_change: TopologyType,
58}
59
60impl Portal {
61 pub fn new(entry: PortalFrame, exit: PortalFrame, topology_change: TopologyType) -> Self {
62 Self {
63 entry,
64 exit,
65 topology_change,
66 }
67 }
68
69 pub fn will_cross(&self, pos: Vec2, vel: Vec2, dt: f32) -> bool {
72 let d_before = self.entry.signed_distance(pos);
73 let d_after = self.entry.signed_distance(pos + vel * dt);
74
75 if d_before <= 0.0 || d_after > 0.0 {
77 return false;
78 }
79
80 let t = d_before / (d_before - d_after);
82 let cross_point = pos + vel * dt * t;
83 let tangent = Vec2::new(-self.entry.normal.y, self.entry.normal.x);
84 let along = (cross_point - self.entry.position).dot(tangent);
85 along.abs() <= self.entry.width / 2.0
86 }
87}
88
89pub fn transform_through_portal(pos: Vec2, vel: Vec2, portal: &Portal) -> (Vec2, Vec2) {
92 let entry = &portal.entry;
93 let exit = &portal.exit;
94
95 let entry_tangent = Vec2::new(-entry.normal.y, entry.normal.x);
97 let local_along = (pos - entry.position).dot(entry_tangent);
98 let local_through = (pos - entry.position).dot(entry.normal);
99
100 let scale = exit.width / entry.width.max(1e-6);
102 let scaled_along = local_along * scale;
103
104 let exit_tangent = Vec2::new(-exit.normal.y, exit.normal.x);
106 let new_pos = exit.position + exit_tangent * scaled_along - exit.normal * local_through;
107
108 let vel_along = vel.dot(entry_tangent);
110 let vel_through = vel.dot(entry.normal);
111 let new_vel = exit_tangent * vel_along * scale - exit.normal * vel_through;
112
113 (new_pos, new_vel)
114}
115
116pub struct PortalManager {
120 pub portals: Vec<Portal>,
121}
122
123impl PortalManager {
124 pub fn new() -> Self {
125 Self {
126 portals: Vec::new(),
127 }
128 }
129
130 pub fn add_portal(&mut self, portal: Portal) {
132 self.portals.push(portal);
133 }
134
135 pub fn remove_portal(&mut self, index: usize) {
137 if index < self.portals.len() {
138 self.portals.remove(index);
139 }
140 }
141
142 pub fn update_entity(
145 &self,
146 pos: Vec2,
147 vel: Vec2,
148 dt: f32,
149 ) -> (Vec2, Vec2, Option<TopologyType>) {
150 for portal in &self.portals {
151 if portal.will_cross(pos, vel, dt) {
152 let (new_pos, new_vel) = transform_through_portal(pos, vel, portal);
153 return (new_pos, new_vel, Some(portal.topology_change));
154 }
155 }
156 (pos + vel * dt, vel, None)
157 }
158
159 pub fn visible_portals(&self, pos: Vec2) -> Vec<usize> {
161 self.portals
162 .iter()
163 .enumerate()
164 .filter(|(_, p)| p.entry.is_front_side(pos))
165 .map(|(i, _)| i)
166 .collect()
167 }
168}
169
170pub fn render_through_portal(viewpoint: Vec2, portal: &Portal, view_depth: f32) -> [Vec2; 4] {
173 let (left, right) = portal.entry.endpoints();
174
175 let to_left = (left - viewpoint).normalize();
177 let to_right = (right - viewpoint).normalize();
178
179 let entry = &portal.entry;
181 let exit = &portal.exit;
182 let exit_tangent = Vec2::new(-exit.normal.y, exit.normal.x);
183 let (exit_left, exit_right) = exit.endpoints();
184
185 let far_left = exit_left - exit.normal * view_depth;
187 let far_right = exit_right - exit.normal * view_depth;
188
189 [exit_left, exit_right, far_right, far_left]
190}
191
192#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn make_simple_portal() -> Portal {
199 Portal::new(
200 PortalFrame::new(Vec2::new(5.0, 0.0), Vec2::new(-1.0, 0.0), 4.0),
201 PortalFrame::new(Vec2::new(20.0, 0.0), Vec2::new(1.0, 0.0), 4.0),
202 TopologyType::Euclidean,
203 )
204 }
205
206 #[test]
207 fn test_portal_frame_endpoints() {
208 let frame = PortalFrame::new(Vec2::new(5.0, 0.0), Vec2::new(1.0, 0.0), 4.0);
209 let (a, b) = frame.endpoints();
210 assert!((a.y - (-2.0)).abs() < 1e-4);
211 assert!((b.y - 2.0).abs() < 1e-4);
212 assert!((a.x - 5.0).abs() < 1e-4);
213 }
214
215 #[test]
216 fn test_portal_front_side() {
217 let frame = PortalFrame::new(Vec2::new(5.0, 0.0), Vec2::new(1.0, 0.0), 4.0);
218 assert!(frame.is_front_side(Vec2::new(10.0, 0.0)));
219 assert!(!frame.is_front_side(Vec2::new(0.0, 0.0)));
220 }
221
222 #[test]
223 fn test_will_cross() {
224 let portal = make_simple_portal();
225 let crosses = portal.will_cross(
227 Vec2::new(6.0, 0.0), Vec2::new(-10.0, 0.0), 1.0,
230 );
231 assert!(crosses, "Should cross the portal");
232 }
233
234 #[test]
235 fn test_will_not_cross_wrong_direction() {
236 let portal = make_simple_portal();
237 let crosses = portal.will_cross(
238 Vec2::new(6.0, 0.0),
239 Vec2::new(10.0, 0.0), 1.0,
241 );
242 assert!(!crosses);
243 }
244
245 #[test]
246 fn test_will_not_cross_outside_width() {
247 let portal = make_simple_portal();
248 let crosses = portal.will_cross(
249 Vec2::new(6.0, 10.0), Vec2::new(-10.0, 0.0),
251 1.0,
252 );
253 assert!(!crosses);
254 }
255
256 #[test]
257 fn test_transform_through_portal() {
258 let portal = Portal::new(
259 PortalFrame::new(Vec2::new(5.0, 0.0), Vec2::new(-1.0, 0.0), 4.0),
260 PortalFrame::new(Vec2::new(20.0, 0.0), Vec2::new(1.0, 0.0), 4.0),
261 TopologyType::Euclidean,
262 );
263 let (new_pos, new_vel) = transform_through_portal(
265 Vec2::new(5.0, 0.0),
266 Vec2::new(-5.0, 0.0),
267 &portal,
268 );
269 assert!((new_pos.x - 20.0).abs() < 1e-3, "new_pos.x = {}", new_pos.x);
271 }
272
273 #[test]
274 fn test_portal_manager_no_portals() {
275 let mgr = PortalManager::new();
276 let (pos, vel, topo) = mgr.update_entity(
277 Vec2::new(0.0, 0.0),
278 Vec2::new(1.0, 0.0),
279 1.0,
280 );
281 assert!((pos.x - 1.0).abs() < 1e-4);
282 assert!(topo.is_none());
283 }
284
285 #[test]
286 fn test_portal_manager_crossing() {
287 let mut mgr = PortalManager::new();
288 mgr.add_portal(make_simple_portal());
289
290 let (new_pos, _new_vel, topo) = mgr.update_entity(
291 Vec2::new(6.0, 0.0),
292 Vec2::new(-10.0, 0.0),
293 1.0,
294 );
295 assert!(topo.is_some());
296 assert_eq!(topo.unwrap(), TopologyType::Euclidean);
297 }
298
299 #[test]
300 fn test_visible_portals() {
301 let mut mgr = PortalManager::new();
302 mgr.add_portal(make_simple_portal());
303 let visible = mgr.visible_portals(Vec2::new(4.0, 0.0));
304 assert_eq!(visible.len(), 1);
305
306 let visible2 = mgr.visible_portals(Vec2::new(6.0, 0.0));
307 assert_eq!(visible2.len(), 0); }
309
310 #[test]
311 fn test_render_through_portal() {
312 let portal = make_simple_portal();
313 let region = render_through_portal(Vec2::new(0.0, 0.0), &portal, 10.0);
314 assert_eq!(region.len(), 4);
316 }
317
318 #[test]
319 fn test_portal_manager_remove() {
320 let mut mgr = PortalManager::new();
321 mgr.add_portal(make_simple_portal());
322 assert_eq!(mgr.portals.len(), 1);
323 mgr.remove_portal(0);
324 assert_eq!(mgr.portals.len(), 0);
325 }
326}