1use loam::math::{Bivector4, EuclideanR4, Plane4, Rotor4};
2use loam::runtime::{
3 Dispatch, DomainHandle, Domains, EdgeShading, Entity, Instance, Pose, Rejection, SpawnBundle,
4};
5
6use crate::catalog::ShapeEntry;
7use crate::consts::BASE_ROTATION_RATE;
8use crate::strip::{Strip, MAX_CELLS, MAX_T_EXTENT, MIN_CELLS, MIN_T_EXTENT};
9use crate::toy;
10use crate::{Card, Playground, Toy};
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub(crate) enum Mode {
14 #[default]
15 Rotate,
16 Toybox,
17}
18
19impl Mode {
20 pub(crate) const ALL: [Mode; 2] = [Mode::Rotate, Mode::Toybox];
21
22 pub(crate) fn name(self) -> &'static str {
23 match self {
24 Mode::Rotate => "rotate",
25 Mode::Toybox => "toybox",
26 }
27 }
28
29 pub(crate) fn from_token(token: &str) -> Option<Self> {
30 Mode::ALL.into_iter().find(|mode| mode.name() == token)
31 }
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub(crate) struct Spin {
36 pub(crate) planes: [bool; 6],
37 pub(crate) rate: f32,
38 pub(crate) running: bool,
39}
40
41impl Default for Spin {
42 fn default() -> Self {
43 Self {
44 planes: [false, false, true, false, false, false],
45 rate: 1.0,
46 running: false,
47 }
48 }
49}
50
51impl Spin {
52 pub(crate) fn omega(&self) -> Bivector4 {
53 Plane4::ALL
54 .into_iter()
55 .enumerate()
56 .filter(|(index, _)| self.planes[*index])
57 .fold(Bivector4::ZERO, |sum, (_, plane)| {
58 sum + plane.unit_bivector() * (BASE_ROTATION_RATE * self.rate)
59 })
60 }
61}
62
63pub(crate) fn set_mode(
64 dispatch: &mut Dispatch<'_, Playground>,
65 domain: DomainHandle<EuclideanR4>,
66 cards: &[Card],
67 mode: Mode,
68) -> Result<(), Rejection> {
69 let held = *dispatch.app.mode.get();
70 if held == mode {
71 return Ok(());
72 }
73 match (held, mode) {
74 (_, Mode::Toybox) => toy::populate(dispatch, domain, cards)?,
75 (Mode::Toybox, _) => toy::clear(dispatch, domain)?,
76 _ => {}
77 }
78 dispatch.app.strip.get_mut().on = false;
79 dispatch.app.display.get_mut().single = false;
80 dispatch.app.active.set(0);
81 dispatch.app.mode.set(mode);
82 dispatch.app.slice.set(0.0);
83 dispatch.app.spin.get_mut().running = false;
84 Ok(())
85}
86
87pub(crate) fn reset(
88 dispatch: &mut Dispatch<'_, Playground>,
89 domain: DomainHandle<EuclideanR4>,
90 cards: &[Card],
91) -> Result<(), Rejection> {
92 if *dispatch.app.mode.get() == Mode::Toybox {
93 toy::reset(dispatch, domain, cards)?;
94 } else {
95 let r4 = dispatch.domains.typed(domain)?;
96 for (entity, slot) in dispatch.app.slots.iter() {
97 if r4.poses().contains(entity) {
98 r4.set_pose(entity, Pose::at(slot.rest))?;
99 }
100 }
101 }
102 dispatch.app.time.set(0.0);
103 dispatch.app.angles.set([0.0; 6]);
104 dispatch.app.spin.set(Spin::default());
105 dispatch.app.slice.set(0.0);
106 Ok(())
107}
108
109pub(crate) fn throw_toy(
110 dispatch: &mut Dispatch<'_, Playground>,
111 domain: DomainHandle<EuclideanR4>,
112 entity: Entity,
113 pointer: [f32; 3],
114) -> Result<(), Rejection> {
115 if !dispatch.app.toys.contains(entity) {
116 return Ok(());
117 }
118 let rope = *dispatch.app.rope.get();
119 let physics = dispatch
120 .domains
121 .typed(domain)?
122 .physics_mut()
123 .ok_or(Rejection::Unsupported("the domain has no physics"))?;
124 toy::release(physics, entity, pointer, rope).map_err(Rejection::Edit)
125}
126
127pub(crate) fn toggle_plane(
128 dispatch: &mut Dispatch<'_, Playground>,
129 plane: usize,
130) -> Result<(), Rejection> {
131 let held = *dispatch
132 .app
133 .spin
134 .get()
135 .planes
136 .get(plane)
137 .ok_or(Rejection::Unsupported("no such plane"))?;
138 let elapsed = *dispatch.app.time.get() * BASE_ROTATION_RATE;
139 let angle = dispatch
140 .app
141 .angles
142 .get_mut()
143 .get_mut(plane)
144 .ok_or(Rejection::Unsupported("no such plane"))?;
145 *angle += if held { elapsed } else { -elapsed };
146 dispatch.app.spin.get_mut().planes[plane] = !held;
147 Ok(())
148}
149
150pub(crate) fn turn_row(
151 app: &Playground,
152 domains: &mut Domains,
153 domain: DomainHandle<EuclideanR4>,
154 rotor: Rotor4,
155) -> Result<(), Rejection> {
156 if *app.mode.get() != Mode::Rotate {
157 return Err(Rejection::Unsupported("rotation handles belong to Rotate"));
158 }
159 let entities = app.slots.iter().map(|(entity, _)| entity);
160 let r4 = domains.typed(domain)?;
161 for entity in entities {
162 if let Some(mut pose) = r4.poses().get(entity).copied() {
163 pose.frame = (rotor * pose.frame).normalize();
164 r4.set_pose(entity, pose)?;
165 }
166 }
167 Ok(())
168}
169
170pub(crate) fn set_shape(
171 dispatch: &mut Dispatch<'_, Playground>,
172 domain: DomainHandle<EuclideanR4>,
173 slot: usize,
174 entry: ShapeEntry,
175 card: Card,
176) -> Result<(), Rejection> {
177 let held = dispatch
178 .app
179 .slots
180 .iter()
181 .find(|(_, held)| held.index == slot)
182 .map(|(entity, held)| (entity, *held))
183 .ok_or(Rejection::Unsupported("no such slot"))?;
184 let (entity, mut row) = held;
185 if row.entry == entry {
186 return Ok(());
187 }
188 let is_toy = dispatch.app.toys.contains(entity);
189 let polytope = if is_toy {
190 Some(
191 entry
192 .collider_polytope()
193 .ok_or(Rejection::Unsupported("the toy has no collider"))?,
194 )
195 } else {
196 None
197 };
198 let geometry = if is_toy {
199 Some(
200 card.toy_geometry
201 .ok_or(Rejection::Unsupported("the toy has no prepared geometry"))?,
202 )
203 } else {
204 card.geometry
205 };
206 let frame = dispatch
207 .domains
208 .typed(domain)?
209 .poses()
210 .get(entity)
211 .map_or(Rotor4::IDENTITY, |pose| pose.frame);
212 let pose = polytope.map_or_else(
213 || Pose {
214 point: row.rest,
215 frame,
216 },
217 |polytope| toy::pose_at(polytope, row.rest.x),
218 );
219 row.entry = entry;
220 row.rest = pose.point;
221 dispatch.despawn(entity)?;
222 let mut bundle = SpawnBundle::new().at(domain, pose).row(row);
223 if is_toy {
224 bundle = bundle.row(Toy {
225 rest_anchor: pose.point,
226 rest_time: 0.0,
227 });
228 }
229 if let Some(geometry) = geometry {
230 let display = *dispatch.app.display.get();
231 let mode = *dispatch.app.color.get();
232 let shading = card
233 .shades
234 .map_or(EdgeShading::Material, |shades| shades.of(mode));
235 bundle = bundle.instance(
236 Instance::new(geometry, card.material)
237 .shaded(shading)
238 .sectioned(card.cut)
239 .line_style(display.wireframe_width_px, display.wireframe_opacity),
240 );
241 }
242 let spawned = dispatch.spawn(bundle)?;
243 if let Some(polytope) = polytope {
244 toy::add_body(dispatch, domain, spawned, polytope, pose)?;
245 }
246 Ok(())
247}
248
249pub(crate) fn set_strip(app: &mut Playground, mut strip: Strip) -> Result<(), Rejection> {
250 if strip.on && *app.mode.get() == Mode::Toybox {
251 return Err(Rejection::Unsupported("filmstrip belongs to Rotate"));
252 }
253 if !strip.t_extent.is_finite() {
254 return Err(Rejection::Unsupported("the t extent is not finite"));
255 }
256 if !strip.w && !strip.t {
257 return Err(Rejection::Unsupported("the strip needs one axis"));
258 }
259 strip.count_w = strip.count_w.clamp(MIN_CELLS, MAX_CELLS);
260 strip.count_t = strip.count_t.clamp(MIN_CELLS, MAX_CELLS);
261 strip.t_extent = strip.t_extent.clamp(MIN_T_EXTENT, MAX_T_EXTENT);
262 app.strip.set(strip);
263 Ok(())
264}