Skip to main content

polytope_playground/
main.rs

1use std::sync::Arc;
2
3use glam::Vec4;
4use loam::app::args::Args;
5use loam::app::session::{launch_or_headless, look, FrameHook, Orbit, SessionApp};
6use loam::math::{Bivector, Bivector4, EuclideanR4};
7use loam::render::pass::FramePass;
8use loam::render::raymarch::BodyUniform;
9use loam::render::{HyperslicePass, LinePass, PointPass, SkyGroundPass};
10use loam::runtime::host::run_headless;
11use loam::runtime::host::{HostConfig, HostError};
12#[cfg(test)]
13use loam::runtime::Input;
14use loam::runtime::{
15    ActionId, AppCommand, Bindings, Ctx, Dispatch, DomainBuilder, DomainError, DomainHandle,
16    Domains, Entity, Eye, GrabHold, Instance, Key, LogCapacity, Material, MaterialId, Outcome,
17    Phase, Pointer, PointerButton, PointerPhase, Pose, PreparedGeometry, PreparedId, Rejection,
18    Section4, Session, SimConfig, SpawnBundle, ViewId, ViewSpec,
19};
20
21#[cfg(test)]
22#[global_allocator]
23static COUNTING_ALLOCATOR: loam_time::alloc::CountingAllocator<std::alloc::System> =
24    loam_time::alloc::CountingAllocator::new(std::alloc::System);
25
26mod camera;
27mod catalog;
28mod color;
29mod console;
30mod consts;
31mod display;
32mod gimbal;
33mod guides;
34mod hud;
35mod mode;
36mod projection;
37mod row;
38mod scene;
39mod strip;
40mod toy;
41mod ui;
42
43use catalog::ShapeEntry;
44use color::{ColorMode, Shades};
45use consts::{BODY_SIZE, BODY_X_SPACING, BODY_Y, W_SCRUB_RATE};
46use display::{Display, Surface};
47use gimbal::Gimbal;
48use mode::{Mode, Spin};
49use projection::Family;
50use strip::{Cell, Strip};
51
52const SPIN: ActionId = ActionId(0);
53const SLICE_UP: ActionId = ActionId(1);
54const SLICE_DOWN: ActionId = ActionId(2);
55const NEXT_MODE: ActionId = ActionId(3);
56const RESET: ActionId = ActionId(4);
57const GIMBAL: ActionId = ActionId(5);
58const STRIP: ActionId = ActionId(6);
59const CONTROLS: ActionId = ActionId(7);
60const PLANE: [ActionId; 6] = [
61    ActionId(10),
62    ActionId(11),
63    ActionId(12),
64    ActionId(13),
65    ActionId(14),
66    ActionId(15),
67];
68
69const SECTION_COLOR: [f32; 4] = [1.0, 0.85, 0.35, 1.0];
70const SECTION_WIDTH_PX: f32 = 2.0;
71const EDGE_WIDTH_PX: f32 = display::DEFAULT_WIREFRAME_WIDTH_PX;
72const HEADLESS_STEPS: u32 = 8;
73const HEADLESS_FRAME: (u32, u32) = (1280, 720);
74const CAMERA_DISTANCE: f32 = 8.0;
75const DOMAIN_NAME: &str = "r4";
76
77#[derive(Clone, Copy)]
78pub(crate) struct Slot {
79    pub(crate) index: usize,
80    pub(crate) entry: ShapeEntry,
81    pub(crate) rest: Vec4,
82}
83
84#[derive(Clone, Copy)]
85pub(crate) struct Wall;
86
87#[derive(Clone, Copy)]
88pub(crate) struct Toy {
89    pub(crate) rest_anchor: Vec4,
90    pub(crate) rest_time: f32,
91}
92
93#[derive(Clone, Copy)]
94pub(crate) struct HiddenSlot {
95    pub(crate) slot: Slot,
96    pub(crate) instance: Option<Instance>,
97}
98
99#[derive(Clone, Copy)]
100pub(crate) struct Control {
101    orbit: Orbit,
102    gimbal: Gimbal,
103    latest: Option<Pointer>,
104    center: glam::Vec3,
105    camera_focus: Option<(bool, usize, Mode)>,
106    filmstrip_return: Option<Orbit>,
107}
108
109impl Default for Control {
110    fn default() -> Self {
111        let mut orbit = Orbit::around([0.0; 3], CAMERA_DISTANCE);
112        orbit.pitch = -0.25;
113        Self {
114            orbit,
115            gimbal: Gimbal::default(),
116            latest: None,
117            center: glam::Vec3::ZERO,
118            camera_focus: None,
119            filmstrip_return: None,
120        }
121    }
122}
123
124#[derive(Clone, Copy)]
125pub(crate) struct Card {
126    geometry: Option<PreparedId>,
127    toy_geometry: Option<PreparedId>,
128    material: MaterialId,
129    cut: MaterialId,
130    shades: Option<Shades>,
131}
132
133impl Card {
134    fn dressed(&self, geometry: PreparedId) -> Instance {
135        Instance::new(geometry, self.material).sectioned(self.cut)
136    }
137
138    pub(crate) fn body(&self) -> Option<Instance> {
139        self.geometry.map(|geometry| self.dressed(geometry))
140    }
141
142    pub(crate) fn toy(&self) -> Option<Instance> {
143        self.toy_geometry.map(|geometry| self.dressed(geometry))
144    }
145}
146
147#[derive(Clone, Default)]
148pub(crate) struct Catalog {
149    cards: Arc<[Card]>,
150}
151
152loam::runtime::stores! {
153    #[derive(Default)]
154    pub struct Playground {
155        slots: Store<Slot>,
156        walls: Store<Wall>,
157        toys: Store<Toy>,
158        hidden: Store<HiddenSlot>,
159        mode: Value<Mode>,
160        spin: Value<Spin>,
161        active: Value<usize>,
162        slice: Value<f32>,
163        projection: Value<Family>,
164        gimbal: Value<bool>,
165        hud: Value<bool>,
166        color: Value<ColorMode>,
167        strip: Value<Strip>,
168        environment: Value<loam::app::environment::Environment>,
169        display: Value<Display>,
170        time: Value<f32>,
171        angles: Value<[f32; 6]>,
172        controls: Value<bool>,
173        formula: Value<bool>,
174        labels: Value<bool>,
175        guides: Value<bool>,
176        toybox_debug: Value<bool>,
177        rope: Value<bool>,
178        camera: Value<camera::Camera>,
179        control: Value<Control>,
180        catalog: Value<Catalog>,
181    }
182}
183
184#[derive(Clone, Copy)]
185pub(crate) enum Action {
186    Mode(Mode),
187    Active(usize),
188    Slice(f32),
189    ExactSlice(f32),
190    Plane(usize),
191    Gimbal(Option<bool>),
192    Running(Option<bool>),
193    Projection(Family),
194    NextProjection,
195    Color(ColorMode),
196    NextColor,
197    Hud(Option<bool>),
198    Shape(usize, usize),
199    AddShape(usize),
200    RemoveShape(usize),
201    ReorderShape { from: usize, to: usize },
202    Controls(Option<bool>),
203    Formula(Option<bool>),
204    Labels(Option<bool>),
205    Guides(Option<bool>),
206    ToyboxDebug(Option<bool>),
207    Rope(Option<bool>),
208    Strip(Strip),
209    Rate(f32),
210    Display(Display),
211    Reset,
212    Time(f32),
213    PlaneAngle(usize, f32),
214    Throw(Entity, [f32; 3]),
215}
216
217pub(crate) struct Boot {
218    pub(crate) session: Session<Playground>,
219    pub(crate) domain: DomainHandle<EuclideanR4>,
220}
221
222fn rest_of(index: usize, len: usize) -> Vec4 {
223    let center = (len.max(1) - 1) as f32 * 0.5;
224    Vec4::new((index as f32 - center) * BODY_X_SPACING, BODY_Y, 0.0, 0.0)
225}
226
227pub(crate) fn boot(row: &[ShapeEntry]) -> Result<Boot, HostError> {
228    let mut session = Session::new(
229        Playground {
230            slots: loam::runtime::Store::tracked(LogCapacity::default()),
231            ..Playground::default()
232        },
233        SimConfig::default(),
234    );
235    let domain = session.register_domain(
236        DomainBuilder::new(DOMAIN_NAME, EuclideanR4)
237            .tracked(LogCapacity::default())
238            .physics(toy::physics_config())
239            .map_err(|error| HostError::Setup(Rejection::Edit(error)))?,
240    );
241    let root = session.views().root();
242
243    let layers = session.dispatch(|d| -> Result<Layers, Rejection> {
244        let cut = d.add_material(Material::lines(SECTION_COLOR, SECTION_WIDTH_PX));
245        let cards: Arc<[Card]> = prepare_catalog(d, cut).into();
246        for (index, entry) in row.iter().enumerate() {
247            let rest = rest_of(index, row.len());
248            let mut bundle = SpawnBundle::new().at(domain, Pose::at(rest)).row(Slot {
249                index,
250                entry: *entry,
251                rest,
252            });
253            if let Some(instance) = card_of(entry).and_then(|card| cards[card].body()) {
254                bundle = bundle.instance(instance);
255            }
256            d.spawn(bundle)?;
257        }
258        let eye = d.spawn(SpawnBundle::new().at(domain, Pose::at(Vec4::ZERO)))?;
259        let r4 = d.domains.typed(domain)?;
260        let section = r4.add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))?;
261        let projection = r4.add_view(ViewSpec::new(root, eye, Family::default().mapping(0.0)))?;
262        d.app.catalog.set(Catalog { cards });
263        Ok(Layers {
264            section,
265            projection,
266        })
267    })?;
268    session.views_mut().root_mut().eye =
269        Eye::looking_at([0.0, 3.0, 9.0], [0.0, BODY_Y, 0.0], [0.0, 1.0, 0.0]);
270    session.app.controls.set(true);
271    session.app.guides.set(true);
272
273    let cards = session.app.catalog.get().cards.clone();
274    install_systems(&mut session, domain, layers, cards);
275    session.set_initial()?;
276    Ok(Boot { session, domain })
277}
278
279#[derive(Clone, Copy)]
280struct Layers {
281    section: ViewId,
282    projection: ViewId,
283}
284
285fn card_of(entry: &ShapeEntry) -> Option<usize> {
286    catalog::SHAPE_CATALOG.iter().position(|held| held == entry)
287}
288
289fn prepare_catalog(dispatch: &mut Dispatch<'_, Playground>, cut: MaterialId) -> Vec<Card> {
290    catalog::SHAPE_CATALOG
291        .iter()
292        .map(|entry| {
293            let [r, g, b] = entry.body_color;
294            let material = dispatch.add_material(Material::lines([r, g, b, 1.0], EDGE_WIDTH_PX));
295            let polytope = entry.shape.polytope4();
296            Card {
297                geometry: polytope.map(|polytope| {
298                    dispatch.prepare(PreparedGeometry::Polytope4 {
299                        polytope,
300                        scale: BODY_SIZE,
301                    })
302                }),
303                toy_geometry: polytope.map(|polytope| {
304                    dispatch.prepare(PreparedGeometry::Polytope4 {
305                        polytope,
306                        scale: toy::BODY_SIZE,
307                    })
308                }),
309                material,
310                cut,
311                shades: polytope.map(|polytope| {
312                    let topology = polytope.topology();
313                    Shades {
314                        gradient: dispatch.add_palette(color::vertex_gradient_colors(topology)),
315                        unique: dispatch.add_palette(color::unique_edge_colors(topology.edges)),
316                        extent: color::w_extent(topology, BODY_SIZE),
317                    }
318                }),
319            }
320        })
321        .collect()
322}
323
324fn install_systems(
325    session: &mut Session<Playground>,
326    domain: DomainHandle<EuclideanR4>,
327    layers: Layers,
328    cards: Arc<[Card]>,
329) {
330    session.system(
331        Phase::Dispatch,
332        "controls",
333        move |mut ctx: Ctx<'_, Playground>| {
334            control_camera(&mut ctx, domain);
335            control_primary(&mut ctx, domain);
336            if ctx.input.pressed(SPIN) && *ctx.app.mode.get() == Mode::Rotate {
337                let running = ctx.app.spin.get().running;
338                ctx.commands.app(Action::Running(Some(!running)));
339            }
340            if ctx.input.pressed(NEXT_MODE) {
341                let next = match *ctx.app.mode.get() {
342                    Mode::Rotate => Mode::Toybox,
343                    Mode::Toybox => Mode::Rotate,
344                };
345                ctx.commands.app(Action::Mode(next));
346            }
347            if ctx.input.pressed(RESET) {
348                ctx.commands.app(Action::Reset);
349            }
350            if ctx.input.pressed(GIMBAL) {
351                ctx.commands.app(Action::Gimbal(None));
352            }
353            if ctx.input.pressed(CONTROLS) {
354                ctx.commands.app(Action::Controls(None));
355            }
356            if ctx.input.pressed(STRIP) {
357                let mut strip = *ctx.app.strip.get();
358                strip.on = !strip.on;
359                ctx.commands.app(Action::Strip(strip));
360            }
361            for (index, action) in PLANE.into_iter().enumerate() {
362                if ctx.input.pressed(action) {
363                    ctx.commands.app(Action::Plane(index));
364                }
365            }
366            Ok(())
367        },
368    );
369
370    session.system(
371        Phase::Dispatch,
372        "grab hold",
373        move |ctx: Ctx<'_, Playground>| -> Result<(), DomainError> {
374            let hold = if *ctx.app.rope.get() {
375                GrabHold::Anchor
376            } else {
377                GrabHold::Center
378            };
379            if let Some(physics) = ctx.domains.typed(domain)?.physics_mut() {
380                physics.set_grab_hold(hold);
381            }
382            Ok(())
383        },
384    );
385
386    let mut applied = None;
387    session.system(
388        Phase::Publication,
389        "slice view",
390        move |ctx: Ctx<'_, Playground>| -> Result<(), DomainError> {
391            let w = *ctx.app.slice.get();
392            let revision = ctx.app.slice.version();
393            if Some(revision) == applied {
394                return Ok(());
395            }
396            let r4 = ctx.domains.typed(domain)?;
397            let spec = r4
398                .view_mut(layers.section)
399                .ok_or(DomainError::Unsupported("section view"))?;
400            spec.set_mapping(Section4 { w });
401            applied = Some(revision);
402            Ok(())
403        },
404    );
405
406    let mut styled = None;
407    let mut styled_slots = loam::runtime::Cursor::default();
408    session.system(
409        Phase::Publication,
410        "shading",
411        move |ctx: Ctx<'_, Playground>| -> Result<(), DomainError> {
412            let app = &mut *ctx.app;
413            let revision = (
414                app.color.version(),
415                app.strip.version(),
416                app.display.version(),
417                app.active.version(),
418            );
419            let slots_changed = app.slots.changed_since(&mut styled_slots);
420            if !slots_changed && styled == Some(revision) {
421                return Ok(());
422            }
423            let mode = *app.color.get();
424            let strip = app.strip.get().on;
425            let display = *app.display.get();
426            let active = *app.active.get();
427            let subject = if display.single {
428                app.slots
429                    .iter()
430                    .find(|(_, slot)| slot.index == active)
431                    .map(|(entity, _)| entity)
432            } else {
433                None
434            };
435            let r4 = ctx.domains.typed(domain)?;
436            r4.set_view_subject(layers.section, subject)?;
437            r4.set_view_subject(layers.projection, subject)?;
438            let spec = r4
439                .view_mut(layers.section)
440                .ok_or(DomainError::Unsupported("section view"))?;
441            spec.enabled = !strip;
442            spec.edges = false;
443            spec.section_edges = display.wireframe && display.section_perimeter;
444            spec.section_faces = display.surface == Surface::Raster;
445            let spec = r4
446                .view_mut(layers.projection)
447                .ok_or(DomainError::Unsupported("projection view"))?;
448            spec.enabled = display.wireframe && !strip;
449            spec.section_edges = false;
450            spec.section_faces = false;
451            for (entity, slot) in app.slots.iter() {
452                let Some(card) = card_of(&slot.entry).map(|index| cards[index]) else {
453                    continue;
454                };
455                let Some(shades) = card.shades else {
456                    continue;
457                };
458                if let Some(instance) = r4.instance_mut(entity) {
459                    instance.shading = shades.of(mode);
460                    instance.section = (!strip).then_some(card.cut);
461                    instance.line_width_px = Some(display.wireframe_width_px);
462                    instance.line_opacity = Some(display.wireframe_opacity);
463                }
464            }
465            styled = Some(revision);
466            app.slots.catch_up(&mut styled_slots);
467            Ok(())
468        },
469    );
470
471    let mut shown = None;
472    session.system(
473        Phase::Publication,
474        "projection view",
475        move |ctx: Ctx<'_, Playground>| -> Result<(), DomainError> {
476            let family = *ctx.app.projection.get();
477            let slice = *ctx.app.slice.get();
478            let revision = (ctx.app.projection.version(), ctx.app.slice.version());
479            if shown == Some(revision) {
480                return Ok(());
481            }
482            let r4 = ctx.domains.typed(domain)?;
483            let spec = r4
484                .view_mut(layers.projection)
485                .ok_or(DomainError::Unsupported("projection view"))?;
486            spec.set_mapping(family.mapping(slice));
487            shown = Some(revision);
488            Ok(())
489        },
490    );
491
492    session.system(
493        Phase::Simulation,
494        "slice scrub",
495        |ctx: Ctx<'_, Playground>| {
496            if ctx.app.camera.get().mode != camera::CameraMode::Orbit {
497                return Ok(());
498            }
499            let scrub =
500                f32::from(ctx.input.is_held(SLICE_UP)) - f32::from(ctx.input.is_held(SLICE_DOWN));
501            if scrub == 0.0 {
502                return Ok(());
503            }
504            let next = *ctx.app.slice.get() + scrub * W_SCRUB_RATE * ctx.step.dt;
505            let reach = slice_reach(*ctx.app.mode.get());
506            ctx.app.slice.set(next.clamp(-reach, reach));
507            Ok(())
508        },
509    );
510    session.system(
511        Phase::Simulation,
512        "spin",
513        move |ctx: Ctx<'_, Playground>| -> Result<(), DomainError> {
514            if !ctx.app.spin.get().running || *ctx.app.mode.get() == Mode::Toybox {
515                return Ok(());
516            }
517            let time = *ctx.app.time.get() + ctx.step.dt * ctx.app.spin.get().rate;
518            display::seek(ctx.app, ctx.domains, domain, time)?;
519            Ok(())
520        },
521    );
522    session.system(
523        Phase::Simulation,
524        "free camera",
525        |ctx: Ctx<'_, Playground>| {
526            if ctx.app.camera.get().mode == camera::CameraMode::Freecam && ctx.input.cursor_locked {
527                let axes = [
528                    f32::from(ctx.input.is_held(camera::RIGHT))
529                        - f32::from(ctx.input.is_held(camera::LEFT)),
530                    f32::from(ctx.input.is_held(SLICE_UP))
531                        - f32::from(ctx.input.is_held(SLICE_DOWN)),
532                    f32::from(ctx.input.is_held(camera::FORWARD))
533                        - f32::from(ctx.input.is_held(camera::BACKWARD)),
534                ];
535                let camera = ctx.app.camera.get_mut();
536                camera.free.travel(axes, camera.speed * ctx.step.dt);
537                look(ctx.views, camera.free.eye);
538            }
539            Ok(())
540        },
541    );
542    session.system(
543        Phase::Simulation,
544        "toy settle",
545        move |ctx: Ctx<'_, Playground>| -> Result<(), DomainError> {
546            toy::settle(ctx.app, ctx.domains, domain, ctx.step)
547        },
548    );
549}
550
551impl AppCommand<Playground> for Action {
552    fn name(&self) -> &'static str {
553        match self {
554            Self::Mode(mode) => mode.name(),
555            Self::Active(_) => "active",
556            Self::Slice(_) | Self::ExactSlice(_) => "slice",
557            Self::Plane(_) => "plane",
558            Self::Gimbal(None) => "gimbal",
559            Self::Gimbal(Some(_)) => "handles",
560            Self::Running(_) => "spin",
561            Self::Projection(family) => family.name(),
562            Self::NextProjection => "wireframe perspective",
563            Self::Color(mode) => mode.name(),
564            Self::NextColor => "wireframe color",
565            Self::Hud(_) => "hud",
566            Self::Shape(_, card) => catalog::SHAPE_CATALOG
567                .get(*card)
568                .map_or("shape", |entry| entry.label),
569            Self::AddShape(_) => "add shape",
570            Self::RemoveShape(_) => "remove shape",
571            Self::ReorderShape { .. } => "reorder shape",
572            Self::Controls(_) => "controls",
573            Self::Formula(_) => "formula",
574            Self::Labels(_) => "labels",
575            Self::Guides(_) => "guides",
576            Self::ToyboxDebug(_) => "toybox debug",
577            Self::Rope(_) => "rope carry",
578            Self::Strip(_) => "strip",
579            Self::Rate(_) => "rate",
580            Self::Display(_) => "display",
581            Self::Reset => "reset",
582            Self::Time(_) => "time",
583            Self::PlaneAngle(_, _) => "plane angle",
584            Self::Throw(_, _) => "throw toy",
585        }
586    }
587
588    fn apply(&mut self, dispatch: &mut Dispatch<'_, Playground>) -> Result<Outcome, Rejection> {
589        let domain = dispatch.domains.named::<EuclideanR4>(DOMAIN_NAME)?;
590        let Catalog { cards } = dispatch.app.catalog.get().clone();
591        match *self {
592            Action::Mode(mode) => mode::set_mode(dispatch, domain, &cards, mode)?,
593            Action::Active(slot) => {
594                if slot >= dispatch.app.slots.len() {
595                    return Err(Rejection::Unsupported("no such slot"));
596                }
597                dispatch.app.active.set(slot);
598            }
599            Action::ReorderShape { from, to } => {
600                row::reorder_shape(dispatch, domain, from, to)?;
601            }
602            Action::Slice(w) => {
603                if !w.is_finite() {
604                    return Err(Rejection::Unsupported("slice is not finite"));
605                }
606                let reach = slice_reach(*dispatch.app.mode.get());
607                dispatch.app.slice.set(w.clamp(-reach, reach));
608            }
609            Action::ExactSlice(w) => {
610                if !w.is_finite() {
611                    return Err(Rejection::Unsupported("slice is not finite"));
612                }
613                let reach = slice_reach(*dispatch.app.mode.get());
614                if !(-reach..=reach).contains(&w) {
615                    return Err(Rejection::Unsupported(
616                        "slice is outside the mode's w range",
617                    ));
618                }
619                dispatch.app.slice.set(w);
620            }
621            Action::Plane(plane) => mode::toggle_plane(dispatch, plane)?,
622            Action::Gimbal(setting) => {
623                let shown = *dispatch.app.gimbal.get();
624                dispatch.app.gimbal.set(setting.unwrap_or(!shown));
625            }
626            Action::Running(setting) => {
627                if *dispatch.app.mode.get() == Mode::Toybox {
628                    return Err(Rejection::Unsupported("rotation belongs to Rotate"));
629                }
630                let running = dispatch.app.spin.get_mut();
631                running.running = setting.unwrap_or(!running.running);
632            }
633            Action::Projection(family) => {
634                dispatch.app.projection.set(family);
635                if family == Family::Stereographic {
636                    dispatch.app.display.get_mut().wireframe = true;
637                }
638            }
639            Action::NextProjection => {
640                let held = *dispatch.app.projection.get();
641                let at = Family::ALL
642                    .iter()
643                    .position(|family| *family == held)
644                    .unwrap_or(0);
645                let family = Family::ALL[(at + 1) % Family::ALL.len()];
646                dispatch.app.projection.set(family);
647                if family == Family::Stereographic {
648                    dispatch.app.display.get_mut().wireframe = true;
649                }
650            }
651            Action::Color(mode) => dispatch.app.color.set(mode),
652            Action::NextColor => {
653                let held = *dispatch.app.color.get();
654                let at = ColorMode::ALL
655                    .iter()
656                    .position(|mode| *mode == held)
657                    .unwrap_or(0);
658                dispatch
659                    .app
660                    .color
661                    .set(ColorMode::ALL[(at + 1) % ColorMode::ALL.len()]);
662            }
663            Action::Hud(setting) => {
664                let shown = *dispatch.app.hud.get();
665                dispatch.app.hud.set(setting.unwrap_or(!shown));
666            }
667            Action::Strip(strip) => mode::set_strip(dispatch.app, strip)?,
668            Action::Rate(rate) => {
669                if !rate.is_finite() {
670                    return Err(Rejection::Unsupported("the rate is not finite"));
671                }
672                dispatch.app.spin.get_mut().rate = rate.clamp(0.0, consts::MAX_RATE);
673            }
674            Action::Display(display) => display::set(dispatch.app, display)?,
675            Action::Reset => mode::reset(dispatch, domain, &cards)?,
676            Action::Controls(setting) => {
677                let visible = *dispatch.app.controls.get();
678                dispatch.app.controls.set(setting.unwrap_or(!visible));
679            }
680            Action::Formula(setting) => {
681                let visible = *dispatch.app.formula.get();
682                dispatch.app.formula.set(setting.unwrap_or(!visible));
683            }
684            Action::Labels(setting) => {
685                let visible = *dispatch.app.labels.get();
686                dispatch.app.labels.set(setting.unwrap_or(!visible));
687            }
688            Action::Guides(setting) => {
689                let visible = *dispatch.app.guides.get();
690                dispatch.app.guides.set(setting.unwrap_or(!visible));
691            }
692            Action::Rope(setting) => {
693                let rope = *dispatch.app.rope.get();
694                dispatch.app.rope.set(setting.unwrap_or(!rope));
695            }
696            Action::ToyboxDebug(setting) => {
697                let visible = *dispatch.app.toybox_debug.get();
698                dispatch.app.toybox_debug.set(setting.unwrap_or(!visible));
699            }
700            Action::Time(time) => {
701                display::seek(dispatch.app, dispatch.domains, domain, time)
702                    .map_err(Rejection::Domain)?;
703            }
704            Action::PlaneAngle(plane, angle) => {
705                display::set_plane_angle(dispatch, domain, plane, angle)?;
706            }
707            Action::Shape(slot, card) => {
708                let Some(entry) = catalog::SHAPE_CATALOG.get(card) else {
709                    return Ok(Outcome::Done);
710                };
711                mode::set_shape(dispatch, domain, slot, *entry, cards[card])?;
712            }
713            Action::AddShape(card) => {
714                let Some(entry) = catalog::SHAPE_CATALOG.get(card) else {
715                    return Ok(Outcome::Done);
716                };
717                row::add_shape(dispatch, domain, *entry, cards[card])?;
718            }
719            Action::RemoveShape(slot) => row::remove_shape(dispatch, domain, slot)?,
720            Action::Throw(entity, pointer) => {
721                mode::throw_toy(dispatch, domain, entity, pointer)?;
722            }
723        }
724        Ok(Outcome::Done)
725    }
726}
727
728pub(crate) fn bindings() -> Bindings {
729    let mut bound = Bindings::new()
730        .key(Key::Space, SPIN)
731        .key(Key::Letter('t'), SPIN)
732        .key(Key::Letter('m'), NEXT_MODE)
733        .key(Key::Letter('r'), RESET)
734        .key(Key::Letter('g'), GIMBAL)
735        .key(Key::Letter('f'), STRIP)
736        .key(Key::Letter('h'), CONTROLS)
737        .key(Key::Letter('d'), camera::RIGHT)
738        .key(Key::Letter('a'), camera::LEFT)
739        .key(Key::Letter('w'), camera::FORWARD)
740        .key(Key::Letter('s'), camera::BACKWARD)
741        .key(Key::Letter('e'), SLICE_UP)
742        .key(Key::Letter('q'), SLICE_DOWN);
743    for (index, action) in PLANE.into_iter().enumerate() {
744        bound = bound.key(Key::Digit(index as u8 + 1), action);
745    }
746    bound
747}
748
749pub(crate) struct Frame {
750    sky: SkyGroundPass,
751    hyperslice: HyperslicePass,
752    rings: LinePass,
753    guides: LinePass,
754    grab_point: PointPass,
755    hud: loam::text::TextPass,
756}
757
758impl Frame {
759    pub(crate) fn new() -> Self {
760        Self {
761            sky: SkyGroundPass::new(scene::ground(true)),
762            hyperslice: HyperslicePass::new(scene::shader_source()),
763            rings: LinePass::new("gimbal"),
764            guides: LinePass::new("toybox-guides"),
765            grab_point: PointPass::new("grab-point"),
766            hud: hud::pass(),
767        }
768    }
769
770    fn passes(&self) -> Vec<Box<dyn FramePass>> {
771        vec![
772            Box::new(self.sky.clone()),
773            Box::new(self.hyperslice.clone()),
774            Box::new(self.rings.clone()),
775            Box::new(self.guides.clone()),
776            Box::new(self.grab_point.clone()),
777            Box::new(self.hud.clone()),
778        ]
779    }
780}
781
782struct Scratch {
783    center: glam::Vec3,
784    slots: Vec<(Entity, ShapeEntry, f32)>,
785    bodies: Vec<BodyUniform>,
786    cells: Vec<Cell>,
787    strip: Vec<(loam::render::Viewport, f32, BodyUniform)>,
788    subject: loam::math::Rotor4,
789    anchors: Vec<(usize, &'static str, glam::Vec3)>,
790    depths: Vec<toy::DepthBand>,
791}
792
793impl Scratch {
794    fn new() -> Self {
795        Self {
796            center: glam::Vec3::ZERO,
797            slots: Vec::new(),
798            bodies: Vec::new(),
799            cells: Vec::new(),
800            strip: Vec::new(),
801            subject: loam::math::Rotor4::IDENTITY,
802            anchors: Vec::new(),
803            depths: Vec::with_capacity(5),
804        }
805    }
806}
807
808fn collect(
809    session: &Session<Playground>,
810    domain: DomainHandle<EuclideanR4>,
811    alpha: f32,
812    scratch: &mut Scratch,
813) {
814    let active = *session.app.active.get();
815    let display = *session.app.display.get();
816    scratch.slots.clear();
817    scratch.slots.extend(
818        session
819            .app
820            .slots
821            .iter()
822            .filter(|(_, slot)| !display.single || slot.index == active)
823            .map(|(entity, slot)| {
824                let size = if session.app.toys.get(entity).is_some() {
825                    toy::BODY_SIZE
826                } else {
827                    BODY_SIZE
828                };
829                (entity, slot.entry, size)
830            }),
831    );
832    scratch.anchors.clear();
833    scratch.anchors.extend(
834        session
835            .app
836            .slots
837            .iter()
838            .filter(|(_, slot)| !display.single || slot.index == active)
839            .map(|(_, slot)| (slot.index, slot.entry.label, glam::Vec3::ZERO)),
840    );
841    scratch.bodies.clear();
842    scratch.subject = loam::math::Rotor4::IDENTITY;
843    let mut sum = glam::Vec3::ZERO;
844    let Ok(r4) = session.domains().read(domain) else {
845        return;
846    };
847    for (at, (entity, entry, size)) in scratch.slots.iter().enumerate() {
848        let Some(pose) = r4.drawn_pose(*entity, alpha) else {
849            continue;
850        };
851        if scratch.anchors.get(at).is_some_and(|held| held.0 == active) {
852            scratch.subject = pose.frame;
853        }
854        let center = pose.point.truncate();
855        sum += center;
856        if let Some(anchor) = scratch.anchors.get_mut(at) {
857            anchor.2 = center;
858        }
859        if display.surface == Surface::Sdf
860            || (display.surface == Surface::Raster && entry.shape.polytope4().is_none())
861        {
862            scratch.bodies.push(scene::body_of(entry, &pose, *size));
863        }
864    }
865    scratch.center = sum / scratch.slots.len().max(1) as f32;
866}
867
868fn main() -> Result<(), HostError> {
869    launch_or_headless(interactive, headless)
870}
871
872fn interactive(args: Args) -> Result<(Session<Playground>, SessionApp<Playground>), HostError> {
873    let row = catalog::parse_row(&args).map_err(|error| HostError::Host(format!("{error:#}")))?;
874    let booted = boot(&row)?;
875    let frame = Frame::new();
876    let config = HostConfig::new("polytope playground", bindings());
877
878    let sky = frame.sky.clone();
879    let hyperslice = frame.hyperslice.clone();
880    let rings = frame.rings.clone();
881    let guide_pass = frame.guides.clone();
882    let grab_point = frame.grab_point.clone();
883    let mut guides = guides::Guides::default();
884    let readout = frame.hud.clone();
885    let mut lines = String::new();
886    let mut gimbal_renderer = gimbal::GimbalRenderer::default();
887    let domain = booted.domain;
888    let mut scratch = Scratch::new();
889    let mut panel = ui::Panel::default();
890    let mut app = SessionApp::with_args(config, args).recover_on_fault(RESET);
891    for pass in frame.passes() {
892        app = app.pass(pass);
893    }
894    app = console::install(app).on_frame(move |hook: &mut FrameHook<'_, Playground>| {
895        let camera = *hook.session.app.camera.get();
896        hook.capture_cursor(
897            camera.mode == camera::CameraMode::Freecam
898                && !hook
899                    .ui
900                    .is_some_and(|context| context.wants_keyboard_input()),
901            camera.cursor_policy,
902        );
903        collect(hook.session, domain, hook.alpha, &mut scratch);
904        let eye = hook
905            .session
906            .views()
907            .get(hook.session.views().root())
908            .map_or(Eye::default(), |root| root.eye);
909        let strip = *hook.session.app.strip.get();
910
911        let slice = *hook.session.app.slice.get();
912        let environment = *hook.session.app.environment.get();
913        let floor = environment.floor_visible;
914        sky.publish(
915            &eye,
916            environment.sky,
917            environment.ground(consts::FLOOR_Y, floor),
918        );
919        hyperslice.set_enabled(strip.on || !scratch.bodies.is_empty());
920        hyperslice.publish(
921            scene::uniforms(&eye, environment.sky, slice, floor),
922            &scratch.bodies,
923        );
924        fill_strip(
925            &strip,
926            turn_of(hook.session),
927            hook.size,
928            slice,
929            &mut scratch,
930        );
931        hyperslice.publish_strip(&scratch.strip);
932        rings.publish(
933            &eye,
934            gimbal_renderer.rings(&hook.session.app.control.get().gimbal, scratch.center),
935        );
936        guides.update(hook.session, domain, hook.alpha);
937        guide_pass.publish(&eye, &guides.lines);
938        grab_point.publish(&eye, &guides.points);
939        let shown = *hook.session.app.hud.get();
940        let seat = hook.ui.map_or(hud::Seat::default(), |context| {
941            hud::Seat::in_panel(context.available_rect(), context.pixels_per_point())
942        });
943        hud::publish(
944            &readout,
945            shown.then(|| readout_of(hook.session)).as_ref(),
946            seat,
947            &mut lines,
948        );
949        if let Some(context) = hook.ui {
950            toy::fill_depth_bands(hook.session, domain, hook.alpha, &mut scratch.depths);
951            ui::draw(
952                context,
953                hook.session,
954                &mut panel,
955                hook.sender,
956                scratch.subject,
957                &scratch.depths,
958            );
959            if *hook.session.app.labels.get() && !strip.on {
960                ui::callouts(context, hook.session, &scratch.anchors);
961            }
962            ui::strip_labels(context, hook.session, &scratch.cells);
963        }
964    });
965    Ok((booted.session, app))
966}
967
968fn focus_camera(
969    orbit: &mut Orbit,
970    camera_focus: &mut Option<(bool, usize, Mode)>,
971    filmstrip_return: &mut Option<Orbit>,
972    strip: Strip,
973    focus: (bool, usize, Mode),
974    center: glam::Vec3,
975) {
976    if strip.on {
977        if filmstrip_return.is_none() {
978            let mut centered = Orbit::around([0.0; 3], orbit.distance);
979            centered.yaw = orbit.yaw;
980            centered.pitch = orbit.pitch;
981            *filmstrip_return = Some(std::mem::replace(orbit, centered));
982            *camera_focus = Some(focus);
983        }
984        orbit.target[1] = if strip.w && strip.t { BODY_Y } else { 0.0 };
985        return;
986    }
987    if let Some(returned) = filmstrip_return.take() {
988        *orbit = returned;
989    }
990    if *camera_focus == Some(focus) {
991        return;
992    }
993    let reset =
994        camera_focus.is_some_and(|previous| previous.2 != focus.2 || previous.0 && !focus.0);
995    let target = [center.x, 0.0, center.z];
996    if reset {
997        *orbit = Orbit::around(target, CAMERA_DISTANCE);
998        orbit.pitch = -0.25;
999    } else {
1000        orbit.target = target;
1001    }
1002    *camera_focus = Some(focus);
1003}
1004
1005fn slice_reach(mode: Mode) -> f32 {
1006    match mode {
1007        Mode::Rotate => consts::W_RANGE,
1008        Mode::Toybox => toy::ARENA_HALF,
1009    }
1010}
1011
1012fn readout_of(session: &Session<Playground>) -> hud::Readout {
1013    hud::Readout {
1014        slice: *session.app.slice.get(),
1015        rate: session.app.spin.get().rate,
1016        bodies: session.app.slots.len(),
1017        planes: session.app.spin.get().planes,
1018        toybox: *session.app.mode.get() == Mode::Toybox,
1019    }
1020}
1021
1022fn turn_of(session: &Session<Playground>) -> Bivector4 {
1023    match *session.app.mode.get() {
1024        Mode::Rotate => session.app.spin.get().omega(),
1025        Mode::Toybox => Bivector4::ZERO,
1026    }
1027}
1028
1029fn fill_strip(
1030    strip: &Strip,
1031    omega: Bivector4,
1032    frame: (u32, u32),
1033    slice: f32,
1034    scratch: &mut Scratch,
1035) {
1036    scratch.strip.clear();
1037    if !strip.on {
1038        scratch.cells.clear();
1039        return;
1040    }
1041    strip.cells([frame.0, frame.1], slice, BODY_SIZE, &mut scratch.cells);
1042    let entry = catalog::SHAPE_CATALOG[strip.subject()];
1043    for cell in &scratch.cells {
1044        let rotor = ((omega * cell.t).exp() * scratch.subject).normalize();
1045        scratch.strip.push((
1046            cell.viewport,
1047            cell.w,
1048            BodyUniform::polytope_with_rotor(
1049                [0.0, BODY_Y, 0.0, 0.0],
1050                entry.shape.shape_id(),
1051                BODY_SIZE,
1052                rotor,
1053                entry.body_color,
1054            ),
1055        ));
1056    }
1057}
1058
1059fn scene_center(
1060    app: &Playground,
1061    domains: &Domains,
1062    domain: DomainHandle<EuclideanR4>,
1063) -> glam::Vec3 {
1064    let active = *app.active.get();
1065    let single = app.display.get().single;
1066    let Ok(r4) = domains.read(domain) else {
1067        return glam::Vec3::ZERO;
1068    };
1069    let mut sum = glam::Vec3::ZERO;
1070    let mut count = 0;
1071    for (entity, slot) in app.slots.iter() {
1072        if single && slot.index != active {
1073            continue;
1074        }
1075        let Some(pose) = r4.poses().get(entity) else {
1076            continue;
1077        };
1078        sum += pose.point.truncate();
1079        count += 1;
1080    }
1081    sum / count.max(1) as f32
1082}
1083
1084fn control_camera(ctx: &mut Ctx<'_, Playground>, domain: DomainHandle<EuclideanR4>) {
1085    let center = scene_center(ctx.app, ctx.domains, domain);
1086    let focus = (
1087        ctx.app.display.get().single,
1088        *ctx.app.active.get(),
1089        *ctx.app.mode.get(),
1090    );
1091    let strip = *ctx.app.strip.get();
1092    {
1093        let control = ctx.app.control.get_mut();
1094        for pointer in &ctx.input.pointers {
1095            control.latest = Some(*pointer);
1096        }
1097        control.center = center;
1098        focus_camera(
1099            &mut control.orbit,
1100            &mut control.camera_focus,
1101            &mut control.filmstrip_return,
1102            strip,
1103            focus,
1104            center,
1105        );
1106    }
1107    let mode = ctx.app.camera.get().mode;
1108    let eye = match mode {
1109        camera::CameraMode::Orbit => {
1110            let control = ctx.app.control.get_mut();
1111            control.orbit.apply(ctx.input);
1112            control.orbit.eye()
1113        }
1114        camera::CameraMode::Freecam => {
1115            let camera = ctx.app.camera.get_mut();
1116            if ctx.input.cursor_locked {
1117                camera.free.look(ctx.input.look);
1118            }
1119            camera.free.eye
1120        }
1121    };
1122    look(ctx.views, eye);
1123}
1124
1125fn control_primary(ctx: &mut Ctx<'_, Playground>, domain: DomainHandle<EuclideanR4>) {
1126    if ctx.dragging().is_some_and(|drag| {
1127        *ctx.app.mode.get() != Mode::Toybox || !ctx.app.toys.contains(drag.entity)
1128    }) {
1129        ctx.cancel_drag();
1130    }
1131    let (latest, center, mut gimbal) = {
1132        let control = ctx.app.control.get();
1133        (control.latest, control.center, control.gimbal)
1134    };
1135    gimbal.enabled =
1136        *ctx.app.gimbal.get() && *ctx.app.mode.get() == Mode::Rotate && !ctx.app.strip.get().on;
1137    let root = ctx.views.root();
1138    let ray = latest.and_then(|pointer| ctx.views.ray(root, pointer.ndc));
1139    if gimbal.enabled {
1140        gimbal.aim(ray.as_ref(), center);
1141    } else {
1142        gimbal.release();
1143    }
1144    let mut aimed = false;
1145    for index in 0..ctx.input.pointers.len() {
1146        let pointer = ctx.input.pointers[index];
1147        if pointer.button != Some(PointerButton::Primary) {
1148            continue;
1149        }
1150        match pointer.phase {
1151            PointerPhase::Began => {
1152                let ring = gimbal.enabled
1153                    && ctx
1154                        .views
1155                        .ray(root, pointer.ndc)
1156                        .is_some_and(|ray| gimbal.press(&ray, center));
1157                if !ring && *ctx.app.mode.get() == Mode::Toybox && !ctx.app.strip.get().on {
1158                    let _ = ctx.grab(pointer.ndc, pointer.time);
1159                    aimed = true;
1160                }
1161            }
1162            PointerPhase::Moved if gimbal.held() => {
1163                let turn = ctx
1164                    .views
1165                    .ray(root, pointer.ndc)
1166                    .and_then(|ray| gimbal.turn(&ray));
1167                if let Some(rotor) = turn {
1168                    let _ = mode::turn_row(ctx.app, ctx.domains, domain, rotor);
1169                }
1170            }
1171            PointerPhase::Moved if ctx.dragging().is_some() => {
1172                aimed = true;
1173                if ctx.drag(pointer.ndc, pointer.time).is_err() {
1174                    ctx.cancel_drag();
1175                }
1176            }
1177            PointerPhase::Ended if gimbal.held() => gimbal.release(),
1178            PointerPhase::Ended if ctx.dragging().is_some() => {
1179                if let Some(release) = ctx.release_at(pointer.time) {
1180                    ctx.commands
1181                        .app(Action::Throw(release.entity, release.velocity));
1182                }
1183            }
1184            PointerPhase::Cancelled => {
1185                gimbal.release();
1186                ctx.cancel_drag();
1187            }
1188            _ => {}
1189        }
1190    }
1191    if let Some(pointer) = latest.filter(|_| !aimed && ctx.dragging().is_some()) {
1192        let time = ctx.input.time;
1193        if ctx.drag(pointer.ndc, time).is_err() {
1194            ctx.cancel_drag();
1195        }
1196    }
1197    ctx.app.control.get_mut().gimbal = gimbal;
1198}
1199
1200fn report(booted: &mut Boot, config: &HostConfig) -> Result<Vec<String>, HostError> {
1201    let publication = run_headless(&mut booted.session, config, HEADLESS_STEPS, &[])?;
1202    let active = *booted.session.app.active.get();
1203    let entry = booted
1204        .session
1205        .app
1206        .slots
1207        .iter()
1208        .find(|(_, slot)| slot.index == active)
1209        .map(|(_, slot)| slot.entry);
1210    let edges = entry
1211        .and_then(|entry| entry.shape.polytope4())
1212        .map_or(0, |polytope| polytope.edge_count());
1213    Ok(vec![
1214        format!(
1215            "active: {} with {edges} edges",
1216            entry.map_or("none", |entry| entry.label)
1217        ),
1218        format!(
1219            "published segments: {}",
1220            publication
1221                .views
1222                .iter()
1223                .map(|view| view.records.segments().len())
1224                .sum::<usize>()
1225        ),
1226        format!(
1227            "section fills: {} triangles",
1228            publication
1229                .views
1230                .iter()
1231                .map(|view| view.records.triangles().len())
1232                .sum::<usize>()
1233        ),
1234        strip_line(&mut Scratch::new(), *booted.session.app.strip.get()),
1235    ])
1236}
1237
1238fn strip_line(scratch: &mut Scratch, strip: Strip) -> String {
1239    let mut shown = strip;
1240    shown.on = true;
1241    fill_strip(&shown, Bivector4::ZERO, HEADLESS_FRAME, 0.0, scratch);
1242    let (cols, rows, _) = shown.grid();
1243    format!("filmstrip: {} cells, {cols} by {rows}", scratch.strip.len())
1244}
1245
1246fn headless(args: Args) -> Result<(), HostError> {
1247    let row = catalog::parse_row(&args).map_err(|error| HostError::Host(format!("{error:#}")))?;
1248    let mut booted = boot(&row)?;
1249    let config = HostConfig::new("polytope playground", bindings());
1250    for line in report(&mut booted, &config)? {
1251        println!("{line}");
1252    }
1253    Ok(())
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258    use loam::math::Rotor;
1259    use loam::render::raymarch::RaymarchShape;
1260    use loam::runtime::{AppCommand, ChartCommand, ChartPoint, Command, Records};
1261    use loam::shape::polytope::Polytope4;
1262
1263    use super::*;
1264
1265    const CELL24: ShapeEntry = ShapeEntry {
1266        shape: RaymarchShape::Polytope(Polytope4::Cell24),
1267        body_color: [0.95, 0.45, 0.85],
1268        label: "24-cell",
1269        long_name: "icositetrachoron",
1270        category: catalog::Category::RegularPolychoron,
1271    };
1272
1273    const EYE_BACK: f32 = 5.0;
1274
1275    fn one_slot() -> Boot {
1276        let mut booted = boot(&[CELL24]).expect("the session boots");
1277        booted.session.views_mut().root_mut().eye =
1278            Eye::looking_at([0.0, BODY_Y, EYE_BACK], [0.0, BODY_Y, 0.0], [0.0, 1.0, 0.0]);
1279        booted.session.app.spin.get_mut().running = false;
1280        booted
1281    }
1282
1283    pub(crate) fn send(booted: &mut Boot, action: Action) {
1284        booted.session.submit(Command::App(Box::new(action)));
1285    }
1286
1287    fn slot_entity(booted: &Boot) -> Entity {
1288        booted
1289            .session
1290            .app
1291            .slots
1292            .iter()
1293            .map(|(entity, _)| entity)
1294            .next()
1295            .expect("the row has a slot")
1296    }
1297
1298    fn published<R>(
1299        session: &mut Session<Playground>,
1300        records: &mut Records,
1301        read: impl FnOnce(&loam::runtime::Publication) -> R,
1302    ) -> R {
1303        records.publish(session).expect("published");
1304        let publication = records.lend().expect("the buffer is free");
1305        let value = read(&publication);
1306        records.release(publication);
1307        value
1308    }
1309
1310    #[test]
1311    fn a_reset_before_any_action_leaves_every_action_working() {
1312        let mut booted = one_slot();
1313        booted.session.submit(Command::Reset);
1314        booted
1315            .session
1316            .boundary(Input::default())
1317            .expect("the reset applied");
1318        send(&mut booted, Action::Mode(Mode::Toybox));
1319        booted
1320            .session
1321            .boundary(Input::default())
1322            .expect("the boundary ran");
1323
1324        assert_eq!(*booted.session.app.mode.get(), Mode::Toybox);
1325        assert!(
1326            booted.session.results()[0].outcome.is_ok(),
1327            "the first action after a reset was refused: {:?}",
1328            booted.session.results()[0].outcome
1329        );
1330    }
1331
1332    #[test]
1333    fn spin_is_refused_in_toybox_instead_of_reporting_done() {
1334        let mut booted = one_slot();
1335        send(&mut booted, Action::Mode(Mode::Toybox));
1336        booted
1337            .session
1338            .boundary(Input::default())
1339            .expect("the boundary ran");
1340        send(&mut booted, Action::Running(Some(true)));
1341        booted
1342            .session
1343            .boundary(Input::default())
1344            .expect("the boundary ran");
1345        let outcome = &booted.session.results()[0].outcome;
1346        assert!(
1347            matches!(outcome, Err(Rejection::Unsupported(_))),
1348            "spin in Toybox reported {outcome:?}"
1349        );
1350    }
1351
1352    #[test]
1353    fn a_mode_command_lands_in_the_store_at_the_next_boundary() {
1354        let mut booted = one_slot();
1355        send(&mut booted, Action::Mode(Mode::Toybox));
1356        assert_eq!(
1357            *booted.session.app.mode.get(),
1358            Mode::Rotate,
1359            "an intent changed the store before any boundary applied it"
1360        );
1361
1362        booted
1363            .session
1364            .boundary(Input::default())
1365            .expect("the boundary ran");
1366
1367        assert_eq!(*booted.session.app.mode.get(), Mode::Toybox);
1368        assert_eq!(
1369            booted.session.results().len(),
1370            1,
1371            "the boundary applied {} commands, not the one mode change",
1372            booted.session.results().len()
1373        );
1374        assert!(booted.session.results()[0].outcome.is_ok());
1375    }
1376
1377    #[test]
1378    fn wireframe_and_perimeter_toggles_control_separate_edge_layers() {
1379        let mut booted = one_slot();
1380        booted.session.boundary(Input::default()).expect("boundary");
1381        let mut records = Records::default();
1382        for reset in [false, true] {
1383            if reset {
1384                send(&mut booted, Action::Reset);
1385                booted.session.boundary(Input::default()).expect("reset");
1386            }
1387            published(&mut booted.session, &mut records, |publication| {
1388                assert!(publication
1389                    .views
1390                    .iter()
1391                    .all(|view| view.records.segments().is_empty()));
1392                assert!(!publication.views[0].records.triangles().is_empty());
1393            });
1394        }
1395        send(
1396            &mut booted,
1397            Action::Display(Display {
1398                wireframe: true,
1399                ..Display::default()
1400            }),
1401        );
1402        booted
1403            .session
1404            .boundary(Input::default())
1405            .expect("the boundary ran");
1406        let counts = published(&mut booted.session, &mut records, |publication| {
1407            publication
1408                .views
1409                .iter()
1410                .map(|view| view.records.segments().len())
1411                .collect::<Vec<_>>()
1412        });
1413        assert!(counts[0] > 0);
1414        assert_eq!(counts[1], 96);
1415        let mut display = *booted.session.app.display.get();
1416        display.section_perimeter = false;
1417        send(&mut booted, Action::Display(display));
1418        booted
1419            .session
1420            .boundary(Input::default())
1421            .expect("perimeter off");
1422        published(&mut booted.session, &mut records, |publication| {
1423            assert!(publication.views[0].records.segments().is_empty());
1424            assert_eq!(publication.views[1].records.segments().len(), 96);
1425        });
1426    }
1427
1428    #[test]
1429    fn an_unmoved_row_under_an_unchanged_view_is_not_republished() {
1430        let mut booted = one_slot();
1431        let mut records = Records::default();
1432        booted
1433            .session
1434            .boundary(Input::default())
1435            .expect("the boundary ran");
1436        let built = |session: &mut Session<Playground>, records: &mut Records| {
1437            published(session, records, |publication| {
1438                publication.views[0].records.built()
1439            })
1440        };
1441        built(&mut booted.session, &mut records);
1442        let first = built(&mut booted.session, &mut records);
1443        booted
1444            .session
1445            .boundary(Input::default())
1446            .expect("the boundary ran");
1447        let idle = built(&mut booted.session, &mut records);
1448        assert_eq!(first, idle, "publication rebuilt records nothing changed");
1449
1450        send(&mut booted, Action::Slice(0.4));
1451        booted
1452            .session
1453            .boundary(Input::default())
1454            .expect("the boundary ran");
1455        let moved = built(&mut booted.session, &mut records);
1456        assert_ne!(
1457            idle, moved,
1458            "a new slice left the view's records at their old build"
1459        );
1460    }
1461
1462    #[test]
1463    fn toybox_mode_parks_the_rotation_row_and_spawns_five_floor_cleared_toys() {
1464        let mut booted = one_slot();
1465        let original = slot_entity(&booted);
1466        send(&mut booted, Action::Mode(Mode::Toybox));
1467        booted
1468            .session
1469            .boundary(Input::default())
1470            .expect("the boundary ran");
1471        assert!(booted.session.app.hidden.contains(original));
1472        assert!(!booted.session.app.slots.contains(original));
1473        assert_eq!(booted.session.app.slots.len(), 5);
1474        assert_eq!(booted.session.app.toys.len(), 5);
1475        assert_eq!(booted.session.app.walls.len(), 8);
1476        let expected = [
1477            Polytope4::Cell24,
1478            Polytope4::Tesseract,
1479            Polytope4::Pentatope,
1480            Polytope4::Cell16,
1481            Polytope4::Tesseract,
1482        ];
1483        let slots: Vec<(Entity, Slot)> = booted
1484            .session
1485            .app
1486            .slots
1487            .iter()
1488            .map(|(entity, slot)| (entity, *slot))
1489            .collect();
1490        let r4 = booted
1491            .session
1492            .domains()
1493            .read(booted.domain)
1494            .expect("the r4 domain");
1495        for &(entity, slot) in &slots {
1496            assert_eq!(slot.entry.collider_polytope(), Some(expected[slot.index]));
1497            let pose = r4.poses().get(entity).expect("the toy has a pose");
1498            let polytope = expected[slot.index];
1499            let lowest = polytope
1500                .topology()
1501                .vertices
1502                .iter()
1503                .map(|vertex| (pose.point + pose.frame.apply(*vertex * toy::BODY_SIZE)).y)
1504                .fold(f32::INFINITY, f32::min);
1505            assert!((lowest - consts::FLOOR_Y - 0.20).abs() < 1e-5);
1506            assert!(r4
1507                .physics()
1508                .and_then(|physics| physics.body(entity))
1509                .is_some());
1510        }
1511        for _ in 0..600 {
1512            booted.session.tick().expect("the tick ran");
1513        }
1514        let toys: Vec<Entity> = booted
1515            .session
1516            .app
1517            .toys
1518            .iter()
1519            .map(|(entity, _)| entity)
1520            .collect();
1521        let r4 = booted
1522            .session
1523            .domains()
1524            .read(booted.domain)
1525            .expect("the r4 domain");
1526        let physics = r4.physics().expect("the domain has physics");
1527        assert!(toys.iter().all(|entity| physics
1528            .body(*entity)
1529            .and_then(|body| physics.world().body(body))
1530            .is_some_and(|body| body.is_sleeping())));
1531        for (entity, slot) in slots {
1532            let body = physics
1533                .world()
1534                .body(physics.body(entity).expect("body"))
1535                .expect("body row");
1536            let lowest = slot
1537                .entry
1538                .collider_polytope()
1539                .expect("polytope")
1540                .topology()
1541                .vertices
1542                .iter()
1543                .map(|vertex| {
1544                    (body.position + body.orientation.rotation.apply(*vertex * toy::BODY_SIZE)).y
1545                })
1546                .fold(f32::INFINITY, f32::min);
1547            assert!(
1548                (consts::FLOOR_Y..=consts::FLOOR_Y + loam::physics::manifold::PENETRATION_SLOP)
1549                    .contains(&lowest),
1550                "{} rests at y={lowest}",
1551                slot.entry.label
1552            );
1553        }
1554    }
1555
1556    #[test]
1557    fn row_edits_preserve_rotation_and_reset_keeps_authored_shapes() {
1558        let mut booted = one_slot();
1559        send(&mut booted, Action::PlaneAngle(0, 0.4));
1560        send(&mut booted, Action::AddShape(4));
1561        send(&mut booted, Action::AddShape(6));
1562        send(&mut booted, Action::RemoveShape(0));
1563        booted.session.boundary(Input::default()).expect("edit row");
1564        assert!(booted.session.results().iter().all(|r| r.outcome.is_ok()));
1565        let active = booted
1566            .session
1567            .app
1568            .slots
1569            .iter()
1570            .find(|(_, slot)| slot.index == *booted.session.app.active.get())
1571            .expect("active shape")
1572            .0;
1573        let frame = booted
1574            .session
1575            .domains()
1576            .read(booted.domain)
1577            .expect("domain")
1578            .poses()
1579            .get(active)
1580            .expect("pose")
1581            .frame;
1582        send(&mut booted, Action::ReorderShape { from: 0, to: 2 });
1583        booted.session.boundary(Input::default()).expect("reorder");
1584        assert!(booted.session.results().iter().all(|r| r.outcome.is_ok()));
1585        assert_eq!(booted.session.app.active.get(), &0);
1586        assert_eq!(
1587            booted
1588                .session
1589                .app
1590                .slots
1591                .get(active)
1592                .expect("active shape")
1593                .index,
1594            0
1595        );
1596        let rows: Vec<_> = booted
1597            .session
1598            .app
1599            .slots
1600            .iter()
1601            .map(|(entity, slot)| (entity, *slot))
1602            .collect();
1603        assert_eq!(rows.len(), 2);
1604        let domain = booted
1605            .session
1606            .domains()
1607            .read(booted.domain)
1608            .expect("domain");
1609        for (entity, slot) in &rows {
1610            assert_eq!(slot.rest, rest_of(slot.index, 2));
1611            let pose = domain.poses().get(*entity).expect("pose");
1612            assert_ne!(pose.frame, loam::math::Rotor4::IDENTITY);
1613            assert_eq!(pose.frame, frame);
1614            assert_eq!(pose.point, slot.rest);
1615            assert_eq!(domain.instances().contains(*entity), slot.index == 1);
1616        }
1617        send(&mut booted, Action::Reset);
1618        booted.session.boundary(Input::default()).expect("reset");
1619        assert_eq!(booted.session.app.slots.len(), 2);
1620        let domain = booted
1621            .session
1622            .domains()
1623            .read(booted.domain)
1624            .expect("domain");
1625        for (entity, slot) in rows {
1626            let pose = domain.poses().get(entity).expect("same entity");
1627            assert_eq!(pose.point, slot.rest);
1628            assert_eq!(pose.frame, loam::math::Rotor4::IDENTITY);
1629        }
1630    }
1631
1632    #[test]
1633    fn rotate_pointer_drag_cannot_pick_or_translate_a_shape() {
1634        let mut booted = one_slot();
1635        booted.session.boundary(Input::default()).expect("boundary");
1636        let entity = slot_entity(&booted);
1637        let before = *booted
1638            .session
1639            .domains()
1640            .read(booted.domain)
1641            .expect("domain")
1642            .poses()
1643            .get(entity)
1644            .expect("pose");
1645        for (phase, ndc) in [
1646            (PointerPhase::Began, [0.0, 0.0]),
1647            (PointerPhase::Moved, [0.5, 0.0]),
1648            (PointerPhase::Ended, [0.5, 0.0]),
1649        ] {
1650            let pointer = Pointer {
1651                id: 0,
1652                button: Some(PointerButton::Primary),
1653                ndc,
1654                delta: [0.5, 0.0],
1655                phase,
1656                time: 1.0,
1657            };
1658            booted
1659                .session
1660                .boundary(Input {
1661                    pointers: vec![pointer],
1662                    ..Input::default()
1663                })
1664                .expect("boundary");
1665        }
1666        assert!(booted.session.dragging().is_none());
1667        let after = booted
1668            .session
1669            .domains()
1670            .read(booted.domain)
1671            .expect("domain")
1672            .poses()
1673            .get(entity)
1674            .expect("pose");
1675        assert_eq!(after.point, before.point);
1676        assert_eq!(after.frame, before.frame);
1677    }
1678
1679    #[test]
1680    fn a_warmed_frame_with_every_overlay_on_asks_the_allocator_for_nothing() {
1681        let mut booted = one_slot();
1682        booted
1683            .session
1684            .dispatch(|dispatch| Action::Gimbal(None).apply(dispatch))
1685            .expect("handles");
1686        booted
1687            .session
1688            .dispatch(|dispatch| Action::Hud(None).apply(dispatch))
1689            .expect("readout");
1690        send(&mut booted, Action::Color(ColorMode::WDepth));
1691        send(
1692            &mut booted,
1693            Action::Strip(Strip {
1694                on: true,
1695                w: true,
1696                t: true,
1697                ..Strip::default()
1698            }),
1699        );
1700        booted
1701            .session
1702            .boundary(Input::default())
1703            .expect("the boundary ran");
1704        assert!(booted.session.results().iter().all(|r| r.outcome.is_ok()));
1705
1706        let mut records = Records::default();
1707        let mut scratch = Scratch::new();
1708        let mut gimbal = Gimbal::default();
1709        gimbal.enabled = true;
1710        let mut gimbal_renderer = gimbal::GimbalRenderer::default();
1711        let mut lines = String::new();
1712        let frame = |booted: &mut Boot,
1713                     records: &mut Records,
1714                     scratch: &mut Scratch,
1715                     gimbal_renderer: &mut gimbal::GimbalRenderer,
1716                     lines: &mut String| {
1717            booted
1718                .session
1719                .boundary(Input::default())
1720                .expect("the boundary ran");
1721            booted.session.tick().expect("the tick ran");
1722            records.publish(&mut booted.session).expect("published");
1723            let publication = records.lend().expect("the buffer is free");
1724            let alpha = publication.alpha;
1725            records.release(publication);
1726            collect(&booted.session, booted.domain, alpha, scratch);
1727            fill_strip(
1728                &{ *booted.session.app.strip.get() },
1729                turn_of(&booted.session),
1730                HEADLESS_FRAME,
1731                0.0,
1732                scratch,
1733            );
1734            gimbal_renderer.rings(&gimbal, scratch.center);
1735            hud::write_readout(lines, &readout_of(&booted.session));
1736        };
1737        for _ in 0..16 {
1738            frame(
1739                &mut booted,
1740                &mut records,
1741                &mut scratch,
1742                &mut gimbal_renderer,
1743                &mut lines,
1744            );
1745        }
1746
1747        let bytes = loam_time::alloc::bytes_allocated_by(|| {
1748            for _ in 0..16 {
1749                frame(
1750                    &mut booted,
1751                    &mut records,
1752                    &mut scratch,
1753                    &mut gimbal_renderer,
1754                    &mut lines,
1755                );
1756            }
1757        })
1758        .expect("the counting allocator is installed");
1759        assert_eq!(
1760            bytes, 0,
1761            "sixteen warmed frames of boundary, tick, publication, collection, strip, gimbal, and readout asked the allocator for {bytes} bytes"
1762        );
1763    }
1764
1765    #[test]
1766    fn reset_restyles_replacement_toys_even_when_the_row_count_does_not_change() {
1767        let mut booted = one_slot();
1768        send(&mut booted, Action::Mode(Mode::Toybox));
1769        send(&mut booted, Action::Color(ColorMode::UniqueEdge));
1770        send(
1771            &mut booted,
1772            Action::Display(Display {
1773                wireframe: true,
1774                wireframe_width_px: 4.0,
1775                wireframe_opacity: 0.2,
1776                ..Display::default()
1777            }),
1778        );
1779        booted
1780            .session
1781            .boundary(Input::default())
1782            .expect("style toys");
1783        let mut records = Records::default();
1784        published(&mut booted.session, &mut records, |_| ());
1785        let instances = |booted: &Boot| {
1786            booted
1787                .session
1788                .domains()
1789                .read(booted.domain)
1790                .expect("domain")
1791                .instances()
1792                .iter()
1793                .map(|(_, instance)| *instance)
1794                .collect::<Vec<_>>()
1795        };
1796        let before = instances(&booted);
1797        assert_eq!(before.len(), 5);
1798        assert!(before
1799            .iter()
1800            .all(|i| i.line_width_px == Some(4.0) && i.line_opacity == Some(0.2)));
1801        send(&mut booted, Action::Reset);
1802        booted
1803            .session
1804            .boundary(Input::default())
1805            .expect("reset toys");
1806        published(&mut booted.session, &mut records, |_| ());
1807        assert_eq!(instances(&booted), before);
1808    }
1809
1810    #[test]
1811    fn a_primary_grab_does_not_block_secondary_orbit_or_wheel_zoom() {
1812        let mut booted = one_slot();
1813        send(&mut booted, Action::Mode(Mode::Toybox));
1814        booted.session.boundary(Input::default()).expect("toybox");
1815        let center = toy::pose_at(Polytope4::Pentatope, 0.0).point.truncate();
1816        booted.session.app.control.get_mut().orbit = Orbit::around(center.to_array(), EYE_BACK);
1817        let mut input = loam::app::session::input::InputMap::default();
1818        input.resize(800, 600, 1.0);
1819        input.button([0.0; 2], PointerButton::Primary, true);
1820        booted.session.boundary(input.take()).expect("grab");
1821        let picked = booted.session.dragging().expect("grab");
1822        booted.session.boundary(Input::default()).expect("hold");
1823
1824        input.button([0.0; 2], PointerButton::Secondary, true);
1825        input.moved([0.05, 0.05]);
1826        input.wheel([0.0, 1.0]);
1827        let movement = input.take();
1828        booted.session.boundary(movement).expect("move");
1829        let orbit = booted.session.app.control.get().orbit;
1830        let root = booted.session.views().root();
1831        let eye = booted.session.views().get(root).expect("root view").eye;
1832        assert!(orbit.yaw < -0.1 && orbit.pitch > 0.08);
1833        assert!(orbit.distance < EYE_BACK);
1834        let held = booted.session.dragging().expect("still grabbed");
1835        assert_eq!(held.entity, picked.entity);
1836        let clip = loam::render::view::root_view_projection(&eye)
1837            * glam::Vec3::from(held.image_point()).extend(1.0);
1838        assert!((clip.x / clip.w - 0.05).abs() < 1e-5);
1839        assert!((clip.y / clip.w - 0.05).abs() < 1e-5);
1840        let physics = booted
1841            .session
1842            .domains()
1843            .read(booted.domain)
1844            .expect("domain");
1845        assert!(physics.physics().expect("physics").is_held(picked.entity));
1846    }
1847
1848    fn flicked(step_ndc: f32) -> (Vec4, Vec4, Vec4) {
1849        use loam::physics::PhysicsSpace;
1850
1851        let mut booted = one_slot();
1852        send(&mut booted, Action::Mode(Mode::Toybox));
1853        booted.session.boundary(Input::default()).expect("toybox");
1854        let entity = booted
1855            .session
1856            .app
1857            .slots
1858            .iter()
1859            .find(|(_, slot)| slot.index == 2)
1860            .map(|(entity, _)| entity)
1861            .expect("middle toy");
1862        let center = booted
1863            .session
1864            .domains()
1865            .read(booted.domain)
1866            .expect("domain")
1867            .poses()
1868            .get(entity)
1869            .expect("pose")
1870            .point
1871            .truncate();
1872        booted.session.app.control.get_mut().orbit = Orbit::around(center.to_array(), EYE_BACK);
1873        booted.session.boundary(Input::default()).expect("aim");
1874        booted.session.grab([0.0; 2], 0.0).expect("grab");
1875        booted.session.boundary(Input::default()).expect("hold");
1876        booted.session.tick().expect("tick");
1877        let mut time = 0.0;
1878        for step in 1..=6 {
1879            time = f64::from(step) / 60.0;
1880            booted
1881                .session
1882                .drag([0.0, step_ndc * step as f32], time)
1883                .expect("drag");
1884            booted.session.boundary(Input::default()).expect("carry");
1885            booted.session.tick().expect("tick");
1886        }
1887        let release = booted.session.release_at(time).expect("held");
1888        send(&mut booted, Action::Throw(release.entity, release.velocity));
1889        booted.session.boundary(Input::default()).expect("throw");
1890        let r4 = booted
1891            .session
1892            .domains()
1893            .read(booted.domain)
1894            .expect("domain");
1895        let physics = r4.physics().expect("physics");
1896        let body = physics
1897            .world()
1898            .body(physics.body(entity).expect("body"))
1899            .expect("row");
1900        let handle = physics.released_anchor(entity).expect("released");
1901        let slip = EuclideanR4.velocity_at_point(body, handle) - body.velocity;
1902        let flick = glam::Vec3::from(release.velocity).extend(0.0);
1903        (body.velocity, slip, flick)
1904    }
1905
1906    #[test]
1907    fn a_rigid_flick_spins_the_toy_the_way_it_moved_the_grab_point() {
1908        let (_, slip, flick) = flicked(0.06);
1909        assert!(
1910            slip.dot(flick.normalize()) > 0.5,
1911            "a flick along {flick} left the grab point turning at {slip}"
1912        );
1913    }
1914
1915    #[test]
1916    fn a_flick_past_the_carry_cap_still_throws_faster() {
1917        let (fast, _, _) = flicked(0.22);
1918        let (faster, _, _) = flicked(0.35);
1919        assert!(
1920            faster.length() > 1.4 * fast.length(),
1921            "a faster flick moved the throw only from {} to {}",
1922            fast.length(),
1923            faster.length()
1924        );
1925    }
1926
1927    #[test]
1928    fn a_carry_by_the_camera_alone_throws_with_its_motion() {
1929        let mut booted = one_slot();
1930        send(&mut booted, Action::Mode(Mode::Toybox));
1931        booted.session.boundary(Input::default()).expect("toybox");
1932        let center = toy::pose_at(Polytope4::Pentatope, 0.0).point.truncate();
1933        booted.session.app.control.get_mut().orbit = Orbit::around(center.to_array(), EYE_BACK);
1934        let pointer = |phase, time| Pointer {
1935            id: 0,
1936            button: Some(PointerButton::Primary),
1937            ndc: [0.0; 2],
1938            delta: [0.0; 2],
1939            phase,
1940            time,
1941        };
1942        booted
1943            .session
1944            .boundary(Input {
1945                pointers: vec![pointer(PointerPhase::Began, 0.0)],
1946                ..Input::default()
1947            })
1948            .expect("grab");
1949        let picked = booted.session.dragging().expect("grab").entity;
1950        for step in 1..=12 {
1951            booted.session.app.control.get_mut().orbit.distance -= 0.1;
1952            booted
1953                .session
1954                .boundary(Input {
1955                    time: step as f64 * 0.016,
1956                    ..Input::default()
1957                })
1958                .expect("carry");
1959            booted.session.tick().expect("the carry tick ran");
1960        }
1961        booted
1962            .session
1963            .boundary(Input {
1964                pointers: vec![pointer(PointerPhase::Ended, 13.0 * 0.016)],
1965                time: 13.0 * 0.016,
1966                ..Input::default()
1967            })
1968            .expect("release");
1969        let physics = booted
1970            .session
1971            .domains()
1972            .read(booted.domain)
1973            .expect("domain")
1974            .physics()
1975            .expect("physics");
1976        let body = physics.body(picked).expect("body");
1977        let velocity = physics.world().body(body).expect("body row").velocity;
1978        assert!(velocity.z < -1.0, "a camera carry released at {velocity}");
1979    }
1980
1981    #[test]
1982    fn a_toybox_drag_stays_held_between_samples_and_reports_its_guides() {
1983        let mut booted = one_slot();
1984        send(&mut booted, Action::Mode(Mode::Toybox));
1985        booted
1986            .session
1987            .boundary(Input::default())
1988            .expect("the boundary ran");
1989        let entity = booted
1990            .session
1991            .app
1992            .slots
1993            .iter()
1994            .find(|(_, slot)| slot.index == 2)
1995            .map(|(entity, _)| entity)
1996            .expect("the center toy is live");
1997        let center = booted
1998            .session
1999            .domains()
2000            .read(booted.domain)
2001            .expect("the r4 domain")
2002            .poses()
2003            .get(entity)
2004            .expect("the center toy has a pose")
2005            .point
2006            .truncate();
2007        booted.session.views_mut().root_mut().eye = Eye::looking_at(
2008            [center.x, center.y, EYE_BACK],
2009            center.to_array(),
2010            [0.0, 1.0, 0.0],
2011        );
2012        let picked = booted
2013            .session
2014            .grab([0.0, 0.0], 0.0)
2015            .expect("the ray through the slot center picks it");
2016        assert_eq!(picked.entity, entity);
2017        booted.session.boundary(Input::default()).expect("hold");
2018        booted.session.app.toybox_debug.set(true);
2019        let mut guides = guides::Guides::default();
2020        guides.update(&booted.session, booted.domain, 1.0);
2021        assert_eq!(guides.points.len(), 1);
2022        assert_eq!(guides.lines.len(), 8);
2023        let anchor = guides.points[0].position;
2024
2025        let target = booted
2026            .session
2027            .drag([0.0, 0.28], 1.0)
2028            .expect("the drag meets its plane");
2029        booted
2030            .session
2031            .boundary(Input::default())
2032            .expect("the boundary applied the move");
2033        guides.update(&booted.session, booted.domain, 1.0);
2034        assert_eq!(guides.points[0].position, anchor);
2035        let refusal = booted
2036            .session
2037            .dispatch(|dispatch| {
2038                dispatch.apply(Command::Chart(
2039                    booted.domain.id(),
2040                    ChartCommand::Move {
2041                        entity: picked.entity,
2042                        point: ChartPoint {
2043                            chart: target.chart,
2044                            coordinates: [f32::NAN, 0.0, 0.0, 0.0],
2045                        },
2046                    },
2047                ))
2048            })
2049            .expect_err("a held body accepted a nonfinite target");
2050        assert!(matches!(
2051            refusal,
2052            Rejection::Domain(loam::runtime::DomainError::InvalidCoordinate("x"))
2053        ));
2054        for _ in 0..120 {
2055            booted.session.tick().expect("the tick ran");
2056        }
2057        let r4 = booted
2058            .session
2059            .domains()
2060            .read(booted.domain)
2061            .expect("the r4 domain");
2062        let grip = r4
2063            .physics()
2064            .and_then(|physics| physics.held_anchor(picked.entity))
2065            .expect("the toy is held");
2066        let goal = anchor[1] + target.coordinates[1] - center.y;
2067        assert!(
2068            (grip.y - goal).abs() < 0.1,
2069            "the grab point stopped following at y {} before its goal {goal}",
2070            grip.y
2071        );
2072
2073        let release = booted.session.release_at(1.05).expect("the drag was live");
2074        guides.update(&booted.session, booted.domain, 1.0);
2075        assert!(guides.points.is_empty());
2076        assert_eq!(guides.lines.len(), 4);
2077        send(&mut booted, Action::Throw(release.entity, release.velocity));
2078        booted
2079            .session
2080            .boundary(Input::default())
2081            .expect("the release and throw landed");
2082        for _ in 0..600 {
2083            booted.session.tick().expect("the tick ran");
2084        }
2085        let polytope = booted
2086            .session
2087            .app
2088            .slots
2089            .get(release.entity)
2090            .and_then(|slot| slot.entry.collider_polytope())
2091            .expect("the released toy has a polytope");
2092        let r4 = booted
2093            .session
2094            .domains()
2095            .read(booted.domain)
2096            .expect("the r4 domain");
2097        let physics = r4.physics().expect("the domain has physics");
2098        let body = physics.body(release.entity).expect("the toy has a body");
2099        let body = physics.world().body(body).expect("body row");
2100        let slice_reach = polytope
2101            .topology()
2102            .vertices
2103            .iter()
2104            .map(|vertex| {
2105                (body.orientation.rotation.apply(*vertex * toy::BODY_SIZE))
2106                    .w
2107                    .abs()
2108            })
2109            .fold(0.0f32, f32::max);
2110        assert!(
2111            body.position.w.abs() <= slice_reach,
2112            "the released toy left its slice reach {slice_reach} at w {} with velocity {}",
2113            body.position.w,
2114            body.velocity.w
2115        );
2116        assert!(body.is_sleeping());
2117    }
2118
2119    #[test]
2120    fn a_shape_card_respawns_the_slot_in_place_with_the_new_polytopes_edges() {
2121        let mut booted = one_slot();
2122        send(
2123            &mut booted,
2124            Action::Display(Display {
2125                wireframe: true,
2126                ..Display::default()
2127            }),
2128        );
2129        send(&mut booted, Action::Slice(consts::W_RANGE));
2130        let rest = booted
2131            .session
2132            .app
2133            .slots
2134            .iter()
2135            .map(|(_, slot)| slot.rest)
2136            .next()
2137            .expect("the row has a slot");
2138
2139        send(&mut booted, Action::Shape(0, 0));
2140        booted
2141            .session
2142            .boundary(Input::default())
2143            .expect("the boundary ran");
2144
2145        let held: Vec<Slot> = booted.session.app.slots.iter().map(|(_, s)| *s).collect();
2146        assert_eq!(held.len(), 1, "the swap left {} slots", held.len());
2147        assert_eq!(held[0].entry.label, "5-cell");
2148        assert_eq!(held[0].index, 0);
2149        assert_eq!(
2150            held[0].rest, rest,
2151            "the replacement moved off the slot's rest"
2152        );
2153
2154        let mut records = Records::default();
2155        let counts = published(&mut booted.session, &mut records, |publication| {
2156            publication
2157                .views
2158                .iter()
2159                .map(|view| view.records.segments().len())
2160                .collect::<Vec<_>>()
2161        });
2162        assert_eq!(
2163            counts,
2164            [0, 10],
2165            "the swapped slot still publishes the old polytope's edges"
2166        );
2167    }
2168
2169    #[test]
2170    fn held_slice_motion_uses_fixed_ticks_while_authored_spin_is_paused() {
2171        const TICKS: usize = 8;
2172
2173        let scrubbed = |boundaries: usize| {
2174            let mut booted = one_slot();
2175            booted.session.app.spin.get_mut().running = false;
2176            for boundary in 0..boundaries {
2177                let held = if boundary == 0 || boundaries > 1 {
2178                    vec![SLICE_UP]
2179                } else {
2180                    Vec::new()
2181                };
2182                booted
2183                    .session
2184                    .boundary(Input {
2185                        held,
2186                        ..Input::default()
2187                    })
2188                    .expect("the boundary ran");
2189                for _ in 0..TICKS / boundaries {
2190                    booted.session.tick().expect("the tick ran");
2191                }
2192            }
2193            *booted.session.app.slice.get()
2194        };
2195
2196        assert!((scrubbed(1) - scrubbed(TICKS)).abs() < 1e-6);
2197        assert!(scrubbed(1) > 0.0);
2198    }
2199
2200    #[test]
2201    fn the_gimbal_rotor_turns_the_row_by_the_angle_its_ring_names() {
2202        use loam::math::{Bivector, Plane4, Rotor};
2203
2204        let mut booted = one_slot();
2205        const ANGLE: f32 = 0.4;
2206        let domain = booted.domain;
2207        booted.session.dispatch(|d| {
2208            mode::turn_row(
2209                d.app,
2210                d.domains,
2211                domain,
2212                (Plane4::Xw.unit_bivector() * ANGLE).exp(),
2213            )
2214            .expect("the turn applies")
2215        });
2216
2217        let entity = slot_entity(&booted);
2218        let r4 = booted
2219            .session
2220            .domains()
2221            .read(booted.domain)
2222            .expect("the r4 domain");
2223        let turned = r4.poses().get(entity).expect("pose").frame.apply(Vec4::X);
2224        let expected = Vec4::new(ANGLE.cos(), 0.0, 0.0, ANGLE.sin());
2225        assert!(
2226            (turned - expected).length() < 1e-5,
2227            "the ring's rotor sent x to {turned} rather than the analytic {expected}"
2228        );
2229    }
2230
2231    #[test]
2232    fn a_press_on_a_ring_turns_the_row_only_while_the_filmstrip_is_off() {
2233        fn press_and_drag(booted: &mut Boot, ndc: [f32; 2]) -> bool {
2234            let mut at = |phase, ndc, time| {
2235                let pointer = Pointer {
2236                    id: 0,
2237                    button: Some(PointerButton::Primary),
2238                    ndc,
2239                    delta: [0.0; 2],
2240                    phase,
2241                    time,
2242                };
2243                booted
2244                    .session
2245                    .boundary(Input {
2246                        pointers: vec![pointer],
2247                        ..Input::default()
2248                    })
2249                    .expect("pointer boundary");
2250                booted.session.app.control.get().gimbal.held()
2251            };
2252            let taken = at(PointerPhase::Began, ndc, 0.0);
2253            at(PointerPhase::Moved, [ndc[0] + 0.2, ndc[1] + 0.2], 1.0);
2254            taken
2255        }
2256
2257        let mut booted = one_slot();
2258        booted
2259            .session
2260            .dispatch(|dispatch| Action::Gimbal(None).apply(dispatch))
2261            .expect("handles");
2262        booted
2263            .session
2264            .boundary(Input::default())
2265            .expect("the boundary ran");
2266        let on_ring = gimbal::widget(glam::Vec3::new(0.0, BODY_Y, 0.0)).rings()[0].point(0.0);
2267        let ndc = booted
2268            .session
2269            .views()
2270            .ndc(on_ring.to_array())
2271            .expect("the ring is in front of the eye");
2272
2273        assert!(
2274            press_and_drag(&mut booted, ndc),
2275            "the press missed the ring, so the strip has nothing to mask"
2276        );
2277        booted
2278            .session
2279            .boundary(Input::default())
2280            .expect("the boundary ran");
2281        let entity = slot_entity(&booted);
2282        let turned = booted
2283            .session
2284            .domains()
2285            .read(booted.domain)
2286            .expect("the r4 domain")
2287            .poses()
2288            .get(entity)
2289            .expect("pose")
2290            .frame;
2291        assert_ne!(
2292            turned,
2293            loam::math::Rotor4::IDENTITY,
2294            "the ring drag never turned the row, so the check below proves nothing"
2295        );
2296
2297        send(
2298            &mut booted,
2299            Action::Strip(Strip {
2300                on: true,
2301                ..Strip::default()
2302            }),
2303        );
2304        booted
2305            .session
2306            .boundary(Input::default())
2307            .expect("the boundary ran");
2308        assert!(
2309            !press_and_drag(&mut booted, ndc),
2310            "the gimbal took a press while the filmstrip covered it"
2311        );
2312        booted
2313            .session
2314            .boundary(Input::default())
2315            .expect("the boundary ran");
2316        let held = booted
2317            .session
2318            .domains()
2319            .read(booted.domain)
2320            .expect("the r4 domain")
2321            .poses()
2322            .get(entity)
2323            .expect("pose")
2324            .frame;
2325        assert_eq!(
2326            held, turned,
2327            "an invisible gimbal turned the row under the filmstrip"
2328        );
2329    }
2330
2331    #[test]
2332    fn a_cancelled_pointer_releases_a_live_grab() {
2333        let mut booted = one_slot();
2334        send(&mut booted, Action::Mode(Mode::Toybox));
2335        booted
2336            .session
2337            .boundary(Input::default())
2338            .expect("the boundary ran");
2339        let entity = booted
2340            .session
2341            .app
2342            .slots
2343            .iter()
2344            .find(|(_, slot)| slot.index == 2)
2345            .map(|(entity, _)| entity)
2346            .expect("middle toy");
2347        let center = booted
2348            .session
2349            .domains()
2350            .read(booted.domain)
2351            .expect("domain")
2352            .poses()
2353            .get(entity)
2354            .expect("pose")
2355            .point
2356            .truncate();
2357        booted.session.views_mut().root_mut().eye = Eye::looking_at(
2358            [center.x, center.y, EYE_BACK],
2359            center.to_array(),
2360            [0.0, 1.0, 0.0],
2361        );
2362        booted
2363            .session
2364            .grab([0.0, 0.0], 0.0)
2365            .expect("the ray through the slot center picks it");
2366        let pointer = Pointer {
2367            id: 0,
2368            button: Some(PointerButton::Primary),
2369            ndc: [0.0, 0.0],
2370            delta: [0.0; 2],
2371            phase: PointerPhase::Cancelled,
2372            time: 1.0,
2373        };
2374        let hover = Pointer {
2375            button: None,
2376            phase: PointerPhase::Moved,
2377            ..pointer
2378        };
2379        let input = Input {
2380            pointers: vec![pointer, hover],
2381            ..Input::default()
2382        };
2383        booted.session.boundary(input).expect("cancel boundary");
2384
2385        assert!(booted.session.dragging().is_none());
2386        assert!(
2387            booted.session.drag([0.5, 0.0], 2.0).is_err(),
2388            "the grab survived the focus change that cancelled the pointer"
2389        );
2390    }
2391
2392    #[test]
2393    fn a_raster_cut_is_duplicated_in_the_projection_layer() {
2394        const TESSERACT: ShapeEntry = ShapeEntry {
2395            shape: RaymarchShape::Polytope(Polytope4::Tesseract),
2396            body_color: [0.30, 0.55, 0.95],
2397            label: "8-cell",
2398            long_name: "tesseract",
2399            category: catalog::Category::RegularPolychoron,
2400        };
2401        let mut booted = boot(&[TESSERACT]).expect("the session boots");
2402        booted.session.boundary(Input::default()).expect("boundary");
2403        let mut records = Records::default();
2404        published(&mut booted.session, &mut records, |publication| {
2405            assert_eq!(publication.views[0].records.triangles().len(), 24);
2406            assert!(publication.views[1].records.triangles().is_empty());
2407        });
2408    }
2409
2410    #[test]
2411    fn filmstrip_recenters_after_single_and_restores_the_single_camera() {
2412        let focus = (true, 0, Mode::Rotate);
2413        let single_target = [-2.4, 0.0, 0.0];
2414        let mut orbit = Orbit::around(single_target, 5.0);
2415        orbit.yaw = 0.3;
2416        orbit.pitch = -0.4;
2417        let mut camera_focus = Some(focus);
2418        let mut filmstrip_return = None;
2419        let strip = Strip {
2420            on: true,
2421            ..Strip::default()
2422        };
2423
2424        focus_camera(
2425            &mut orbit,
2426            &mut camera_focus,
2427            &mut filmstrip_return,
2428            strip,
2429            focus,
2430            glam::Vec3::from(single_target),
2431        );
2432        assert_eq!(orbit.target, [0.0; 3]);
2433        assert_eq!((orbit.yaw, orbit.pitch, orbit.distance), (0.3, -0.4, 5.0));
2434
2435        focus_camera(
2436            &mut orbit,
2437            &mut camera_focus,
2438            &mut filmstrip_return,
2439            Strip { t: true, ..strip },
2440            focus,
2441            glam::Vec3::from(single_target),
2442        );
2443        assert_eq!(orbit.target, [0.0, BODY_Y, 0.0]);
2444
2445        focus_camera(
2446            &mut orbit,
2447            &mut camera_focus,
2448            &mut filmstrip_return,
2449            Strip { on: false, ..strip },
2450            focus,
2451            glam::Vec3::from(single_target),
2452        );
2453        assert_eq!(orbit.target, single_target);
2454        assert_eq!((orbit.yaw, orbit.pitch, orbit.distance), (0.3, -0.4, 5.0));
2455    }
2456
2457    #[test]
2458    fn the_marcher_takes_one_strip_cell_per_grid_rectangle_and_none_once_the_strip_is_off() {
2459        let mut scratch = Scratch::new();
2460        let strip = Strip {
2461            on: true,
2462            w: true,
2463            t: true,
2464            count_w: 5,
2465            count_t: 3,
2466            ..Strip::default()
2467        };
2468        fill_strip(&strip, Bivector4::ZERO, HEADLESS_FRAME, 0.25, &mut scratch);
2469        assert_eq!(scratch.strip.len(), 15, "a 5 by 3 grid is fifteen draws");
2470        let covered: u64 = scratch
2471            .strip
2472            .iter()
2473            .map(|(viewport, _, _)| u64::from(viewport.width) * u64::from(viewport.height))
2474            .sum();
2475        assert_eq!(
2476            covered,
2477            u64::from(HEADLESS_FRAME.0) * u64::from(HEADLESS_FRAME.1),
2478            "the cells the marcher draws leave a gap or overlap"
2479        );
2480        let slices: Vec<f32> = scratch.strip.iter().map(|(_, w, _)| *w).collect();
2481        assert!(
2482            (slices[0] - (0.25 - BODY_SIZE)).abs() < 1e-6
2483                && (slices[14] - (0.25 + BODY_SIZE)).abs() < 1e-6,
2484            "the strip does not span the body around the slider: {slices:?}"
2485        );
2486
2487        fill_strip(
2488            &Strip { on: false, ..strip },
2489            Bivector4::ZERO,
2490            HEADLESS_FRAME,
2491            0.25,
2492            &mut scratch,
2493        );
2494        assert!(
2495            scratch.strip.is_empty(),
2496            "a stale strip kept the filmstrip on screen after it was switched off"
2497        );
2498    }
2499}