1use loam_shape::polytope::Polytope4Topology;
2
3use crate::bridge::{
4 Bridge, BridgeError, BridgeSpec, Drag, DragError, DragRelease, VELOCITY_SAMPLES,
5};
6use crate::command::{
7 Command, CommandResult, Commands, Dispatch, Outcome, Rejection, Request, RequestId,
8};
9use crate::domain::{
10 presented_alpha, ChartCommand, ChartPoint, DomainBuilder, DomainError, DomainHandle, DomainId,
11 DomainSnapshot, DomainSpace, Domains,
12};
13use crate::entity::{Entities, EntitiesSnapshot, Epoch, RuntimeId, SceneId};
14use crate::input::Input;
15use crate::phase::{Ctx, Order, Phase, PhaseError, Phases, Step, System, SystemEntry, Tick};
16use crate::relation::{LinkId, Relation, RelationSnapshot};
17use crate::store::{Owner, SchemaId, StoreField};
18use crate::stores::Stores;
19use crate::view::{ImageRay, Pick, Rigid, Vec3, ViewRecords, ViewTarget, Views, ViewsSnapshot};
20
21const RELEASE_STALE_SECONDS: f64 = 0.12;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct SimConfig {
25 pub fixed_hz: u32,
26 pub max_ticks_per_frame: u32,
27}
28
29impl SimConfig {
30 pub fn new(fixed_hz: u32, max_ticks_per_frame: u32) -> Result<Self, SimConfigError> {
31 let config = Self {
32 fixed_hz,
33 max_ticks_per_frame,
34 };
35 config.check()?;
36 Ok(config)
37 }
38
39 pub fn check(&self) -> Result<(), SimConfigError> {
40 if self.max_ticks_per_frame == 0 {
41 return Err(SimConfigError::NoCatchUp);
42 }
43 Ok(())
44 }
45
46 pub fn dt(&self) -> Option<f32> {
48 (self.fixed_hz > 0).then(|| 1.0 / self.fixed_hz as f32)
49 }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum SimConfigError {
54 NoCatchUp,
55}
56
57impl std::fmt::Display for SimConfigError {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 Self::NoCatchUp => f.write_str("max_ticks_per_frame must be at least one"),
61 }
62 }
63}
64
65impl std::error::Error for SimConfigError {}
66
67impl Default for SimConfig {
68 fn default() -> Self {
69 Self {
70 fixed_hz: 60,
71 max_ticks_per_frame: 4,
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
77pub struct Stamp {
78 pub tick: Tick,
79 pub sequence: u64,
80}
81
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub struct Growth {
85 pub commands: usize,
86 pub spawned: usize,
87 pub despawned: usize,
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
91pub struct PreparedId(u32);
92
93impl PreparedId {
94 pub fn index(self) -> usize {
95 self.0 as usize
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100pub struct MaterialId(u32);
101
102impl MaterialId {
103 pub fn index(self) -> usize {
104 self.0 as usize
105 }
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
109pub struct PaletteId(u32);
110
111impl PaletteId {
112 pub fn index(self) -> usize {
113 self.0 as usize
114 }
115}
116
117#[derive(Clone, Debug, PartialEq)]
118pub enum PreparedGeometry {
119 Lines4 {
120 segments: Vec<[[f32; 4]; 2]>,
121 },
122 Lines3 {
123 segments: Vec<[[f32; 3]; 2]>,
124 },
125 Polytope4 {
127 polytope: loam_shape::polytope::Polytope4,
128 scale: f32,
129 },
130}
131
132impl PreparedGeometry {
133 pub fn edges_of(topology: &Polytope4Topology, scale: f32) -> Self {
134 let segments = topology
135 .edges
136 .iter()
137 .map(|&[i, j]| {
138 [
139 (topology.vertices[i as usize] * scale).to_array(),
140 (topology.vertices[j as usize] * scale).to_array(),
141 ]
142 })
143 .collect();
144 Self::Lines4 { segments }
145 }
146
147 pub fn bounding_radius(&self) -> f32 {
149 fn farthest<'a>(points: impl Iterator<Item = &'a [f32]>) -> f32 {
150 points
151 .map(|point| point.iter().map(|c| c * c).sum::<f32>().sqrt())
152 .fold(0.0, f32::max)
153 }
154 match self {
155 Self::Lines4 { segments } => {
156 farthest(segments.iter().flatten().map(|point| point.as_slice()))
157 }
158 Self::Lines3 { segments } => {
159 farthest(segments.iter().flatten().map(|point| point.as_slice()))
160 }
161 Self::Polytope4 { polytope, scale } => {
162 polytope
163 .topology()
164 .vertices
165 .iter()
166 .map(|vertex| vertex.length())
167 .fold(0.0, f32::max)
168 * scale
169 }
170 }
171 }
172}
173
174#[derive(Clone, Copy, Debug, PartialEq)]
175pub enum Material {
176 Lines { color: [f32; 4], width_px: f32 },
177 Flat { color: [f32; 4] },
178}
179
180impl Material {
181 pub fn lines(color: [f32; 4], width_px: f32) -> Self {
182 Self::Lines { color, width_px }
183 }
184
185 pub fn flat(color: [f32; 4]) -> Self {
186 Self::Flat { color }
187 }
188}
189
190#[derive(Clone, Copy)]
191pub struct Library<'a> {
192 pub(crate) geometry: &'a [PreparedGeometry],
193 pub(crate) materials: &'a [Material],
194 pub(crate) palettes: &'a [Vec<[f32; 4]>],
195}
196
197impl Library<'_> {
198 pub(crate) fn line_style(&self, material: MaterialId) -> ([f32; 4], f32) {
199 match self.materials.get(material.index()) {
200 Some(Material::Lines { color, width_px }) => (*color, *width_px),
201 Some(Material::Flat { color }) => (*color, 1.0),
202 None => ([1.0; 4], 1.0),
203 }
204 }
205
206 pub(crate) fn palette(&self, palette: PaletteId) -> &[[f32; 4]] {
207 self.palettes
208 .get(palette.index())
209 .map_or(&[][..], Vec::as_slice)
210 }
211}
212
213#[derive(Default)]
214pub(crate) struct Assets {
215 prepared: Vec<PreparedGeometry>,
216 materials: Vec<Material>,
217 palettes: Vec<Vec<[f32; 4]>>,
218}
219
220impl Assets {
221 pub(crate) fn prepare(&mut self, geometry: PreparedGeometry) -> PreparedId {
222 self.prepared.push(geometry);
223 PreparedId((self.prepared.len() - 1) as u32)
224 }
225
226 pub(crate) fn prepared(&self, id: PreparedId) -> Option<&PreparedGeometry> {
227 self.prepared.get(id.index())
228 }
229
230 pub(crate) fn add_palette(&mut self, colors: Vec<[f32; 4]>) -> PaletteId {
231 self.palettes.push(colors);
232 PaletteId((self.palettes.len() - 1) as u32)
233 }
234
235 pub(crate) fn palette(&self, id: PaletteId) -> Option<&[[f32; 4]]> {
236 self.palettes.get(id.index()).map(Vec::as_slice)
237 }
238
239 pub(crate) fn add_material(&mut self, material: Material) -> MaterialId {
240 self.materials.push(material);
241 MaterialId((self.materials.len() - 1) as u32)
242 }
243
244 pub(crate) fn material(&self, id: MaterialId) -> Option<&Material> {
245 self.materials.get(id.index())
246 }
247
248 pub(crate) fn geometry(&self) -> &[PreparedGeometry] {
249 &self.prepared
250 }
251
252 pub(crate) fn library(&self) -> Library<'_> {
253 Library {
254 geometry: &self.prepared,
255 materials: &self.materials,
256 palettes: &self.palettes,
257 }
258 }
259}
260
261pub struct PublishedView {
262 pub domain: DomainId,
263 pub target: ViewTarget,
264 pub placement: Rigid,
265 pub records: ViewRecords,
266}
267
268#[derive(Default)]
269pub struct Publication {
270 pub views: Vec<PublishedView>,
271 pub stamp: Stamp,
272 pub alpha: f32,
274 source: Option<SceneId>,
275 content: u64,
276}
277
278impl Publication {
279 pub fn content(&self) -> u64 {
281 self.content
282 }
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub enum PublishError {
287 Borrowed,
288 Phase(PhaseError),
289}
290
291impl From<PhaseError> for PublishError {
292 fn from(error: PhaseError) -> Self {
293 Self::Phase(error)
294 }
295}
296
297pub struct Records {
298 idle: Option<Publication>,
299}
300
301impl Default for Records {
302 fn default() -> Self {
303 Self {
304 idle: Some(Publication::default()),
305 }
306 }
307}
308
309impl Records {
310 pub fn publish<A: Stores>(&mut self, session: &mut Session<A>) -> Result<Stamp, PublishError> {
311 self.publish_at(session, 1.0)
312 }
313
314 pub fn publish_at<A: Stores>(
315 &mut self,
316 session: &mut Session<A>,
317 alpha: f32,
318 ) -> Result<Stamp, PublishError> {
319 let buffer = self.idle.as_mut().ok_or(PublishError::Borrowed)?;
320 session.publish_at(buffer, alpha)?;
321 Ok(buffer.stamp)
322 }
323
324 pub fn lend(&mut self) -> Option<Publication> {
326 self.idle.take()
327 }
328
329 pub fn release(&mut self, publication: Publication) {
330 self.idle = Some(publication);
331 }
332}
333
334pub struct SessionSnapshot<A: Stores> {
335 runtime: RuntimeId,
336 app: A::Snapshot,
337 entities: EntitiesSnapshot,
338 domains: Vec<DomainSnapshot>,
339 views: ViewsSnapshot,
340 bridges: RelationSnapshot<Bridge>,
341 tick: Tick,
342 config: SimConfig,
343 next_request: RequestId,
344}
345
346#[derive(Clone, Copy, Debug, PartialEq, Eq)]
347pub enum RestoreError {
348 Pending,
349 Unfinished(Phase),
350 NoInitial,
351 ForeignRuntime,
352 Schema(SchemaId),
353 Domain(DomainId),
354 #[cfg(feature = "physics")]
356 Edit(loam_physics::EditError),
357}
358
359impl std::fmt::Display for RestoreError {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 match self {
362 Self::Pending => f.write_str("deferred mutation is still pending"),
363 Self::Unfinished(phase) => write!(f, "the {phase:?} phase is unfinished"),
364 Self::NoInitial => f.write_str("no initial snapshot was captured"),
365 Self::ForeignRuntime => f.write_str("the snapshot belongs to another runtime"),
366 Self::Schema(schema) => write!(f, "no stored snapshot for {}", schema.name()),
367 Self::Domain(domain) => write!(f, "{domain} refused the snapshot"),
368 #[cfg(feature = "physics")]
369 Self::Edit(error) => std::fmt::Display::fmt(error, f),
370 }
371 }
372}
373
374#[derive(Default)]
375pub(crate) struct Manipulation {
376 drag: Option<Drag>,
377}
378
379impl Manipulation {
380 pub(crate) fn dragging(&self) -> Option<Drag> {
381 self.drag
382 }
383
384 pub(crate) fn grab<A: Stores>(
385 &mut self,
386 domains: &Domains,
387 views: &Views,
388 prepared: &[PreparedGeometry],
389 commands: &mut Commands<A>,
390 ndc: [f32; 2],
391 time: f64,
392 ) -> Result<Pick, DragError> {
393 let pick = domains
394 .pick_lifted(views, prepared, ndc)
395 .ok_or(DragError::NoPick)?;
396 let domain = domains
397 .get(pick.domain)
398 .ok_or(DomainError::UnknownDomain(pick.domain))?;
399 let into = views
400 .to_root(pick.image)
401 .and_then(|to| to.rigid())
402 .ok_or(DomainError::Unsupported("image space"))?
403 .inverse();
404 let center = domain
405 .image_of(pick.view, pick.entity)
406 .ok_or(DomainError::Stale(pick.entity))?;
407 let eye = views
408 .get(views.root())
409 .ok_or(DomainError::Unsupported("image space"))?
410 .eye;
411 let plane = into.apply(pick.image_point);
412 let normal = into.direction(eye.forward);
413 let depth =
414 (Vec3::from(plane) - Vec3::from(into.apply(eye.position))).dot(Vec3::from(normal));
415 let hit = pick.hit.ok_or(DomainError::Unsupported("view ray lift"))?;
416 if let Some(held) = self.drag.take() {
417 commands.submit(Command::Chart(
418 held.domain,
419 ChartCommand::Release {
420 entity: held.entity,
421 },
422 ));
423 }
424 self.drag = Some(Drag {
425 entity: pick.entity,
426 domain: pick.domain,
427 view: pick.view,
428 image: pick.image,
429 plane,
430 normal,
431 depth,
432 center,
433 at: plane,
434 samples: [(plane, time); VELOCITY_SAMPLES],
435 sampled: 1,
436 opened: time,
437 });
438 commands.submit(Command::Chart(
439 pick.domain,
440 ChartCommand::Grab {
441 entity: pick.entity,
442 point: hit,
443 },
444 ));
445 Ok(pick)
446 }
447
448 pub(crate) fn drag<A: Stores>(
449 &mut self,
450 domains: &Domains,
451 views: &Views,
452 commands: &mut Commands<A>,
453 ndc: [f32; 2],
454 time: f64,
455 ) -> Result<ChartPoint, DragError> {
456 let mut drag = self.drag.ok_or(DragError::NotGrabbed)?;
457 let domain = domains
458 .get(drag.domain)
459 .ok_or(DomainError::UnknownDomain(drag.domain))?;
460 let name = domain
461 .view(drag.view)
462 .ok_or(DomainError::UnknownView(drag.view))?
463 .name;
464 let ray = views
465 .ray(drag.image, ndc)
466 .ok_or(DomainError::Unsupported("image space"))?;
467 let eye = views
468 .get(views.root())
469 .ok_or(DomainError::Unsupported("image space"))?
470 .eye;
471 let into = views
472 .to_root(drag.image)
473 .and_then(|to| to.rigid())
474 .ok_or(DomainError::Unsupported("image space"))?
475 .inverse();
476 let forward = into.direction(eye.forward);
477 let plane = drag.plane_at(into.apply(eye.position), forward);
478 let at = Drag::meet(&ray, plane, forward).ok_or(DragError::Ambiguous(name))?;
479 let point = domain.lift_origin(
480 drag.view,
481 &ImageRay {
482 origin: drag.moved(at),
483 direction: drag.normal,
484 },
485 )?;
486 drag.sample(at, time);
487 self.drag = Some(drag);
488 commands.submit(Command::Chart(
489 drag.domain,
490 ChartCommand::Move {
491 entity: drag.entity,
492 point,
493 },
494 ));
495 Ok(point)
496 }
497
498 fn finish<A: Stores>(
499 commands: &mut Commands<A>,
500 drag: Drag,
501 velocity: [f32; 3],
502 ) -> DragRelease {
503 commands.submit(Command::Chart(
504 drag.domain,
505 ChartCommand::Release {
506 entity: drag.entity,
507 },
508 ));
509 drag.released(velocity)
510 }
511
512 pub(crate) fn release<A: Stores>(&mut self, commands: &mut Commands<A>) -> Option<DragRelease> {
513 let drag = self.drag.take()?;
514 Some(Self::finish(commands, drag, drag.velocity()))
515 }
516
517 pub(crate) fn release_at<A: Stores>(
518 &mut self,
519 commands: &mut Commands<A>,
520 time: f64,
521 ) -> Option<DragRelease> {
522 let drag = self.drag.take()?;
523 let age = time - drag.newest_time();
524 let velocity = if (0.0..=RELEASE_STALE_SECONDS).contains(&age) {
525 drag.velocity()
526 } else {
527 [0.0; 3]
528 };
529 Some(Self::finish(commands, drag, velocity))
530 }
531
532 pub(crate) fn cancel<A: Stores>(&mut self, commands: &mut Commands<A>) -> Option<DragRelease> {
533 let drag = self.drag.take()?;
534 Some(Self::finish(commands, drag, [0.0; 3]))
535 }
536
537 pub(crate) fn clear(&mut self) {
538 self.drag = None;
539 }
540}
541
542pub const DOMAIN_STEP: &str = "domain step";
544
545pub struct Session<A: Stores> {
547 pub app: A,
548 domains: Domains,
549 views: Views,
550 bridges: Relation<Bridge>,
551 manipulation: Manipulation,
552 phases: Phases<A>,
553 commands: Commands<A>,
554 batch: Vec<Request<A>>,
555 results: Vec<CommandResult>,
556 input: Input,
557 assets: Assets,
558 config: SimConfig,
559 tick: Tick,
560 sequence: u64,
561 initial: Option<SessionSnapshot<A>>,
562 restored: bool,
563 unfinished: Option<Phase>,
564 phase_error: Option<PhaseError>,
565}
566
567impl<A: Stores> Session<A> {
568 pub fn new(mut app: A, config: SimConfig) -> Self {
569 let scene = SceneId {
570 runtime: RuntimeId::allocate(),
571 epoch: Epoch::default(),
572 };
573 app.bind(scene, Owner::new());
574 let mut phases = Phases::new();
575 phases.push(
576 Phase::Simulation,
577 SystemEntry::new(DOMAIN_STEP, |ctx: Ctx<'_, A>| {
578 for domain in ctx.domains.iter_mut() {
579 domain.step(ctx.step)?;
580 }
581 Ok(())
582 }),
583 );
584 let mut bridges = Relation::new();
585 StoreField::bind(&mut bridges, scene, Owner::new());
586 Self {
587 app,
588 domains: Domains::new(scene.runtime),
589 views: Views::new(),
590 bridges,
591 manipulation: Manipulation::default(),
592 phases,
593 commands: Commands::new(scene),
594 batch: Vec::new(),
595 results: Vec::new(),
596 input: Input::default(),
597 assets: Assets::default(),
598 config,
599 tick: Tick::default(),
600 sequence: 0,
601 initial: None,
602 restored: false,
603 unfinished: None,
604 phase_error: None,
605 }
606 }
607
608 pub fn config(&self) -> SimConfig {
609 self.config
610 }
611
612 pub fn scene(&self) -> SceneId {
613 self.commands.entities().scene()
614 }
615
616 pub fn current_tick(&self) -> Tick {
617 self.tick
618 }
619
620 pub fn faulted_phase(&self) -> Option<Phase> {
621 self.unfinished
622 }
623
624 pub fn phase_error(&self) -> Option<PhaseError> {
625 self.phase_error
626 }
627
628 fn unfinished_error(&self) -> Option<PhaseError> {
629 self.unfinished.map(|phase| {
630 self.phase_error.unwrap_or_else(|| {
631 PhaseError::unnamed(
632 phase,
633 DomainError::Unsupported("unfinished phase requires restore"),
634 )
635 })
636 })
637 }
638
639 pub fn register_domain<S: DomainSpace>(
640 &mut self,
641 builder: DomainBuilder<S>,
642 ) -> DomainHandle<S> {
643 let domain = builder.build(self.domains.next_id(), self.scene());
644 let handle = domain.handle();
645 self.domains.push(Box::new(domain));
646 handle
647 }
648
649 pub fn domains(&self) -> &Domains {
650 &self.domains
651 }
652
653 pub fn domains_mut(&mut self) -> &mut Domains {
654 &mut self.domains
655 }
656
657 pub fn views(&self) -> &Views {
658 &self.views
659 }
660
661 pub fn views_mut(&mut self) -> &mut Views {
662 &mut self.views
663 }
664
665 pub fn entities(&self) -> &Entities {
666 self.commands.entities()
667 }
668
669 pub fn submit(&mut self, command: Command<A>) -> RequestId {
670 self.commands.submit(command)
671 }
672
673 pub fn prepare(&mut self, geometry: PreparedGeometry) -> PreparedId {
674 self.assets.prepare(geometry)
675 }
676
677 pub fn prepared(&self, id: PreparedId) -> Option<&PreparedGeometry> {
678 self.assets.prepared(id)
679 }
680
681 pub fn add_palette(&mut self, colors: Vec<[f32; 4]>) -> PaletteId {
683 self.assets.add_palette(colors)
684 }
685
686 pub fn palette(&self, id: PaletteId) -> Option<&[[f32; 4]]> {
687 self.assets.palette(id)
688 }
689
690 pub fn add_material(&mut self, material: Material) -> MaterialId {
691 self.assets.add_material(material)
692 }
693
694 pub fn material(&self, id: MaterialId) -> Option<&Material> {
695 self.assets.material(id)
696 }
697
698 pub fn system(&mut self, phase: Phase, name: &'static str, system: impl System<A>) {
699 self.phases.push(phase, SystemEntry::new(name, system));
700 }
701
702 pub fn system_at(
704 &mut self,
705 phase: Phase,
706 order: Order,
707 name: &'static str,
708 system: impl System<A>,
709 ) -> Option<()> {
710 self.phases
711 .insert(phase, order, SystemEntry::new(name, system))
712 }
713
714 pub fn entries(&self, phase: Phase) -> &[SystemEntry<A>] {
715 self.phases.entries(phase)
716 }
717
718 pub fn dispatch<R>(&mut self, f: impl FnOnce(&mut Dispatch<'_, A>) -> R) -> R {
719 let result = {
720 let mut dispatch = Dispatch::new(
721 &mut self.app,
722 &mut self.domains,
723 &mut self.views,
724 self.commands.entities_mut(),
725 &mut self.bridges,
726 &mut self.assets,
727 );
728 f(&mut dispatch)
729 };
730 self.domains.synchronize();
731 result
732 }
733
734 pub fn bridges(&self) -> &Relation<Bridge> {
735 &self.bridges
736 }
737
738 pub fn bridge(&mut self, spec: BridgeSpec) -> Result<LinkId, BridgeError> {
739 if self.entities().resolve(spec.anchor).is_none() {
740 return Err(BridgeError::Domain(DomainError::Stale(spec.anchor)));
741 }
742 let summary = {
743 let source = self
744 .domains
745 .get(spec.source)
746 .ok_or(BridgeError::UnknownDomain(spec.source))?;
747 source
748 .view(spec.view)
749 .ok_or(BridgeError::UnknownView(spec.view))?
750 };
751 if spec.placement.rigid().is_none() {
752 return Err(BridgeError::Nonlinear(summary.name));
753 }
754 if self.entities().resolve(summary.eye).is_none() {
755 return Err(BridgeError::Domain(DomainError::Stale(summary.eye)));
756 }
757 let image = self
758 .views
759 .place(spec.into, spec.placement)
760 .ok_or(BridgeError::UnknownImage(spec.into))?;
761 let link = self.bridges.link(
762 self.commands.entities(),
763 spec.anchor,
764 summary.eye,
765 Bridge {
766 source: spec.source,
767 view: spec.view,
768 image,
769 },
770 );
771 let link = match link {
772 Ok(link) => link,
773 Err(error) => {
774 self.views.unplace(image);
775 return Err(BridgeError::Link(error));
776 }
777 };
778 let source = self
779 .domains
780 .facade(spec.source)
781 .ok_or(BridgeError::UnknownDomain(spec.source))?;
782 source.retarget(spec.view, image)?;
783 Ok(link)
784 }
785
786 pub fn dragging(&self) -> Option<Drag> {
787 self.manipulation.dragging()
788 }
789
790 pub fn grab(&mut self, ndc: [f32; 2], time: f64) -> Result<Pick, DragError> {
791 self.manipulation.grab(
792 &self.domains,
793 &self.views,
794 self.assets.geometry(),
795 &mut self.commands,
796 ndc,
797 time,
798 )
799 }
800
801 pub fn drag(&mut self, ndc: [f32; 2], time: f64) -> Result<ChartPoint, DragError> {
802 self.manipulation
803 .drag(&self.domains, &self.views, &mut self.commands, ndc, time)
804 }
805
806 pub fn release(&mut self) -> Option<DragRelease> {
807 self.manipulation.release(&mut self.commands)
808 }
809
810 pub fn release_at(&mut self, time: f64) -> Option<DragRelease> {
811 self.manipulation.release_at(&mut self.commands, time)
812 }
813
814 pub fn cancel_drag(&mut self) -> Option<DragRelease> {
815 self.manipulation.cancel(&mut self.commands)
816 }
817
818 pub fn boundary(&mut self, input: Input) -> Result<Growth, PhaseError> {
819 self.input = input;
820 if let Some(error) = self.unfinished_error() {
821 return Err(error);
822 }
823 self.unfinished = Some(Phase::Dispatch);
824 self.results.clear();
825 let mut growth = Growth::default();
826 self.commit(&mut growth);
827 let step = Step {
828 tick: self.tick,
829 dt: self.config.dt().unwrap_or(0.0),
830 };
831 let mut dispatched = Ok(());
832 for index in 0..self.phases.entries(Phase::Dispatch).len() {
833 if let Err(error) = self.run_entry(Phase::Dispatch, index, step) {
834 dispatched = Err(error);
835 break;
836 }
837 self.commit(&mut growth);
838 }
839 self.restored = false;
840 dispatched?;
841 self.app.boundary(Owner::new());
842 for domain in self.domains.iter_mut() {
843 domain.boundary();
844 }
845 self.unfinished = None;
846 if self.initial.is_none() {
847 match self.snapshot() {
848 Ok(initial) => self.initial = Some(initial),
849 Err(cause) => {
850 let error =
851 PhaseError::system(Phase::Dispatch, "initial", DomainError::Restore(cause));
852 self.unfinished = Some(Phase::Dispatch);
853 self.phase_error = Some(error);
854 return Err(error);
855 }
856 }
857 }
858 Ok(growth)
859 }
860
861 pub fn tick(&mut self) -> Result<(), PhaseError> {
862 if let Some(error) = self.unfinished_error() {
863 return Err(error);
864 }
865 let Some(dt) = self.config.dt() else {
866 return Ok(());
867 };
868 self.unfinished = Some(Phase::Simulation);
869 let step = Step {
870 tick: self.tick,
871 dt,
872 };
873 for domain in self.domains.iter_mut() {
874 domain.begin_tick();
875 }
876 let mut stepped = Ok(());
877 for index in 0..self.phases.entries(Phase::Simulation).len() {
878 stepped = self.run_entry(Phase::Simulation, index, step);
879 if stepped.is_err() {
880 break;
881 }
882 }
883 for domain in self.domains.iter_mut() {
884 domain.end_tick();
885 }
886 stepped?;
887 self.tick = Tick(self.tick.0 + 1);
888 self.unfinished = None;
889 Ok(())
890 }
891
892 pub fn publish(&mut self, into: &mut Publication) -> Result<(), PhaseError> {
893 self.publish_at(into, 1.0)
894 }
895
896 pub fn publish_at(&mut self, into: &mut Publication, alpha: f32) -> Result<(), PhaseError> {
898 if let Some(error) = self.unfinished_error() {
899 return Err(error);
900 }
901 let alpha = presented_alpha(alpha);
902 let step = Step {
903 tick: self.tick,
904 dt: self.config.dt().unwrap_or(0.0),
905 };
906 self.unfinished = Some(Phase::Publication);
907 self.run_phase(Phase::Publication, step)?;
908 self.domains.synchronize();
909 for domain in self.domains.iter_mut() {
910 domain.present(alpha);
911 }
912 let scene = self.scene();
913 let reset = into.source != Some(scene);
914 if reset {
915 *into = Publication::default();
916 }
917 let sequence = self.sequence.wrapping_add(1);
918 let stamp = Stamp {
919 tick: self.tick,
920 sequence,
921 };
922 let extracted = (|| {
923 let library = self.assets.library();
924 let mut changed = reset;
925 let mut count = 0;
926 for domain in self.domains.owned() {
927 for &target in domain.views() {
928 let Some(placement) =
929 self.views.to_root(target.image).and_then(|to| to.rigid())
930 else {
931 continue;
932 };
933 let held = into
934 .views
935 .iter()
936 .position(|view| view.domain == domain.id() && view.target == target);
937 match held {
938 Some(index) if index != count => {
939 into.views.swap(index, count);
940 changed = true;
941 }
942 Some(_) => {}
943 None => {
944 into.views.insert(
945 count,
946 PublishedView {
947 domain: domain.id(),
948 target,
949 placement,
950 records: ViewRecords::default(),
951 },
952 );
953 changed = true;
954 }
955 }
956 let view = &mut into.views[count];
957 changed |= view.placement != placement;
958 view.placement = placement;
959 domain.publish(target.view, library, &mut view.records, stamp, alpha)?;
960 changed |= view.records.built == stamp;
961 count += 1;
962 }
963 }
964 changed |= into.views.len() != count;
965 into.views.truncate(count);
966 into.stamp = stamp;
967 if changed {
968 into.content = sequence;
969 }
970 into.alpha = alpha;
971 Ok::<(), DomainError>(())
972 })();
973 if let Err(error) = extracted {
974 *into = Publication::default();
975 let error = PhaseError::unnamed(Phase::Publication, error);
976 self.phase_error = Some(error);
977 return Err(error);
978 }
979 into.source = Some(scene);
980 self.sequence = sequence;
981 self.unfinished = None;
982 Ok(())
983 }
984
985 fn run_phase(&mut self, phase: Phase, step: Step) -> Result<(), PhaseError> {
986 for index in 0..self.phases.entries(phase).len() {
987 self.run_entry(phase, index, step)?;
988 }
989 Ok(())
990 }
991
992 pub fn take_input(&mut self) -> Input {
994 std::mem::take(&mut self.input)
995 }
996
997 pub fn results(&self) -> &[CommandResult] {
998 &self.results
999 }
1000
1001 pub fn pick(&self, ndc: [f32; 2]) -> Option<Pick> {
1002 self.domains.pick(&self.views, self.assets.geometry(), ndc)
1003 }
1004
1005 pub fn snapshot(&self) -> Result<SessionSnapshot<A>, RestoreError> {
1007 if let Some(phase) = self.unfinished {
1008 return Err(RestoreError::Unfinished(phase));
1009 }
1010 if !self.commands.is_empty() || self.entities().has_reservations() {
1011 return Err(RestoreError::Pending);
1012 }
1013 Ok(SessionSnapshot {
1014 runtime: self.scene().runtime,
1015 app: self.app.snapshot(),
1016 entities: self.entities().snapshot(),
1017 domains: self
1018 .domains
1019 .owned()
1020 .map(|domain| domain.snapshot())
1021 .collect(),
1022 views: self.views.snapshot(),
1023 bridges: StoreField::snapshot(&self.bridges),
1024 tick: self.tick,
1025 config: self.config,
1026 next_request: self.commands.next_request(),
1027 })
1028 }
1029
1030 pub fn restore(&mut self, from: &SessionSnapshot<A>) -> Result<(), RestoreError> {
1032 if from.runtime != self.scene().runtime {
1033 return Err(RestoreError::ForeignRuntime);
1034 }
1035 if from.domains.len() != self.domains.len() {
1036 let first = from.domains.len().min(self.domains.len());
1037 return Err(RestoreError::Domain(DomainId::new(first)));
1038 }
1039 for (domain, snapshot) in self.domains.owned().zip(&from.domains) {
1040 domain.check_restore(snapshot)?;
1041 }
1042 self.commands.cancel_into(&mut self.results);
1043 self.commands.restore(&from.entities, from.next_request);
1044 let scene = self.scene();
1045 for (domain, snapshot) in self.domains.iter_mut().zip(&from.domains) {
1046 if let Err(error) = domain.restore(snapshot, scene) {
1047 self.unfinished = Some(Phase::Dispatch);
1048 self.phase_error = Some(PhaseError::system(
1049 Phase::Dispatch,
1050 "restore",
1051 DomainError::Restore(error),
1052 ));
1053 return Err(error);
1054 }
1055 }
1056 self.app.restore(&from.app, scene, Owner::new());
1057 self.views.restore(&from.views);
1058 StoreField::restore(&mut self.bridges, &from.bridges, scene, Owner::new());
1059 self.manipulation.clear();
1060 self.tick = from.tick;
1061 self.config = from.config;
1062 self.unfinished = None;
1063 self.phase_error = None;
1064 Ok(())
1065 }
1066
1067 pub fn set_initial(&mut self) -> Result<(), RestoreError> {
1068 self.initial = Some(self.snapshot()?);
1069 Ok(())
1070 }
1071
1072 pub fn reset(&mut self) -> Result<(), RestoreError> {
1073 let Some(initial) = self.initial.take() else {
1074 return Err(RestoreError::NoInitial);
1075 };
1076 let result = self.restore(&initial);
1077 self.initial = Some(initial);
1078 self.restored = true;
1079 result
1080 }
1081
1082 fn commit(&mut self, growth: &mut Growth) {
1083 let mut batch = std::mem::take(&mut self.batch);
1084 self.commands.drain_into(&mut batch);
1085 for request in batch.drain(..) {
1086 let despawn = matches!(request.command, Command::Despawn(_));
1087 let name = request.command.name();
1088 let outcome = match request.command {
1089 Command::Reset if self.restored => Ok(Outcome::Done),
1090 Command::Reset => {
1091 let unfinished = self.unfinished;
1092 let phase_error = self.phase_error;
1093 let outcome = self
1094 .reset()
1095 .map(|()| Outcome::Done)
1096 .map_err(Rejection::Restore);
1097 if outcome.is_ok() {
1098 self.unfinished = unfinished;
1099 self.phase_error = phase_error;
1100 }
1101 outcome
1102 }
1103 command => self.dispatch(|dispatch| dispatch.apply(command)),
1104 };
1105 growth.commands += 1;
1106 match outcome {
1107 Ok(Outcome::Spawned(_)) => growth.spawned += 1,
1108 Ok(Outcome::Done) if despawn => growth.despawned += 1,
1109 _ => {}
1110 }
1111 self.results.push(CommandResult {
1112 request: request.id,
1113 name,
1114 outcome,
1115 });
1116 }
1117 self.batch = batch;
1118 }
1119
1120 fn run_entry(&mut self, phase: Phase, index: usize, step: Step) -> Result<(), PhaseError> {
1121 let Session {
1122 app,
1123 domains,
1124 views,
1125 phases,
1126 commands,
1127 results,
1128 input,
1129 assets,
1130 manipulation,
1131 phase_error,
1132 ..
1133 } = self;
1134 let Some(system) = phases.entries_mut(phase).get_mut(index) else {
1135 return Ok(());
1136 };
1137 let name = system.name();
1138 let result = system.run(Ctx {
1139 app,
1140 domains,
1141 views,
1142 commands,
1143 results: results.as_slice(),
1144 input,
1145 prepared: assets.geometry(),
1146 step,
1147 manipulation,
1148 });
1149 domains.synchronize();
1150 result.map_err(|cause| {
1151 let error = PhaseError::system(phase, name, cause);
1152 *phase_error = Some(error);
1153 error
1154 })
1155 }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use crate::view::Vec4;
1161 use loam_math::{EuclideanR4, Space};
1162 use loam_time::alloc::bytes_allocated_by;
1163
1164 use super::*;
1165 use crate::command::SpawnBundle;
1166 use crate::domain::{Instance, Pose};
1167 use crate::entity::Entity;
1168 use crate::store::{LogCapacity, Store};
1169 use crate::view::{DepthEnvelope, DomainRay, ImageRay, ViewMapping, ViewSpec};
1170
1171 crate::stores! {
1172 #[derive(Default)]
1173 pub struct Quiet {}
1174 }
1175
1176 #[test]
1177 fn a_caught_system_unwind_keeps_the_unfinished_phase_blocked() {
1178 let mut session = Session::new(Quiet::default(), SimConfig::default());
1179 session.system(Phase::Simulation, "panic", |_ctx: Ctx<'_, Quiet>| {
1180 panic!("system panic")
1181 });
1182 let unwind = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| session.tick()));
1183 assert!(unwind.is_err());
1184
1185 let mut publication = Publication::default();
1186 assert_eq!(
1187 session.publish(&mut publication),
1188 Err(PhaseError {
1189 phase: Phase::Simulation,
1190 system: None,
1191 cause: DomainError::Unsupported("unfinished phase requires restore"),
1192 })
1193 );
1194 assert_eq!(session.faulted_phase(), Some(Phase::Simulation));
1195 }
1196
1197 fn shaded_segments(
1198 shading: crate::domain::EdgeShading,
1199 at_w: f32,
1200 line_style: Option<(f32, f32)>,
1201 sectioned: bool,
1202 prepare: impl FnOnce(&mut Session<Quiet>) -> (PreparedId, MaterialId),
1203 ) -> Vec<crate::view::SegmentRecord> {
1204 use crate::view::{Eye, Section4, Vec4};
1205
1206 let mut session = Session::new(Quiet::default(), SimConfig::default());
1207 let r4 = session
1208 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1209 let (geometry, material) = prepare(&mut session);
1210 let root = session.views().root();
1211 session
1212 .dispatch(|d| -> Result<(), Rejection> {
1213 let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1214 let mut instance = Instance::new(geometry, material).shaded(shading);
1215 if let Some((width_px, opacity)) = line_style {
1216 instance = instance.line_style(width_px, opacity);
1217 }
1218 if sectioned {
1219 instance = instance.sectioned(material);
1220 }
1221 d.spawn(
1222 SpawnBundle::new()
1223 .at(r4, Pose::at(Vec4::new(0.0, 0.0, -4.0, at_w)))
1224 .instance(instance),
1225 )?;
1226 d.domains
1227 .typed(r4)?
1228 .add_view(ViewSpec::new(root, eye, Section4 { w: at_w }))?;
1229 Ok(())
1230 })
1231 .expect("the view registered");
1232 session.views_mut().root_mut().eye = Eye::default();
1233 let mut records = Records::default();
1234 records.publish(&mut session).expect("published");
1235 let publication = records.lend().expect("the buffer is free");
1236 publication.views[0].records.segments().to_vec()
1237 }
1238
1239 #[test]
1240 fn the_tesseract_cut_at_w_zero_publishes_the_square_caps_of_its_six_straddling_cells() {
1241 use crate::view::{Eye, Section4, Vec4, ViewId};
1242
1243 const SIZE: f32 = 0.7;
1244 const LIFT: f32 = 0.2;
1245 let mut session = Session::new(Quiet::default(), SimConfig::default());
1246 let r4 = session
1247 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1248 let geometry = session.prepare(PreparedGeometry::Polytope4 {
1249 polytope: loam_shape::polytope::Polytope4::Tesseract,
1250 scale: SIZE,
1251 });
1252 let body = session.add_material(Material::lines([1.0; 4], 1.0));
1253 let cut = session.add_material(Material::lines([1.0, 0.85, 0.35, 1.0], 2.0));
1254 let root = session.views().root();
1255 let view = session
1256 .dispatch(|d| -> Result<ViewId, Rejection> {
1257 let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1258 d.spawn(
1259 SpawnBundle::new()
1260 .at(r4, Pose::at(Vec4::new(0.0, 0.0, -4.0, 0.0)))
1261 .instance(Instance::new(geometry, body).sectioned(cut)),
1262 )?;
1263 d.domains
1264 .typed(r4)?
1265 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
1266 .map_err(Rejection::from)
1267 })
1268 .expect("the view registered");
1269 session.views_mut().root_mut().eye = Eye::default();
1270
1271 let mut records = Records::default();
1272 let read = |session: &mut Session<Quiet>, records: &mut Records| {
1273 records.publish(session).expect("published");
1274 let publication = records.lend().expect("the buffer is free");
1275 let view = &publication.views[0];
1276 let read = (
1277 view.records.triangles().len(),
1278 view.records.segments().len(),
1279 view.records.built(),
1280 );
1281 records.release(publication);
1282 read
1283 };
1284
1285 let (triangles, segments, built) = read(&mut session, &mut records);
1286 assert_eq!(
1287 triangles, 24,
1288 "the six cells that straddle w = 0 each cut to a square, and each square fans around its centroid into four triangles, not {triangles}"
1289 );
1290 assert_eq!(
1291 segments,
1292 32 + 24,
1293 "the wireframe's 32 edges and the cut cube's 24 perimeter segments came to {segments}"
1294 );
1295 assert_eq!(
1296 read(&mut session, &mut records).2,
1297 built,
1298 "an unchanged source and view rebuilt the section"
1299 );
1300
1301 session
1302 .dispatch(|d| -> Result<(), Rejection> {
1303 d.domains
1304 .typed(r4)?
1305 .view_mut(view)
1306 .ok_or(Rejection::Unsupported("view"))?
1307 .set_mapping(Section4 { w: LIFT });
1308 Ok(())
1309 })
1310 .expect("the slice moved");
1311 let (lifted, _, moved) = read(&mut session, &mut records);
1312 assert_ne!(
1313 moved, built,
1314 "a new slice left the section at its old build"
1315 );
1316 assert_eq!(
1317 lifted, 24,
1318 "the cut at w = {LIFT} still meets six cells, not {lifted}"
1319 );
1320 }
1321
1322 #[test]
1323 fn a_depth_shaded_segment_reads_its_color_from_the_endpoints_own_w_not_the_bodys() {
1324 use crate::domain::EdgeShading;
1325
1326 const EXTENT: f32 = 0.5;
1327 const BACK: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
1328 const FRONT: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
1329 let shading = EdgeShading::Depth {
1330 back: BACK,
1331 front: FRONT,
1332 extent: EXTENT,
1333 };
1334 let prepare = |session: &mut Session<Quiet>| {
1335 (
1336 session.prepare(PreparedGeometry::Lines4 {
1337 segments: vec![[[0.0; 4], [0.0, 0.0, 0.0, EXTENT]]],
1338 }),
1339 session.add_material(Material::lines([1.0; 4], 1.0)),
1340 )
1341 };
1342
1343 for lifted in [0.0, 3.0] {
1344 let segments = shaded_segments(shading, lifted, None, false, prepare);
1345 assert_eq!(
1346 segments[0].start_color,
1347 [0.5, 0.0, 0.5, 1.0],
1348 "the midpoint of the depth ramp is wrong for a body at w {lifted}"
1349 );
1350 assert_eq!(
1351 segments[0].end_color, FRONT,
1352 "the far endpoint of the depth ramp is wrong for a body at w {lifted}"
1353 );
1354 }
1355 }
1356
1357 #[test]
1358 fn a_palette_colors_each_segment_by_its_prepared_index_and_no_shading_keeps_the_material() {
1359 use crate::domain::EdgeShading;
1360
1361 const COLORS: [[f32; 4]; 4] = [
1362 [1.0, 0.0, 0.0, 1.0],
1363 [1.0, 1.0, 0.0, 1.0],
1364 [0.0, 1.0, 0.0, 1.0],
1365 [0.0, 1.0, 1.0, 1.0],
1366 ];
1367 const MATERIAL: [f32; 4] = [0.25, 0.5, 0.75, 1.0];
1368 let two_edges = vec![
1369 [[0.0; 4], [0.5, 0.0, 0.0, 0.0]],
1370 [[0.0; 4], [0.0, 0.5, 0.0, 0.0]],
1371 ];
1372
1373 let painted = shaded_segments(EdgeShading::Material, 0.0, None, false, |session| {
1374 (
1375 session.prepare(PreparedGeometry::Lines4 {
1376 segments: two_edges.clone(),
1377 }),
1378 session.add_material(Material::lines(MATERIAL, 1.0)),
1379 )
1380 });
1381 assert_eq!(
1382 painted.iter().map(|s| s.start_color).collect::<Vec<_>>(),
1383 [MATERIAL, MATERIAL],
1384 "an instance with no shading lost the material's line color"
1385 );
1386
1387 let mut id = None;
1388 let painted = shaded_segments(
1389 EdgeShading::Palette(PaletteId(0)),
1390 0.0,
1391 None,
1392 false,
1393 |session: &mut Session<Quiet>| {
1394 id = Some(session.add_palette(COLORS.to_vec()));
1395 (
1396 session.prepare(PreparedGeometry::Lines4 {
1397 segments: two_edges,
1398 }),
1399 session.add_material(Material::lines(MATERIAL, 1.0)),
1400 )
1401 },
1402 );
1403 assert_eq!(id, Some(PaletteId(0)));
1404 assert_eq!(
1405 painted
1406 .iter()
1407 .flat_map(|s| [s.start_color, s.end_color])
1408 .collect::<Vec<_>>(),
1409 COLORS,
1410 "the palette did not follow the prepared segment endpoints in order"
1411 );
1412 }
1413
1414 #[test]
1415 fn an_instance_line_style_replaces_the_material_width_and_opacity() {
1416 let [segment] = shaded_segments(
1417 crate::domain::EdgeShading::Material,
1418 0.0,
1419 Some((2.5, 0.4)),
1420 false,
1421 |session| {
1422 (
1423 session.prepare(PreparedGeometry::Lines4 {
1424 segments: vec![[[0.0; 4], [0.5, 0.0, 0.0, 0.0]]],
1425 }),
1426 session.add_material(Material::lines([0.2, 0.4, 0.6, 0.8], 1.0)),
1427 )
1428 },
1429 )[..] else {
1430 panic!("the line did not publish as one segment");
1431 };
1432 assert_eq!(segment.width_px, 2.5);
1433 assert_eq!(segment.start_color, [0.2, 0.4, 0.6, 0.4]);
1434 assert_eq!(segment.end_color, [0.2, 0.4, 0.6, 0.4]);
1435
1436 let sectioned = shaded_segments(
1437 crate::domain::EdgeShading::Material,
1438 0.0,
1439 Some((2.5, 0.4)),
1440 true,
1441 |session| {
1442 (
1443 session.prepare(PreparedGeometry::Polytope4 {
1444 polytope: loam_shape::polytope::Polytope4::Tesseract,
1445 scale: 0.7,
1446 }),
1447 session.add_material(Material::lines([0.2, 0.4, 0.6, 0.8], 1.0)),
1448 )
1449 },
1450 );
1451 assert!(sectioned.len() > 32, "the section edge did not publish");
1452 assert!(sectioned[..sectioned.len() - 32].iter().all(|segment| {
1453 segment.width_px == 2.5
1454 && segment.start_color == [0.2, 0.4, 0.6, 0.4]
1455 && segment.end_color == [0.2, 0.4, 0.6, 0.4]
1456 }));
1457 }
1458
1459 #[test]
1460 fn a_grab_takes_the_view_it_can_lift_while_a_plain_pick_keeps_the_nearer_one() {
1461 use crate::view::{Eye, Projection4, Section4, Vec4, ViewId};
1462 use loam_math::EuclideanR4;
1463
1464 const DEPTH: f32 = 4.0;
1465 const FOCAL: f32 = 2.5;
1466 const AT_W: f32 = -2.5;
1467
1468 let mut session = Session::new(Quiet::default(), SimConfig::default());
1469 let r4 = session
1470 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1471 let edges = session.prepare(PreparedGeometry::Lines4 {
1472 segments: vec![[[0.5, 0.0, 0.0, 0.0], [-0.5, 0.0, 0.0, 0.0]]],
1473 });
1474 let white = session.add_material(Material::lines([1.0; 4], 1.0));
1475 let root = session.views().root();
1476 let (section, projection) = session
1477 .dispatch(|d| -> Result<(ViewId, ViewId), Rejection> {
1478 let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1479 d.spawn(
1480 SpawnBundle::new()
1481 .at(r4, Pose::at(Vec4::new(0.0, 0.0, -DEPTH, AT_W)))
1482 .instance(Instance::new(edges, white)),
1483 )?;
1484 let domain = d.domains.typed(r4)?;
1485 let section = domain.add_view(ViewSpec::new(root, eye, Section4 { w: AT_W }))?;
1486 let projection =
1487 domain.add_view(ViewSpec::new(root, eye, Projection4 { focal: FOCAL }))?;
1488 Ok((section, projection))
1489 })
1490 .expect("the two views registered");
1491 session.views_mut().root_mut().eye = Eye::default();
1492
1493 let picked = session
1494 .pick([0.0, 0.0])
1495 .expect("both views cover the origin");
1496 assert_eq!(
1497 picked.view, projection,
1498 "the plain pick lost the nearer projection layer"
1499 );
1500 let grabbed = session.grab([0.0, 0.0], 0.0).expect("the section lifts");
1501 assert_eq!(
1502 grabbed.view, section,
1503 "the grab resolved to a view it cannot lift, so a drag is a coin flip"
1504 );
1505 }
1506
1507 #[test]
1508 fn a_section_pick_passes_through_a_body_the_slice_misses() {
1509 use crate::view::{Eye, Section4};
1510 use loam_shape::polytope::Polytope4;
1511
1512 const SCALE: f32 = 0.45;
1513
1514 let mut session = Session::new(Quiet::default(), SimConfig::default());
1515 let r4 = session
1516 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1517 let tesseract = session.prepare(PreparedGeometry::Polytope4 {
1518 polytope: Polytope4::Tesseract,
1519 scale: SCALE,
1520 });
1521 let white = session.add_material(Material::lines([1.0; 4], 1.0));
1522 let root = session.views().root();
1523 let cut = session
1524 .dispatch(|d| -> Result<Entity, Rejection> {
1525 let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1526 d.spawn(
1527 SpawnBundle::new()
1528 .at(r4, Pose::at(Vec4::new(0.0, 0.0, -3.0, 0.3)))
1529 .instance(Instance::new(tesseract, white)),
1530 )?;
1531 let cut = d.spawn(
1532 SpawnBundle::new()
1533 .at(r4, Pose::at(Vec4::new(0.0, 0.0, -5.0, 0.0)))
1534 .instance(Instance::new(tesseract, white)),
1535 )?;
1536 d.domains
1537 .typed(r4)?
1538 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))?;
1539 Ok(cut)
1540 })
1541 .expect("the scene built");
1542 session.views_mut().root_mut().eye = Eye::default();
1543
1544 let picked = session
1545 .pick([0.0, 0.0])
1546 .expect("the slice cuts the far tesseract");
1547 assert_eq!(
1548 picked.entity, cut,
1549 "the pick took the near tesseract, whose section is empty"
1550 );
1551 let face = picked.hit.expect("a section pick lifts").coordinates[2];
1552 assert!(
1553 (face - (-5.0 + 0.5 * SCALE)).abs() < 1e-4,
1554 "the hit is at z = {face}, off the tesseract's face"
1555 );
1556 }
1557
1558 crate::stores! {
1559 #[derive(Default)]
1560 pub struct Churn {
1561 counters: Store<u32>,
1562 pool: Value<Vec<Entity>>,
1563 }
1564 }
1565
1566 #[test]
1567 fn warmed_tick_and_boundary_allocate_nothing_outside_reported_growth() {
1568 let mut session = Session::new(Churn::default(), SimConfig::default());
1569 let r4 = session
1570 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1571 let pool = session.dispatch(|d| {
1572 (0..8)
1573 .map(|value| {
1574 d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)).row(value))
1575 .unwrap()
1576 })
1577 .collect()
1578 });
1579 session.app.pool.set(pool);
1580 session.system(Phase::Simulation, "churn", |ctx: Ctx<'_, Churn>| {
1581 for (_, counter) in ctx.app.counters.iter_mut() {
1582 *counter += 1;
1583 }
1584 let pool = ctx.app.pool.get_mut();
1585 let retired = pool.swap_remove(0);
1586 ctx.commands.submit(Command::Despawn(retired));
1587 let fresh = ctx.commands.spawn(SpawnBundle::new()).unwrap();
1588 pool.push(fresh.entity);
1589 Ok(())
1590 });
1591 let cycle = |session: &mut Session<Churn>| {
1592 session.tick().unwrap();
1593 session.boundary(Input::default()).unwrap()
1594 };
1595 for _ in 0..16 {
1596 cycle(&mut session);
1597 }
1598
1599 let bytes = bytes_allocated_by(|| {
1600 for _ in 0..16 {
1601 let growth = cycle(&mut session);
1602 assert_eq!(
1603 growth,
1604 Growth {
1605 commands: 2,
1606 spawned: 1,
1607 despawned: 1,
1608 }
1609 );
1610 }
1611 let quiet = session.boundary(Input::default()).unwrap();
1612 assert_eq!(quiet, Growth::default());
1613 })
1614 .expect("the counting allocator is installed");
1615 assert_eq!(
1616 bytes, 0,
1617 "16 warmed cycles asked the allocator for {bytes} bytes"
1618 );
1619 assert_eq!(session.entities().len(), 8);
1620 }
1621
1622 crate::stores! {
1623 #[derive(Default)]
1624 pub struct Tagged {
1625 tags: Store<u32>,
1626 }
1627 }
1628
1629 #[test]
1630 fn a_warmed_restore_allocates_beyond_the_rows_it_copies() {
1631 use crate::domain::{Field, FieldKind};
1632 use crate::field::FieldOp;
1633 use crate::view::Vec4;
1634
1635 let mut session = Session::new(Tagged::default(), SimConfig::default());
1636 let r4 = session.register_domain(
1637 DomainBuilder::new("r4", EuclideanR4)
1638 .tracked(LogCapacity::default())
1639 .fields(),
1640 );
1641 session.dispatch(|d| {
1642 let at = |x: f32| SpawnBundle::new().at(r4, Pose::at(Vec4::new(x, 0.0, 0.0, 0.0)));
1643 let left = d.spawn(at(-1.0).row(1u32)).unwrap();
1644 let right = d.spawn(at(1.0).row(2u32)).unwrap();
1645 let union = d.spawn(at(0.0).row(3u32)).unwrap();
1646 for operand in [left, right] {
1647 d.attach_field(
1648 r4,
1649 operand,
1650 Field {
1651 kind: FieldKind::ExactDistance,
1652 op: FieldOp::HyperSphere { radius: 1.0 },
1653 operands: Vec::new(),
1654 },
1655 )
1656 .unwrap();
1657 }
1658 d.attach_field(
1659 r4,
1660 union,
1661 Field {
1662 kind: FieldKind::ExactDistance,
1663 op: FieldOp::Union,
1664 operands: vec![left, right],
1665 },
1666 )
1667 .unwrap();
1668 });
1669 let snapshot = session.snapshot().unwrap();
1670 for _ in 0..8 {
1671 session.restore(&snapshot).unwrap();
1672 }
1673
1674 let bytes = bytes_allocated_by(|| {
1675 session.restore(&snapshot).unwrap();
1676 })
1677 .expect("the counting allocator is installed");
1678 let copied = (size_of::<Entity>() * 2) as u64;
1679 assert_eq!(
1680 bytes, copied,
1681 "a warmed restore of three rows asked the allocator for {bytes} bytes, not the {copied} its operand list copies"
1682 );
1683 session
1684 .domains_mut()
1685 .facade(r4.id())
1686 .unwrap()
1687 .compile_fields()
1688 .expect("the restored operands still name live rows");
1689 }
1690
1691 crate::stores! {
1692 pub struct Shown {
1693 scores: Store<u32>,
1694 }
1695 }
1696
1697 struct Flat;
1698
1699 impl ViewMapping<EuclideanR4> for Flat {
1700 fn name(&self) -> &'static str {
1701 "flat"
1702 }
1703
1704 fn image_point(
1705 &self,
1706 eye: &Pose<EuclideanR4>,
1707 point: <EuclideanR4 as Space>::Point,
1708 ) -> Option<[f32; 3]> {
1709 let relative = point - eye.point;
1710 Some([relative.x, relative.y, relative.z])
1711 }
1712
1713 fn lift(
1714 &self,
1715 _eye: &Pose<EuclideanR4>,
1716 _ray: &ImageRay,
1717 ) -> Option<DomainRay<EuclideanR4>> {
1718 None
1719 }
1720
1721 fn depth_envelope(&self) -> DepthEnvelope {
1722 DepthEnvelope {
1723 near: 0.0,
1724 far: 1.0,
1725 }
1726 }
1727 }
1728
1729 #[test]
1730 fn a_foreign_publication_buffer_does_not_restamp_the_previous_sessions_records() {
1731 let build = |score: u32, offset: f32| {
1732 let shown = Shown {
1733 scores: Store::tracked(LogCapacity::default()),
1734 };
1735 let mut session = Session::new(shown, SimConfig::default());
1736 let r4 = session.register_domain(
1737 DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()),
1738 );
1739 let geometry = session.prepare(PreparedGeometry::Lines4 {
1740 segments: vec![[[offset, 0.0, 0.0, 0.0], [offset + 1.0, 0.0, 0.0, 0.0]]],
1741 });
1742 let material = session.add_material(Material::flat([1.0; 4]));
1743 let root = session.views().root();
1744 session.dispatch(|dispatch| {
1745 let eye = dispatch
1746 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
1747 .unwrap();
1748 dispatch
1749 .spawn(
1750 SpawnBundle::new()
1751 .at(r4, Pose::at(Vec4::ZERO))
1752 .instance(Instance::new(geometry, material))
1753 .row(score),
1754 )
1755 .unwrap();
1756 dispatch
1757 .domains
1758 .typed(r4)
1759 .unwrap()
1760 .add_view(ViewSpec::new(root, eye, Flat))
1761 .unwrap();
1762 });
1763 session
1764 };
1765 let mut first = build(11, 0.0);
1766 let mut second = build(22, 4.0);
1767 let mut publication = Publication::default();
1768
1769 first.publish(&mut publication).unwrap();
1770 assert_eq!(publication.views[0].records.segments()[0].start[0], 0.0);
1771 second.publish(&mut publication).unwrap();
1772 assert_eq!(publication.views[0].records.segments()[0].start[0], 4.0);
1773 }
1774
1775 #[test]
1776 fn warmed_publish_allocates() {
1777 let shown = Shown {
1778 scores: Store::tracked(LogCapacity::default()),
1779 };
1780 let mut session = Session::new(shown, SimConfig::default());
1781 let r4 = session
1782 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1783 let geometry = session.prepare(PreparedGeometry::Lines4 {
1784 segments: Vec::new(),
1785 });
1786 let material = session.add_material(Material::flat([1.0; 4]));
1787 let root = session.views().root();
1788 session.dispatch(|d| {
1789 let eye = d
1790 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
1791 .unwrap();
1792 d.domains
1793 .typed(r4)
1794 .unwrap()
1795 .add_view(ViewSpec::new(root, eye, Flat))
1796 .unwrap();
1797 for value in 0..8 {
1798 d.spawn(
1799 SpawnBundle::new()
1800 .at(r4, Pose::at(Vec4::ZERO))
1801 .instance(Instance::new(geometry, material))
1802 .row(value),
1803 )
1804 .unwrap();
1805 }
1806 });
1807 session.system(Phase::Simulation, "churn", move |ctx: Ctx<'_, Shown>| {
1808 let domain = ctx.domains.typed(r4).unwrap();
1809 for (entity, score) in ctx.app.scores.iter_mut() {
1810 *score += 1;
1811 let mut point = domain.poses().get(entity).unwrap().point;
1812 point.x += ctx.step.dt;
1813 domain.set_point(entity, point).unwrap();
1814 }
1815 Ok(())
1816 });
1817 let mut publication = Publication::default();
1818 let cycle = |session: &mut Session<Shown>, publication: &mut Publication| {
1819 session.tick().unwrap();
1820 session.boundary(Input::default()).unwrap();
1821 session.publish(publication).unwrap();
1822 };
1823 for _ in 0..4 {
1824 cycle(&mut session, &mut publication);
1825 }
1826
1827 let bytes = bytes_allocated_by(|| {
1828 for _ in 0..16 {
1829 cycle(&mut session, &mut publication);
1830 }
1831 })
1832 .expect("the counting allocator is installed");
1833 assert_eq!(
1834 bytes, 0,
1835 "16 warmed publishes asked the allocator for {bytes} bytes"
1836 );
1837 assert_eq!(publication.views[0].records.instances.rows().len(), 8);
1838 }
1839}