1use std::collections::HashMap;
2
3use crate::field::{Field, NodeId, Vec2};
4use crate::viewport::Viewport;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct SpaceId(u64);
8
9impl SpaceId {
10 pub fn new(raw: u64) -> Self {
11 Self(raw)
12 }
13 pub fn as_u64(self) -> u64 {
14 self.0
15 }
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub enum PortalDir {
20 N,
21 E,
22 S,
23 W,
24}
25
26impl PortalDir {
27 pub fn opposite(self) -> Self {
28 match self {
29 PortalDir::N => PortalDir::S,
30 PortalDir::E => PortalDir::W,
31 PortalDir::S => PortalDir::N,
32 PortalDir::W => PortalDir::E,
33 }
34 }
35}
36
37pub struct World {
40 spaces: HashMap<SpaceId, Field>,
41 neighbors: HashMap<(SpaceId, PortalDir), SpaceId>,
42}
43
44impl World {
45 pub fn new() -> Self {
46 Self {
47 spaces: HashMap::new(),
48 neighbors: HashMap::new(),
49 }
50 }
51
52 pub fn add_space(&mut self, id: SpaceId, field: Field) {
53 self.spaces.insert(id, field);
54 }
55
56 pub fn space(&self, id: SpaceId) -> Option<&Field> {
57 self.spaces.get(&id)
58 }
59
60 pub fn space_mut(&mut self, id: SpaceId) -> Option<&mut Field> {
61 self.spaces.get_mut(&id)
62 }
63
64 pub fn set_neighbor(&mut self, a: SpaceId, dir: PortalDir, b: SpaceId) {
67 self.neighbors.insert((a, dir), b);
68 }
69
70 pub fn neighbor(&self, a: SpaceId, dir: PortalDir) -> Option<SpaceId> {
71 self.neighbors.get(&(a, dir)).copied()
72 }
73
74 pub fn transfer_node(
77 &mut self,
78 from_space: SpaceId,
79 node: NodeId,
80 dir: PortalDir,
81 from_vp: &Viewport,
82 to_vp: &Viewport,
83 ) -> bool {
84 let to_space = match self.neighbor(from_space, dir) {
85 Some(s) => s,
86 None => return false,
87 };
88
89 let (pos, node_data) = {
90 let from = match self.space_mut(from_space) {
91 Some(f) => f,
92 None => return false,
93 };
94
95 let n = match from.node(node) {
96 Some(n) => n,
97 None => return false,
98 };
99
100 if n.pinned {
102 return false;
103 }
104
105 let new_pos = map_across_portal(from_vp, to_vp, dir, n.pos);
107
108 let removed = match from.remove(node) {
110 Some(x) => x,
111 None => return false,
112 };
113
114 (new_pos, removed)
115 };
116
117 let to = match self.space_mut(to_space) {
119 Some(f) => f,
120 None => return false,
121 };
122
123 let mut insert = node_data;
124 insert.pos = pos;
125 to.insert_existing(insert);
126
127 true
128 }
129
130 pub fn transfer_cluster_by_core(
133 &mut self,
134 from_space: SpaceId,
135 core: NodeId,
136 dir: PortalDir,
137 from_vp: &Viewport,
138 to_vp: &Viewport,
139 ) -> bool {
140 let to_space = match self.neighbor(from_space, dir) {
141 Some(s) => s,
142 None => return false,
143 };
144
145 let (cid, members, core_pos) = {
147 let from = match self.space(from_space) {
148 Some(f) => f,
149 None => return false,
150 };
151
152 let cid = match from.cluster_id_for_core_public(core) {
153 Some(cid) => cid,
154 None => return false,
155 };
156
157 let cluster = match from.cluster(cid) {
158 Some(c) => c,
159 None => return false,
160 };
161
162 let core_node = match from.node(core) {
163 Some(n) => n,
164 None => return false,
165 };
166
167 if core_node.pinned {
169 return false;
170 }
171
172 for m in cluster.members() {
173 match from.node(*m) {
174 Some(n) if !n.pinned => {}
175 _ => return false,
176 }
177 }
178
179 (cid, cluster.members().to_vec(), core_node.pos)
180 };
181
182 let mapped = map_across_portal(from_vp, to_vp, dir, core_pos);
184 let delta = Vec2 {
185 x: mapped.x - core_pos.x,
186 y: mapped.y - core_pos.y,
187 };
188
189 let (cluster_obj, core_payload, member_payloads) = {
191 let from = match self.space_mut(from_space) {
192 Some(f) => f,
193 None => return false,
194 };
195
196 let cluster_obj = match from.remove_cluster(cid) {
197 Some(c) => c,
198 None => return false,
199 };
200
201 let core_node = match from.remove(core) {
202 Some(n) => n,
203 None => {
204 from.insert_cluster(cluster_obj);
205 return false;
206 }
207 };
208
209 let mut members_out = Vec::with_capacity(members.len());
210 for m in &members {
211 match from.remove(*m) {
212 Some(n) => members_out.push(n),
213 None => {
214 from.insert_existing(core_node);
216 from.insert_cluster(cluster_obj);
217 return false;
218 }
219 }
220 }
221
222 (cluster_obj, core_node, members_out)
223 };
224
225 let to = match self.space_mut(to_space) {
227 Some(f) => f,
228 None => return false,
229 };
230
231 to.insert_cluster(cluster_obj);
233
234 let mut core_insert = core_payload;
236 core_insert.pos = mapped;
237 to.insert_existing(core_insert);
238
239 for mut mp in member_payloads {
241 mp.pos.x += delta.x;
242 mp.pos.y += delta.y;
243 to.insert_existing(mp);
244 }
245
246 true
247 }
248}
249
250pub fn map_across_portal(from_vp: &Viewport, to_vp: &Viewport, dir: PortalDir, pos: Vec2) -> Vec2 {
258 let to = to_vp.rect();
259 let eps = 1.0;
260
261 let rel = Vec2 {
262 x: pos.x - from_vp.center.x,
263 y: pos.y - from_vp.center.y,
264 };
265
266 match dir {
267 PortalDir::E => Vec2 {
268 x: to.min.x + eps,
269 y: to_vp.center.y + rel.y,
270 },
271 PortalDir::W => Vec2 {
272 x: to.max.x - eps,
273 y: to_vp.center.y + rel.y,
274 },
275 PortalDir::N => Vec2 {
276 x: to_vp.center.x + rel.x,
277 y: to.min.y + eps,
278 },
279 PortalDir::S => Vec2 {
280 x: to_vp.center.x + rel.x,
281 y: to.max.y - eps,
282 },
283 }
284}
285
286impl Default for World {
287 fn default() -> Self {
288 Self::new()
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::field::Vec2;
296
297 #[test]
298 fn map_preserves_tangent_offset_east() {
299 let from_vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
300 let to_vp = Viewport::new(
301 Vec2 {
302 x: 1000.0,
303 y: 500.0,
304 },
305 Vec2 { x: 100.0, y: 100.0 },
306 );
307
308 let pos = Vec2 { x: 49.0, y: 10.0 }; let mapped = map_across_portal(&from_vp, &to_vp, PortalDir::E, pos);
310
311 assert!(mapped.x > to_vp.rect().min.x);
313 assert_eq!(mapped.y, to_vp.center.y + (pos.y - from_vp.center.y));
315 }
316
317 #[test]
318 fn transfer_node_moves_between_spaces() {
319 let mut w = World::new();
320 let mut fa = Field::new();
321 let fb = Field::new();
322
323 let a = SpaceId::new(1);
324 let b = SpaceId::new(2);
325
326 let n = fa.spawn_surface("A", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
327
328 w.add_space(a, fa);
329 w.add_space(b, fb);
330
331 w.set_neighbor(a, PortalDir::E, b);
332 w.set_neighbor(b, PortalDir::W, a);
333
334 let from_vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
335 let to_vp = Viewport::new(Vec2 { x: 1000.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
336
337 assert!(w.transfer_node(a, n, PortalDir::E, &from_vp, &to_vp));
338
339 assert!(w.space(a).unwrap().node(n).is_none());
340 assert!(w.space(b).unwrap().node(n).is_some());
341 }
342
343 #[test]
344 fn transfer_cluster_moves_cluster_record() {
345 let mut w = World::new();
346
347 let mut fa = Field::new();
348 let fb = Field::new();
349
350 let a = SpaceId::new(1);
351 let b = SpaceId::new(2);
352
353 let n1 = fa.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
354 let n2 = fa.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
355
356 let cid = fa.create_cluster(vec![n1, n2]).unwrap();
357 let core = fa.collapse_cluster(cid).unwrap();
358
359 w.add_space(a, fa);
360 w.add_space(b, fb);
361
362 w.set_neighbor(a, PortalDir::E, b);
363 w.set_neighbor(b, PortalDir::W, a);
364
365 let from_vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
366 let to_vp = Viewport::new(Vec2 { x: 1000.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
367
368 assert!(w.transfer_cluster_by_core(a, core, PortalDir::E, &from_vp, &to_vp));
369
370 assert!(w.space(a).unwrap().cluster(cid).is_none());
372
373 assert!(w.space(b).unwrap().cluster(cid).is_some());
375
376 let dest = w.space(b).unwrap();
378 assert_eq!(dest.cluster_id_for_core_public(core), Some(cid));
379 }
380}