1use std::any::Any;
2use std::fmt;
3use std::marker::PhantomData;
4use std::ops::Mul;
5
6pub use loam_shape::field::FieldKind;
7
8use loam_shape::field::DistanceField;
9
10use loam_math::blended::{
11 geodesic_log_checked, integrate_geodesic_frame_checked, integrate_geodesic_frame_to_checked,
12 BlendedSpace, BlendingField, ConformallyFlat, GeodesicError, GEODESIC_DEFAULT_STEPS,
13 GEODESIC_ERROR_BUDGET, LOG_MAX_ITERS, LOG_RESIDUAL_TOL,
14};
15use loam_math::hyperbolic::{in_poincare_ball, poincare_to_hyperboloid};
16use loam_math::{
17 Bivector, EuclideanR3, EuclideanR4, HyperbolicH3, Iso3, Iso3H, Iso4Flat, IsometryGroup, Mat3,
18 Rotor, Rotor4, Space, WPlane,
19};
20use loam_shape::polytope::{
21 polytope_section_faces_append, polytope_section_perimeter_append, Polytope4,
22};
23
24use crate::command::{Outcome, Rejection};
25use crate::entity::{Entity, RuntimeId, SceneId};
26use crate::field::{
27 self, FieldCompiler, FieldCost, FieldError, FieldNode, FieldOp, FieldPrimitive,
28};
29use crate::phase::Step;
30use crate::session::{
31 Library, MaterialId, PaletteId, PreparedGeometry, PreparedId, RestoreError, Stamp,
32};
33use crate::store::{
34 Change, LogCapacity, Owner, Store, StoreError, StoreField, StoreSnapshot, Version,
35};
36use crate::view::{
37 self, DomainRay, EntityOutput, ImageRay, ImageSpaceId, InstanceRecord, Pick, RefusalSource,
38 Rigid, SegmentRecord, TriangleRecord, Vec3, Vec4, ViewId, ViewMapping, ViewRecords,
39 ViewRefusals, ViewSpec, ViewStyle, ViewSummary, ViewTarget, Views,
40};
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub struct DomainId(u32);
44
45impl DomainId {
46 pub(crate) fn new(index: usize) -> Self {
47 Self(index as u32)
48 }
49
50 pub fn index(self) -> usize {
51 self.0 as usize
52 }
53}
54
55impl fmt::Display for DomainId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "domain {}", self.0)
58 }
59}
60
61pub struct DomainHandle<S> {
62 id: DomainId,
63 runtime: RuntimeId,
64 space: PhantomData<fn() -> S>,
65}
66
67impl<S> Clone for DomainHandle<S> {
68 fn clone(&self) -> Self {
69 *self
70 }
71}
72
73impl<S> Copy for DomainHandle<S> {}
74
75impl<S> fmt::Debug for DomainHandle<S> {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.debug_struct("DomainHandle")
78 .field("id", &self.id)
79 .field("runtime", &self.runtime)
80 .finish()
81 }
82}
83
84impl<S> DomainHandle<S> {
85 pub fn id(self) -> DomainId {
86 self.id
87 }
88
89 pub fn runtime(self) -> RuntimeId {
90 self.runtime
91 }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95pub struct ChartId(pub u16);
96
97#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct ChartPose {
100 pub chart: ChartId,
101 pub coordinates: [f32; 4],
102 pub frame: [[f32; 4]; 4],
104}
105
106#[derive(Clone, Copy, Debug, PartialEq)]
107pub struct ChartTangent {
108 pub chart: ChartId,
109 pub vector: [f32; 4],
110}
111
112#[derive(Clone, Copy, Debug, PartialEq)]
113pub struct ChartPoint {
114 pub chart: ChartId,
115 pub coordinates: [f32; 4],
116}
117
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub enum ChartCommand {
120 Place {
121 entity: Entity,
122 pose: ChartPose,
123 },
124 Attach {
125 entity: Entity,
126 instance: Instance,
127 },
128 Walk {
129 entity: Entity,
130 tangent: ChartTangent,
131 dt: f32,
132 },
133 Move {
134 entity: Entity,
135 point: ChartPoint,
136 },
137 Grab {
138 entity: Entity,
139 point: ChartPoint,
140 },
141 Release {
142 entity: Entity,
143 },
144}
145
146impl ChartCommand {
147 pub(crate) fn entity(&self) -> Entity {
148 match *self {
149 Self::Place { entity, .. }
150 | Self::Attach { entity, .. }
151 | Self::Walk { entity, .. }
152 | Self::Move { entity, .. }
153 | Self::Grab { entity, .. }
154 | Self::Release { entity } => entity,
155 }
156 }
157}
158
159#[derive(Clone, Copy, Debug, PartialEq)]
160pub struct Instance {
161 pub geometry: PreparedId,
162 pub material: MaterialId,
163 pub shading: EdgeShading,
164 pub line_width_px: Option<f32>,
165 pub line_opacity: Option<f32>,
166 pub section: Option<MaterialId>,
168}
169
170impl Instance {
171 pub fn new(geometry: PreparedId, material: MaterialId) -> Self {
172 Self {
173 geometry,
174 material,
175 shading: EdgeShading::Material,
176 line_width_px: None,
177 line_opacity: None,
178 section: None,
179 }
180 }
181
182 pub fn shaded(mut self, shading: EdgeShading) -> Self {
183 self.shading = shading;
184 self
185 }
186
187 pub fn line_style(mut self, width_px: f32, opacity: f32) -> Self {
188 self.line_width_px = Some(width_px);
189 self.line_opacity = Some(opacity);
190 self
191 }
192
193 pub fn sectioned(mut self, perimeter: MaterialId) -> Self {
194 self.section = Some(perimeter);
195 self
196 }
197}
198
199#[derive(Clone, Copy, Debug, Default, PartialEq)]
200pub enum EdgeShading {
201 #[default]
202 Material,
203 Palette(PaletteId),
204 Depth {
205 back: [f32; 4],
206 front: [f32; 4],
207 extent: f32,
208 },
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum DomainError {
213 ForeignRuntime,
214 UnknownDomain(DomainId),
215 UnknownDomainName(&'static str),
216 AmbiguousDomainName(&'static str),
217 UnknownView(ViewId),
218 SpaceMismatch(DomainId),
219 Stale(Entity),
220 Store(StoreError),
221 InvalidCoordinate(&'static str),
222 InvalidFrame,
223 ChartBoundary,
224 NoConvergence,
225 ErrorBudget,
226 #[cfg(feature = "physics")]
227 Physics(loam_physics::EditError),
228 Unsupported(&'static str),
229 FieldCycle(Entity),
230 FieldArity(Entity),
231 Restore(RestoreError),
232}
233
234impl fmt::Display for DomainError {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 match self {
237 Self::ForeignRuntime => f.write_str("the handle belongs to another runtime"),
238 Self::UnknownDomain(domain) => write!(f, "no {domain} in this session"),
239 Self::UnknownDomainName(name) => write!(f, "no domain named {name}"),
240 Self::AmbiguousDomainName(name) => {
241 write!(f, "more than one domain named {name}")
242 }
243 Self::UnknownView(view) => write!(f, "no {view} in this session"),
244 Self::SpaceMismatch(domain) => write!(f, "{domain} has another space"),
245 Self::Stale(entity) => write!(f, "{entity} is stale"),
246 Self::Store(error) => fmt::Display::fmt(error, f),
247 Self::InvalidCoordinate(coordinate) => write!(f, "invalid {coordinate}"),
248 Self::InvalidFrame => f.write_str("the frame is not orthonormal"),
249 Self::ChartBoundary => f.write_str("the step left the chart"),
250 Self::NoConvergence => f.write_str("the transport did not converge"),
251 Self::ErrorBudget => f.write_str("the metric error budget was exceeded"),
252 #[cfg(feature = "physics")]
253 Self::Physics(error) => fmt::Display::fmt(error, f),
254 Self::Unsupported(what) => write!(f, "unsupported: {what}"),
255 Self::FieldCycle(entity) => write!(f, "{entity} closes a field cycle"),
256 Self::FieldArity(entity) => write!(f, "{entity} has the wrong operand count"),
257 Self::Restore(error) => fmt::Display::fmt(error, f),
258 }
259 }
260}
261
262#[cfg(feature = "physics")]
263impl From<loam_physics::EditError> for DomainError {
264 fn from(error: loam_physics::EditError) -> Self {
265 Self::Physics(error)
266 }
267}
268
269impl From<StoreError> for DomainError {
270 fn from(error: StoreError) -> Self {
271 match error {
272 StoreError::Stale(entity) => Self::Stale(entity),
273 error => Self::Store(error),
274 }
275 }
276}
277
278pub trait DomainSpace:
279 Space<Vector: Mul<f32, Output = <Self as Space>::Vector>> + Send + Sync + Sized + 'static
280{
281 type Placement: Copy + Send + Sync + 'static;
282
283 type Relative: Copy + Send + Sync + 'static;
284
285 const BLENDS: bool = true;
287
288 fn origin(&self) -> Self::Point;
289
290 fn check(&self, point: Self::Point) -> Result<(), DomainError>;
291
292 fn chart_dimension(&self) -> u32;
293
294 fn chart_point(&self, point: Self::Point) -> ChartPoint;
295
296 fn local_point(&self, coordinates: [f32; 4]) -> Self::Point;
298
299 fn point_from_chart(&self, point: &ChartPoint) -> Result<Self::Point, DomainError> {
300 single_chart(point.chart)?;
301 finite(point.coordinates)?;
302 let point = self.local_point(point.coordinates);
303 self.check(point)?;
304 Ok(point)
305 }
306
307 fn chart_pose(&self, pose: &Pose<Self>) -> ChartPose;
308
309 fn blend(&self, from: &Pose<Self>, to: &Pose<Self>, t: f32) -> Pose<Self>;
311
312 fn pose_from_chart(&self, pose: &ChartPose) -> Result<Pose<Self>, DomainError>;
313
314 fn tangent_from_chart(&self, tangent: &ChartTangent) -> Result<Self::Vector, DomainError>;
315
316 fn chart_reach(&self, at: Self::Point, radius: f32) -> f32;
317
318 fn hit_ball(&self, ray: &DomainRay<Self>, center: Self::Point, radius: f32) -> Option<f32>;
320
321 fn prepare(&self, pose: &Pose<Self>) -> Self::Placement;
322
323 fn place(
324 &self,
325 placement: &Self::Placement,
326 local: Self::Point,
327 ) -> Result<Self::Point, DomainError>;
328
329 fn local(&self, pose: &Pose<Self>, point: Self::Point) -> Result<Self::Point, DomainError>;
330
331 fn relative(&self, eye: &Pose<Self>, pose: &Pose<Self>) -> Result<Self::Relative, DomainError>;
332
333 fn place_relative(
334 &self,
335 relative: &Self::Relative,
336 local: Self::Point,
337 ) -> Result<Self::Point, DomainError>;
338
339 fn carry(
340 &self,
341 pose: &Pose<Self>,
342 local: Self::Point,
343 tangent: Self::Vector,
344 ) -> Result<Self::Vector, DomainError>;
345
346 fn moved(&self, pose: &Pose<Self>, to: Self::Point) -> Result<Pose<Self>, DomainError>;
347
348 fn walk(
349 &self,
350 pose: &Pose<Self>,
351 tangent: Self::Vector,
352 dt: f32,
353 ) -> Result<Pose<Self>, DomainError> {
354 let step = self.carry(pose, self.origin(), tangent * dt)?;
355 let to = self.exp(pose.point, step);
356 self.check(to)?;
357 self.moved(pose, to)
358 }
359}
360
361pub trait Homogeneous: DomainSpace + IsometryGroup {
362 fn iso_of(&self, pose: &Pose<Self>) -> Self::Iso;
363
364 fn pose_of(&self, iso: Self::Iso) -> Pose<Self>;
365
366 fn transvection(&self, to: Self::Point) -> Self::Iso;
367}
368
369pub fn homogeneous_place<S: Homogeneous>(
370 space: &S,
371 placement: &S::Iso,
372 local: S::Point,
373) -> Result<S::Point, DomainError> {
374 let point = space.iso_apply(*placement, local);
375 space.check(point)?;
376 Ok(point)
377}
378
379pub fn homogeneous_local<S: Homogeneous>(
380 space: &S,
381 pose: &Pose<S>,
382 point: S::Point,
383) -> Result<S::Point, DomainError> {
384 Ok(space.iso_apply(space.iso_inverse(space.iso_of(pose)), point))
385}
386
387pub fn homogeneous_carry<S: Homogeneous>(
388 space: &S,
389 pose: &Pose<S>,
390 local: S::Point,
391 tangent: S::Vector,
392) -> Result<S::Vector, DomainError> {
393 Ok(space.iso_transport(space.iso_of(pose), local, tangent))
394}
395
396pub fn homogeneous_relative<S: Homogeneous>(
397 space: &S,
398 eye: &Pose<S>,
399 pose: &Pose<S>,
400) -> Result<S::Iso, DomainError> {
401 Ok(space.iso_compose(space.iso_inverse(space.iso_of(eye)), space.iso_of(pose)))
402}
403
404macro_rules! homogeneous_capabilities {
405 () => {
406 type Placement = <Self as IsometryGroup>::Iso;
407
408 type Relative = <Self as IsometryGroup>::Iso;
409
410 fn prepare(&self, pose: &Pose<Self>) -> Self::Placement {
411 self.iso_of(pose)
412 }
413
414 fn place(
415 &self,
416 placement: &Self::Placement,
417 local: Self::Point,
418 ) -> Result<Self::Point, DomainError> {
419 homogeneous_place(self, placement, local)
420 }
421
422 fn local(&self, pose: &Pose<Self>, point: Self::Point) -> Result<Self::Point, DomainError> {
423 homogeneous_local(self, pose, point)
424 }
425
426 fn relative(
427 &self,
428 eye: &Pose<Self>,
429 pose: &Pose<Self>,
430 ) -> Result<Self::Relative, DomainError> {
431 homogeneous_relative(self, eye, pose)
432 }
433
434 fn place_relative(
435 &self,
436 relative: &Self::Relative,
437 local: Self::Point,
438 ) -> Result<Self::Point, DomainError> {
439 homogeneous_place(self, relative, local)
440 }
441
442 fn carry(
443 &self,
444 pose: &Pose<Self>,
445 local: Self::Point,
446 tangent: Self::Vector,
447 ) -> Result<Self::Vector, DomainError> {
448 homogeneous_carry(self, pose, local, tangent)
449 }
450
451 fn moved(&self, pose: &Pose<Self>, to: Self::Point) -> Result<Pose<Self>, DomainError> {
452 homogeneous_moved(self, pose, to)
453 }
454
455 fn walk(
456 &self,
457 pose: &Pose<Self>,
458 tangent: Self::Vector,
459 dt: f32,
460 ) -> Result<Pose<Self>, DomainError> {
461 homogeneous_walk(self, pose, tangent, dt)
462 }
463 };
464}
465
466pub fn homogeneous_moved<S: Homogeneous>(
467 space: &S,
468 pose: &Pose<S>,
469 to: S::Point,
470) -> Result<Pose<S>, DomainError> {
471 space.check(to)?;
472 let pose_iso = space.iso_of(pose);
473 let local = space.iso_apply(space.iso_inverse(pose_iso), to);
474 let next = space.pose_of(space.iso_compose(pose_iso, space.transvection(local)));
475 space.check(next.point)?;
476 Ok(next)
477}
478
479pub fn homogeneous_walk<S: Homogeneous>(
480 space: &S,
481 pose: &Pose<S>,
482 tangent: S::Vector,
483 dt: f32,
484) -> Result<Pose<S>, DomainError> {
485 let step = space.exp(space.origin(), tangent * dt);
486 space.check(step)?;
487 let next = space.pose_of(space.iso_compose(space.iso_of(pose), space.transvection(step)));
489 space.check(next.point)?;
490 Ok(next)
491}
492
493const FRAME_TOLERANCE: f32 = 1e-3;
494
495fn single_chart(chart: ChartId) -> Result<(), DomainError> {
496 (chart == ChartId(0))
497 .then_some(())
498 .ok_or(DomainError::Unsupported("chart id"))
499}
500
501fn finite(values: [f32; 4]) -> Result<(), DomainError> {
502 for (value, axis) in values.into_iter().zip(["x", "y", "z", "w"]) {
503 if !value.is_finite() {
504 return Err(DomainError::InvalidCoordinate(axis));
505 }
506 }
507 Ok(())
508}
509
510fn put_row<T>(store: &mut Store<T>, entity: Entity, row: T) -> Result<(), Rejection> {
511 match store.get_mut(entity) {
512 Some(slot) => *slot = row,
513 None => store.insert_raw(entity, row)?,
514 }
515 Ok(())
516}
517
518fn frame_columns(frame: &[[f32; 4]; 4]) -> Result<[Vec3; 3], DomainError> {
519 let columns = [0, 1, 2].map(|i| Vec3::new(frame[i][0], frame[i][1], frame[i][2]));
520 let [x, y, z] = columns;
521 let unit = |v: Vec3| v.is_finite() && (v.length_squared() - 1.0).abs() <= FRAME_TOLERANCE;
522 let orthogonal = |a: Vec3, b: Vec3| a.dot(b).abs() <= FRAME_TOLERANCE;
523 let valid = columns.iter().all(|column| unit(*column))
524 && orthogonal(x, y)
525 && orthogonal(y, z)
526 && orthogonal(z, x)
527 && x.cross(y).dot(z) > 0.0;
528 valid.then_some(columns).ok_or(DomainError::InvalidFrame)
529}
530
531fn frame_of(columns: [Vec3; 3]) -> [[f32; 4]; 4] {
532 let mut frame = [[0.0; 4]; 4];
533 for (slot, column) in frame.iter_mut().zip(columns) {
534 *slot = column.extend(0.0).to_array();
535 }
536 frame[3] = [0.0, 0.0, 0.0, 1.0];
537 frame
538}
539
540fn rotation_of([x, y, z]: [Vec3; 3]) -> Iso3 {
542 let mut pose = Iso3::IDENTITY;
543 let q = &mut pose.rotation;
544 let trace = x.x + y.y + z.z;
545 if trace > 0.0 {
546 let s = (trace + 1.0).sqrt() * 2.0;
547 (q.w, q.x, q.y, q.z) = (s * 0.25, (y.z - z.y) / s, (z.x - x.z) / s, (x.y - y.x) / s);
548 } else if x.x > y.y && x.x > z.z {
549 let s = (1.0 + x.x - y.y - z.z).sqrt() * 2.0;
550 (q.w, q.x, q.y, q.z) = ((y.z - z.y) / s, s * 0.25, (y.x + x.y) / s, (z.x + x.z) / s);
551 } else if y.y > z.z {
552 let s = (1.0 + y.y - x.x - z.z).sqrt() * 2.0;
553 (q.w, q.x, q.y, q.z) = ((z.x - x.z) / s, (y.x + x.y) / s, s * 0.25, (z.y + y.z) / s);
554 } else {
555 let s = (1.0 + z.z - x.x - y.y).sqrt() * 2.0;
556 (q.w, q.x, q.y, q.z) = ((x.y - y.x) / s, (z.x + x.z) / s, (z.y + y.z) / s, s * 0.25);
557 }
558 pose.rotation = pose.rotation.normalize();
559 pose
560}
561
562fn lorentz(a: Vec4, b: Vec4) -> f32 {
563 a.x * b.x + a.y * b.y + a.z * b.z - a.w * b.w
564}
565
566impl DomainSpace for EuclideanR4 {
567 homogeneous_capabilities!();
568
569 fn origin(&self) -> Self::Point {
570 Vec4::ZERO
571 }
572
573 fn check(&self, point: Self::Point) -> Result<(), DomainError> {
574 finite(point.to_array())
575 }
576
577 fn chart_dimension(&self) -> u32 {
578 4
579 }
580
581 fn chart_point(&self, point: Self::Point) -> ChartPoint {
582 ChartPoint {
583 chart: ChartId(0),
584 coordinates: point.to_array(),
585 }
586 }
587
588 fn local_point(&self, coordinates: [f32; 4]) -> Self::Point {
589 Vec4::from_array(coordinates)
590 }
591
592 fn blend(&self, from: &Pose<Self>, to: &Pose<Self>, t: f32) -> Pose<Self> {
594 let relative = from.frame.inverse() * to.frame;
595 Pose {
596 point: from.point.lerp(to.point, t),
597 frame: from.frame * (relative.log() * t).exp(),
598 }
599 }
600
601 fn chart_pose(&self, pose: &Pose<Self>) -> ChartPose {
602 ChartPose {
603 chart: ChartId(0),
604 coordinates: pose.point.to_array(),
605 frame: pose.frame.to_mat4(),
606 }
607 }
608
609 fn pose_from_chart(&self, pose: &ChartPose) -> Result<Pose<Self>, DomainError> {
610 single_chart(pose.chart)?;
611 finite(pose.coordinates)?;
612 let frame = Rotor4::from_mat4(&pose.frame).ok_or(DomainError::InvalidFrame)?;
613 Ok(Pose {
614 point: Vec4::from_array(pose.coordinates),
615 frame,
616 })
617 }
618
619 fn tangent_from_chart(&self, tangent: &ChartTangent) -> Result<Self::Vector, DomainError> {
620 single_chart(tangent.chart)?;
621 finite(tangent.vector)?;
622 Ok(Vec4::from_array(tangent.vector))
623 }
624
625 fn chart_reach(&self, _at: Self::Point, radius: f32) -> f32 {
626 radius
627 }
628
629 fn hit_ball(&self, ray: &DomainRay<Self>, center: Self::Point, radius: f32) -> Option<f32> {
630 let offset = ray.origin - center;
631 view::ball_entry(
632 offset.dot(ray.direction),
633 offset.length_squared() - radius * radius,
634 )
635 }
636}
637
638impl Homogeneous for EuclideanR4 {
639 fn iso_of(&self, pose: &Pose<Self>) -> Self::Iso {
640 Iso4Flat {
641 rotation: pose.frame,
642 translation: pose.point,
643 }
644 }
645
646 fn pose_of(&self, iso: Self::Iso) -> Pose<Self> {
647 Pose {
648 point: iso.translation,
649 frame: iso.rotation,
650 }
651 }
652
653 fn transvection(&self, to: Self::Point) -> Self::Iso {
654 Iso4Flat::from_translation(to)
655 }
656}
657
658impl From<Iso4Flat> for Pose<EuclideanR4> {
659 fn from(iso: Iso4Flat) -> Self {
660 EuclideanR4.pose_of(iso)
661 }
662}
663
664impl DomainSpace for HyperbolicH3 {
665 homogeneous_capabilities!();
666
667 const BLENDS: bool = false;
668
669 fn origin(&self) -> Self::Point {
670 Vec3::ZERO
671 }
672
673 fn check(&self, point: Self::Point) -> Result<(), DomainError> {
674 finite(point.extend(0.0).to_array())?;
675 if !in_poincare_ball(point) {
676 return Err(DomainError::ChartBoundary);
677 }
678 self.valid_point(point)
679 .then_some(())
680 .ok_or(DomainError::ErrorBudget)
681 }
682
683 fn chart_dimension(&self) -> u32 {
684 3
685 }
686
687 fn chart_point(&self, point: Self::Point) -> ChartPoint {
688 ChartPoint {
689 chart: ChartId(0),
690 coordinates: point.extend(0.0).to_array(),
691 }
692 }
693
694 fn local_point(&self, coordinates: [f32; 4]) -> Self::Point {
695 Vec3::from_slice(&coordinates[..3])
696 }
697
698 fn blend(&self, _from: &Pose<Self>, to: &Pose<Self>, _t: f32) -> Pose<Self> {
700 *to
701 }
702
703 fn chart_pose(&self, pose: &Pose<Self>) -> ChartPose {
704 let position = pose.point;
705 let rotation = self.iso_compose(
706 self.iso_inverse(Iso3H::from_translation(position)),
707 pose.frame,
708 );
709 let matrix = rotation.matrix;
710 ChartPose {
711 chart: ChartId(0),
712 coordinates: position.extend(0.0).to_array(),
713 frame: frame_of([
714 matrix.x_axis.truncate(),
715 matrix.y_axis.truncate(),
716 matrix.z_axis.truncate(),
717 ]),
718 }
719 }
720
721 fn pose_from_chart(&self, pose: &ChartPose) -> Result<Pose<Self>, DomainError> {
722 single_chart(pose.chart)?;
723 finite(pose.coordinates)?;
724 let position = Vec3::from_slice(&pose.coordinates[..3]);
725 self.check(position)?;
726 let [x, y, z] = frame_columns(&pose.frame)?;
727 let mut rotation = Iso3H::IDENTITY;
728 rotation.matrix.x_axis = x.extend(0.0);
729 rotation.matrix.y_axis = y.extend(0.0);
730 rotation.matrix.z_axis = z.extend(0.0);
731 Ok(self.pose_of(self.iso_compose(Iso3H::from_translation(position), rotation)))
732 }
733
734 fn tangent_from_chart(&self, tangent: &ChartTangent) -> Result<Self::Vector, DomainError> {
735 single_chart(tangent.chart)?;
736 finite(tangent.vector)?;
737 Ok(Vec3::from_slice(&tangent.vector[..3]))
738 }
739
740 fn chart_reach(&self, _at: Self::Point, radius: f32) -> f32 {
741 2.0 * radius
742 .min(loam_math::hyperbolic::POINCARE_R2_MAX.sqrt())
743 .atanh()
744 }
745
746 fn hit_ball(&self, ray: &DomainRay<Self>, center: Self::Point, radius: f32) -> Option<f32> {
747 let start = poincare_to_hyperboloid(ray.origin);
749 let ahead = poincare_to_hyperboloid(self.exp(ray.origin, ray.direction));
750 let tangent = (ahead - start * 1.0_f32.cosh()) / 1.0_f32.sinh();
751 let target = poincare_to_hyperboloid(center);
752 let reach = -lorentz(start, target);
753 let along = -lorentz(tangent, target);
754 let nearest = (reach * reach - along * along).max(1.0).sqrt();
755 let foot = -(along / reach).atanh();
756 let ratio = radius.cosh() / nearest;
757 if ratio < 1.0 {
758 return None;
759 }
760 let half = ratio.acosh();
761 let entry = foot - half;
762 if entry <= 0.0 {
763 return (foot + half >= 0.0).then_some(0.0);
764 }
765 Some(entry)
766 }
767}
768
769impl Homogeneous for HyperbolicH3 {
770 fn iso_of(&self, pose: &Pose<Self>) -> Self::Iso {
771 pose.frame
772 }
773
774 fn pose_of(&self, iso: Self::Iso) -> Pose<Self> {
775 Pose {
776 point: self.iso_apply(iso, Vec3::ZERO),
777 frame: iso,
778 }
779 }
780
781 fn transvection(&self, to: Self::Point) -> Self::Iso {
782 Iso3H::from_translation(to)
783 }
784}
785
786impl From<Iso3H> for Pose<HyperbolicH3> {
787 fn from(iso: Iso3H) -> Self {
788 HyperbolicH3.pose_of(iso)
789 }
790}
791
792impl DomainSpace for EuclideanR3 {
793 homogeneous_capabilities!();
794
795 fn origin(&self) -> Self::Point {
796 Vec3::ZERO
797 }
798
799 fn check(&self, point: Self::Point) -> Result<(), DomainError> {
800 finite(point.extend(0.0).to_array())
801 }
802
803 fn chart_dimension(&self) -> u32 {
804 3
805 }
806
807 fn chart_point(&self, point: Self::Point) -> ChartPoint {
808 ChartPoint {
809 chart: ChartId(0),
810 coordinates: point.extend(0.0).to_array(),
811 }
812 }
813
814 fn local_point(&self, coordinates: [f32; 4]) -> Self::Point {
815 Vec3::from_slice(&coordinates[..3])
816 }
817
818 fn blend(&self, from: &Pose<Self>, to: &Pose<Self>, t: f32) -> Pose<Self> {
819 Pose {
820 point: from.point.lerp(to.point, t),
821 frame: from.frame.slerp(to.frame, t),
822 }
823 }
824
825 fn chart_pose(&self, pose: &Pose<Self>) -> ChartPose {
826 ChartPose {
827 chart: ChartId(0),
828 coordinates: pose.point.extend(0.0).to_array(),
829 frame: frame_of([Vec3::X, Vec3::Y, Vec3::Z].map(|axis| pose.frame * axis)),
830 }
831 }
832
833 fn pose_from_chart(&self, pose: &ChartPose) -> Result<Pose<Self>, DomainError> {
834 single_chart(pose.chart)?;
835 finite(pose.coordinates)?;
836 let frame = rotation_of(frame_columns(&pose.frame)?).rotation;
837 Ok(Pose {
838 point: Vec3::from_slice(&pose.coordinates[..3]),
839 frame,
840 })
841 }
842
843 fn tangent_from_chart(&self, tangent: &ChartTangent) -> Result<Self::Vector, DomainError> {
844 single_chart(tangent.chart)?;
845 finite(tangent.vector)?;
846 Ok(Vec3::from_slice(&tangent.vector[..3]))
847 }
848
849 fn chart_reach(&self, _at: Self::Point, radius: f32) -> f32 {
850 radius
851 }
852
853 fn hit_ball(&self, ray: &DomainRay<Self>, center: Self::Point, radius: f32) -> Option<f32> {
854 let offset = ray.origin - center;
855 view::ball_entry(
856 offset.dot(ray.direction),
857 offset.length_squared() - radius * radius,
858 )
859 }
860}
861
862impl Homogeneous for EuclideanR3 {
863 fn iso_of(&self, pose: &Pose<Self>) -> Self::Iso {
864 Iso3 {
865 rotation: pose.frame,
866 translation: pose.point,
867 }
868 }
869
870 fn pose_of(&self, iso: Self::Iso) -> Pose<Self> {
871 Pose {
872 point: iso.translation,
873 frame: iso.rotation,
874 }
875 }
876
877 fn transvection(&self, to: Self::Point) -> Self::Iso {
878 Iso3::from_translation(to)
879 }
880}
881
882impl From<Iso3> for Pose<EuclideanR3> {
883 fn from(iso: Iso3) -> Self {
884 EuclideanR3.pose_of(iso)
885 }
886}
887
888fn geodesic_error(error: GeodesicError) -> DomainError {
889 match error {
890 GeodesicError::NonFinite => DomainError::InvalidCoordinate("geodesic"),
891 GeodesicError::ChartBoundary => DomainError::ChartBoundary,
892 GeodesicError::ErrorBudget { .. } => DomainError::ErrorBudget,
893 GeodesicError::InvalidStepCount
894 | GeodesicError::InvalidErrorBudget
895 | GeodesicError::Singular
896 | GeodesicError::NoConvergence => DomainError::NoConvergence,
897 }
898}
899
900impl<A, B, F> DomainSpace for BlendedSpace<A, B, F>
901where
902 A: Space<Point = Vec3, Vector = Vec3> + ConformallyFlat + Send + Sync + 'static,
903 B: Space<Point = Vec3, Vector = Vec3> + ConformallyFlat + Send + Sync + 'static,
904 F: BlendingField,
905{
906 type Placement = Pose<Self>;
907
908 type Relative = (Pose<Self>, Pose<Self>);
909
910 const BLENDS: bool = false;
911
912 fn origin(&self) -> Self::Point {
913 Vec3::ZERO
914 }
915
916 fn check(&self, point: Self::Point) -> Result<(), DomainError> {
917 finite(point.extend(0.0).to_array())?;
918 self.valid_point(point)
919 .then_some(())
920 .ok_or(DomainError::ChartBoundary)
921 }
922
923 fn chart_dimension(&self) -> u32 {
924 3
925 }
926
927 fn chart_point(&self, point: Self::Point) -> ChartPoint {
928 ChartPoint {
929 chart: ChartId(0),
930 coordinates: point.extend(0.0).to_array(),
931 }
932 }
933
934 fn local_point(&self, coordinates: [f32; 4]) -> Self::Point {
935 Vec3::from_slice(&coordinates[..3])
936 }
937
938 fn blend(&self, _from: &Pose<Self>, to: &Pose<Self>, _t: f32) -> Pose<Self> {
940 *to
941 }
942
943 fn chart_pose(&self, pose: &Pose<Self>) -> ChartPose {
944 let relative = self.frame_at(pose.point).inverse() * pose.frame;
945 ChartPose {
946 chart: ChartId(0),
947 coordinates: pose.point.extend(0.0).to_array(),
948 frame: frame_of([relative.x_axis, relative.y_axis, relative.z_axis]),
949 }
950 }
951
952 fn pose_from_chart(&self, pose: &ChartPose) -> Result<Pose<Self>, DomainError> {
953 single_chart(pose.chart)?;
954 finite(pose.coordinates)?;
955 let point = Vec3::from_slice(&pose.coordinates[..3]);
956 self.check(point)?;
957 let [x, y, z] = frame_columns(&pose.frame)?;
958 let frame = self.frame_at(point) * Mat3::from_cols(x, y, z);
959 Ok(Pose { point, frame })
960 }
961
962 fn tangent_from_chart(&self, tangent: &ChartTangent) -> Result<Self::Vector, DomainError> {
963 single_chart(tangent.chart)?;
964 finite(tangent.vector)?;
965 Ok(Vec3::from_slice(&tangent.vector[..3]))
966 }
967
968 fn chart_reach(&self, at: Self::Point, radius: f32) -> f32 {
969 self.conformal_factor(at).sqrt() * radius
970 }
971
972 fn hit_ball(&self, ray: &DomainRay<Self>, center: Self::Point, radius: f32) -> Option<f32> {
973 let unit = ray.direction.try_normalize()?;
974 let chart_radius = radius / self.conformal_factor(center).sqrt();
975 let offset = ray.origin - center;
976 let entry = view::ball_entry(
977 offset.dot(unit),
978 offset.length_squared() - chart_radius * chart_radius,
979 )?;
980 Some(entry / ray.direction.length())
981 }
982
983 fn prepare(&self, pose: &Pose<Self>) -> Self::Placement {
984 *pose
985 }
986
987 fn place(
988 &self,
989 placement: &Self::Placement,
990 local: Self::Point,
991 ) -> Result<Self::Point, DomainError> {
992 self.walk(placement, local, 1.0).map(|pose| pose.point)
993 }
994
995 fn local(&self, pose: &Pose<Self>, point: Self::Point) -> Result<Self::Point, DomainError> {
996 self.check(point)?;
997 if self.conformal_factor(point).sqrt() * LOG_RESIDUAL_TOL > GEODESIC_ERROR_BUDGET {
998 return Err(DomainError::ErrorBudget);
999 }
1000 let chart = geodesic_log_checked(
1001 self,
1002 pose.point,
1003 point,
1004 GEODESIC_DEFAULT_STEPS,
1005 LOG_MAX_ITERS,
1006 GEODESIC_ERROR_BUDGET,
1007 )
1008 .map_err(geodesic_error)?
1009 .vector;
1010 let inverse = pose.frame.inverse();
1011 if !inverse.is_finite() {
1012 return Err(DomainError::InvalidFrame);
1013 }
1014 Ok(inverse * chart)
1015 }
1016
1017 fn relative(&self, eye: &Pose<Self>, pose: &Pose<Self>) -> Result<Self::Relative, DomainError> {
1018 Ok((*eye, *pose))
1019 }
1020
1021 fn place_relative(
1022 &self,
1023 relative: &Self::Relative,
1024 local: Self::Point,
1025 ) -> Result<Self::Point, DomainError> {
1026 let (eye, pose) = relative;
1027 self.local(eye, self.place(pose, local)?)
1028 }
1029
1030 fn carry(
1031 &self,
1032 pose: &Pose<Self>,
1033 local: Self::Point,
1034 tangent: Self::Vector,
1035 ) -> Result<Self::Vector, DomainError> {
1036 let state = integrate_geodesic_frame_checked(
1037 self,
1038 pose.point,
1039 pose.frame * local,
1040 pose.frame,
1041 GEODESIC_DEFAULT_STEPS,
1042 GEODESIC_ERROR_BUDGET,
1043 )
1044 .map_err(geodesic_error)?;
1045 Ok(state.frame * tangent)
1046 }
1047
1048 fn moved(&self, pose: &Pose<Self>, to: Self::Point) -> Result<Pose<Self>, DomainError> {
1049 self.check(to)?;
1050 let state = integrate_geodesic_frame_to_checked(
1051 self,
1052 pose.point,
1053 to,
1054 pose.frame,
1055 GEODESIC_DEFAULT_STEPS,
1056 LOG_MAX_ITERS,
1057 GEODESIC_ERROR_BUDGET,
1058 )
1059 .map_err(geodesic_error)?;
1060 Ok(Pose {
1061 point: state.point,
1062 frame: state.frame,
1063 })
1064 }
1065
1066 fn walk(
1067 &self,
1068 pose: &Pose<Self>,
1069 tangent: Self::Vector,
1070 dt: f32,
1071 ) -> Result<Pose<Self>, DomainError> {
1072 let state = integrate_geodesic_frame_checked(
1073 self,
1074 pose.point,
1075 pose.frame * (tangent * dt),
1076 pose.frame,
1077 GEODESIC_DEFAULT_STEPS,
1078 GEODESIC_ERROR_BUDGET,
1079 )
1080 .map_err(geodesic_error)?;
1081 Ok(Pose {
1082 point: state.point,
1083 frame: state.frame,
1084 })
1085 }
1086}
1087
1088fn lerp_color(back: [f32; 4], front: [f32; 4], t: f32) -> [f32; 4] {
1089 let mut mixed = [0.0; 4];
1090 for (channel, value) in mixed.iter_mut().enumerate() {
1091 *value = back[channel] + (front[channel] - back[channel]) * t;
1092 }
1093 mixed
1094}
1095
1096fn polytope_entry<S: DomainSpace>(
1098 space: &S,
1099 ray: &DomainRay<S>,
1100 pose: &Pose<S>,
1101 polytope: Polytope4,
1102 scale: f32,
1103) -> Option<f32> {
1104 let canonical = |point| {
1105 let local = space.local(pose, point).ok()?;
1106 Some(Vec4::from_array(space.chart_point(local).coordinates) / scale)
1107 };
1108 let origin = canonical(ray.origin)?;
1109 let direction = canonical(space.exp(ray.origin, ray.direction))? - origin;
1110 let topology = polytope.topology();
1111 let mut entry = 0.0f32;
1112 let mut exit = f32::INFINITY;
1113 for cell in topology.cells {
1114 let centroid = cell
1115 .iter()
1116 .map(|&index| topology.vertices[index as usize])
1117 .sum::<Vec4>()
1118 / cell.len() as f32;
1119 let offset = centroid.length();
1120 let normal = centroid / offset;
1121 let along = normal.dot(direction);
1122 let gap = offset - normal.dot(origin);
1123 if along == 0.0 {
1124 if gap < 0.0 {
1125 return None;
1126 }
1127 continue;
1128 }
1129 let t = gap / along;
1130 if along < 0.0 {
1131 entry = entry.max(t);
1132 } else {
1133 exit = exit.min(t);
1134 }
1135 if entry > exit {
1136 return None;
1137 }
1138 }
1139 Some(entry)
1140}
1141
1142struct SegmentProjection<'a, S: DomainSpace> {
1143 space: &'a S,
1144 mapping: &'a dyn ViewMapping<S>,
1145 eye: &'a Pose<S>,
1146 pose: &'a Pose<S>,
1147 relative: &'a S::Relative,
1148}
1149
1150fn push_segments<S: DomainSpace>(
1151 projection: SegmentProjection<'_, S>,
1152 library: &Library<'_>,
1153 instance: &Instance,
1154 entity: Entity,
1155 segments: &mut Vec<SegmentRecord>,
1156 refusals: &mut ViewRefusals,
1157) {
1158 let SegmentProjection {
1159 space,
1160 mapping,
1161 eye,
1162 pose,
1163 relative,
1164 } = projection;
1165 let Some(geometry) = library.geometry.get(instance.geometry.index()) else {
1166 return;
1167 };
1168 let (color, material_width_px) = library.line_style(instance.material);
1169 let width_px = instance.line_width_px.unwrap_or(material_width_px);
1170 let opacity = instance.line_opacity;
1171 let palette = match instance.shading {
1172 EdgeShading::Palette(id) => library.palette(id),
1173 _ => &[],
1174 };
1175 let placement = space.prepare(pose);
1176 let origin = match space.place(&placement, space.origin()) {
1177 Ok(origin) => origin,
1178 Err(error) => {
1179 refusals.record(entity, error, RefusalSource::Segment);
1180 return;
1181 }
1182 };
1183 let origin_depth = space.chart_point(origin).coordinates[3];
1184 let depth_color = |point: S::Point| match instance.shading {
1185 EdgeShading::Depth {
1186 back,
1187 front,
1188 extent,
1189 } => {
1190 let depth = space.chart_point(point).coordinates[3] - origin_depth;
1191 Some(lerp_color(
1192 back,
1193 front,
1194 (depth / extent.max(1e-6) * 0.5 + 0.5).clamp(0.0, 1.0),
1195 ))
1196 }
1197 _ => None,
1198 };
1199 let mut push = |index: usize, a: [f32; 4], b: [f32; 4]| {
1200 let start = space.local_point(a);
1201 let end = space.local_point(b);
1202 let start_point = match space.place(&placement, start) {
1203 Ok(point) => point,
1204 Err(error) => {
1205 refusals.record(entity, error, RefusalSource::Segment);
1206 return;
1207 }
1208 };
1209 let end_point = match space.place(&placement, end) {
1210 Ok(point) => point,
1211 Err(error) => {
1212 refusals.record(entity, error, RefusalSource::Segment);
1213 return;
1214 }
1215 };
1216 let painted = |at: usize| palette.get(at).copied().unwrap_or(color);
1217 let with_opacity = |mut color: [f32; 4]| {
1218 if let Some(opacity) = opacity {
1219 color[3] = opacity;
1220 }
1221 color
1222 };
1223 let start_color =
1224 with_opacity(depth_color(start_point).unwrap_or_else(|| painted(index * 2)));
1225 let end_color =
1226 with_opacity(depth_color(end_point).unwrap_or_else(|| painted(index * 2 + 1)));
1227 let mut previous = None;
1228 mapping.image_segment(space, eye, pose, relative, [start, end], &mut |t, point| {
1229 let Some(point) = point else {
1230 previous = None;
1231 return;
1232 };
1233 if let Some((previous_t, previous_point)) = previous {
1234 segments.push(SegmentRecord {
1235 start: previous_point,
1236 _pad0: 0.0,
1237 end: point,
1238 _pad1: 0.0,
1239 start_color: lerp_color(start_color, end_color, previous_t),
1240 end_color: lerp_color(start_color, end_color, t),
1241 width_px,
1242 _pad2: [0.0; 3],
1243 });
1244 }
1245 previous = Some((t, point));
1246 });
1247 };
1248 match geometry {
1249 PreparedGeometry::Lines4 { segments } => {
1250 for (index, &[a, b]) in segments.iter().enumerate() {
1251 push(index, a, b);
1252 }
1253 }
1254 PreparedGeometry::Lines3 { segments } => {
1255 for (index, &[a, b]) in segments.iter().enumerate() {
1256 push(index, [a[0], a[1], a[2], 0.0], [b[0], b[1], b[2], 0.0]);
1257 }
1258 }
1259 PreparedGeometry::Polytope4 { polytope, scale } => {
1260 let topology = polytope.topology();
1261 for (index, &[a, b]) in topology.edges.iter().enumerate() {
1262 push(
1263 index,
1264 (topology.vertices[a as usize] * *scale).to_array(),
1265 (topology.vertices[b as usize] * *scale).to_array(),
1266 );
1267 }
1268 }
1269 }
1270}
1271
1272struct ViewEntry<S: DomainSpace> {
1273 eye: Entity,
1274 subject: Option<Entity>,
1275 style: ViewStyle<S>,
1276}
1277
1278fn same_style<S: DomainSpace>(a: &ViewStyle<S>, b: &ViewStyle<S>) -> bool {
1279 let ViewStyle {
1280 enabled,
1281 edges,
1282 section_edges,
1283 section_faces,
1284 mapping,
1285 } = a;
1286 let ViewStyle {
1287 enabled: other_enabled,
1288 edges: other_edges,
1289 section_edges: other_section_edges,
1290 section_faces: other_section_faces,
1291 mapping: other_mapping,
1292 } = b;
1293 enabled == other_enabled
1294 && edges == other_edges
1295 && section_edges == other_section_edges
1296 && section_faces == other_section_faces
1297 && std::sync::Arc::ptr_eq(mapping, other_mapping)
1298}
1299
1300impl<S: DomainSpace> Clone for ViewEntry<S> {
1301 fn clone(&self) -> Self {
1302 Self {
1303 eye: self.eye,
1304 subject: self.subject,
1305 style: self.style.clone(),
1306 }
1307 }
1308}
1309
1310struct ViewProjection<'a, S: DomainSpace> {
1311 space: &'a S,
1312 spec: &'a ViewEntry<S>,
1313 eye: &'a Pose<S>,
1314}
1315
1316impl<'a, S: DomainSpace> ViewProjection<'a, S> {
1317 fn push_section(
1318 &self,
1319 pose: &Pose<S>,
1320 library: &Library<'_>,
1321 instance: &Instance,
1322 scratch: &mut view::SectionScratchpad,
1323 segments: &mut Vec<SegmentRecord>,
1324 triangles: &mut Vec<TriangleRecord>,
1325 ) -> Result<(), DomainError> {
1326 let space = self.space;
1327 let spec = self.spec;
1328 let eye = self.eye;
1329 let mapping = spec.style.mapping.as_ref();
1330 let Some(PreparedGeometry::Polytope4 { polytope, scale }) =
1331 library.geometry.get(instance.geometry.index())
1332 else {
1333 return Ok(());
1334 };
1335 let (Some(perimeter_material), Some(cut)) = (instance.section, mapping.section(eye, pose))
1336 else {
1337 return Ok(());
1338 };
1339 let relative = space.relative(eye, pose)?;
1340 let Some(place) = mapping.image_local(space, eye, pose, &relative, space.origin()) else {
1341 return Ok(());
1342 };
1343 let origin = space.place_relative(&relative, space.origin())?;
1344 let base = Vec4::from(space.chart_point(origin).coordinates);
1345 let topology = polytope.topology();
1346 scratch.rotated.clear();
1347 for vertex in topology.vertices.iter() {
1348 let local = space.local_point((*vertex * *scale).to_array());
1349 let placed = match space.place_relative(&relative, local) {
1350 Ok(placed) => placed,
1351 Err(error) => {
1352 scratch.rotated.clear();
1353 return Err(error);
1354 }
1355 };
1356 scratch
1357 .rotated
1358 .push(Vec4::from(space.chart_point(placed).coordinates) - base);
1359 }
1360 let plane = WPlane::new(cut.offset);
1361 let placed = |point: [f32; 3]| {
1362 [
1363 point[0] * cut.scale + place[0],
1364 point[1] * cut.scale + place[1],
1365 point[2] * cut.scale + place[2],
1366 ]
1367 };
1368
1369 if spec.style.section_faces {
1370 let (fill_color, _) = library.line_style(instance.material);
1371 scratch.faces.vertices.clear();
1372 scratch.faces.colors.clear();
1373 scratch.faces.indices.clear();
1374 polytope_section_faces_append(
1375 topology.edges,
1376 topology.cells,
1377 &scratch.rotated,
1378 plane,
1379 fill_color,
1380 &mut scratch.cut,
1381 &mut scratch.faces,
1382 );
1383 if spec.style.section_edges {
1384 let (mut edge_color, material_width_px) = library.line_style(perimeter_material);
1385 let width_px = instance.line_width_px.unwrap_or(material_width_px);
1386 if let Some(opacity) = instance.line_opacity {
1387 edge_color[3] = opacity;
1388 }
1389 for [_, start, end] in &scratch.faces.indices {
1390 segments.push(SegmentRecord {
1391 start: placed(scratch.faces.vertices[*start as usize]),
1392 _pad0: 0.0,
1393 end: placed(scratch.faces.vertices[*end as usize]),
1394 _pad1: 0.0,
1395 start_color: edge_color,
1396 end_color: edge_color,
1397 width_px,
1398 _pad2: [0.0; 3],
1399 });
1400 }
1401 }
1402 for [a, b, c] in &scratch.faces.indices {
1403 triangles.push(TriangleRecord {
1404 vertices: [
1405 placed(scratch.faces.vertices[*a as usize]),
1406 placed(scratch.faces.vertices[*b as usize]),
1407 placed(scratch.faces.vertices[*c as usize]),
1408 ],
1409 color: fill_color,
1410 });
1411 }
1412 } else if spec.style.section_edges {
1413 let (mut edge_color, material_width_px) = library.line_style(perimeter_material);
1414 let width_px = instance.line_width_px.unwrap_or(material_width_px);
1415 if let Some(opacity) = instance.line_opacity {
1416 edge_color[3] = opacity;
1417 }
1418 scratch.perimeter.segments.clear();
1419 scratch.perimeter.colors.clear();
1420 scratch.perimeter.widths.clear();
1421 polytope_section_perimeter_append(
1422 topology.edges,
1423 topology.cells,
1424 &scratch.rotated,
1425 plane,
1426 &mut scratch.cut,
1427 &mut scratch.perimeter,
1428 );
1429 for (start, end) in &scratch.perimeter.segments {
1430 segments.push(SegmentRecord {
1431 start: placed(*start),
1432 _pad0: 0.0,
1433 end: placed(*end),
1434 _pad1: 0.0,
1435 start_color: edge_color,
1436 end_color: edge_color,
1437 width_px,
1438 _pad2: [0.0; 3],
1439 });
1440 }
1441 }
1442 Ok(())
1443 }
1444
1445 fn project_instance(
1446 &self,
1447 pose: &Pose<S>,
1448 library: &Library<'_>,
1449 instance: &Instance,
1450 entity: Entity,
1451 segments: &mut Vec<SegmentRecord>,
1452 refusals: &mut ViewRefusals,
1453 ) -> Option<InstanceRecord> {
1454 let space = self.space;
1455 let spec = self.spec;
1456 let eye = self.eye;
1457 let relative = match space.relative(eye, pose) {
1458 Ok(relative) => relative,
1459 Err(error) => {
1460 refusals.record(entity, error, RefusalSource::Instance);
1461 return None;
1462 }
1463 };
1464 let image_point =
1465 spec.style
1466 .mapping
1467 .image_local(space, eye, pose, &relative, space.origin())?;
1468 if spec.style.edges {
1469 push_segments(
1470 SegmentProjection {
1471 space,
1472 mapping: spec.style.mapping.as_ref(),
1473 eye,
1474 pose,
1475 relative: &relative,
1476 },
1477 library,
1478 instance,
1479 entity,
1480 segments,
1481 refusals,
1482 );
1483 }
1484 Some(InstanceRecord {
1485 entity,
1486 geometry: instance.geometry,
1487 material: instance.material,
1488 pose: space.chart_pose(pose),
1489 image_point,
1490 })
1491 }
1492}
1493
1494pub struct Pose<S: Space> {
1495 pub point: S::Point,
1496 pub frame: S::Frame,
1497}
1498
1499impl<S: Space> Pose<S> {
1500 pub fn new(space: &S, point: S::Point) -> Self {
1501 Self {
1502 point,
1503 frame: space.frame_at(point),
1504 }
1505 }
1506}
1507
1508impl<S: Space + Default> Pose<S> {
1509 pub fn at(point: S::Point) -> Self {
1510 Self::new(&S::default(), point)
1511 }
1512}
1513
1514impl<S: Space> Clone for Pose<S> {
1515 fn clone(&self) -> Self {
1516 *self
1517 }
1518}
1519
1520impl<S: Space> Copy for Pose<S> {}
1521
1522pub trait Facility<S: DomainSpace>: Any + Send + 'static {
1524 fn name(&self) -> &'static str;
1525
1526 fn bind(&mut self, _scene: SceneId, _owner: Owner) {}
1527
1528 fn step(
1529 &mut self,
1530 poses: &mut Store<Pose<S>>,
1531 step: Step,
1532 owner: Owner,
1533 ) -> Result<(), DomainError>;
1534
1535 fn set_pose(
1536 &mut self,
1537 _entity: Entity,
1538 _pose: Pose<S>,
1539 _poses: &mut Store<Pose<S>>,
1540 _owner: Owner,
1541 ) -> Option<Result<(), DomainError>> {
1542 None
1543 }
1544
1545 fn move_to(
1546 &mut self,
1547 _entity: Entity,
1548 _point: S::Point,
1549 _poses: &mut Store<Pose<S>>,
1550 _owner: Owner,
1551 ) -> Option<Result<(), DomainError>> {
1552 None
1553 }
1554
1555 fn synchronize(&mut self, _poses: &mut Store<Pose<S>>, _owner: Owner) {}
1556
1557 fn snapshot(&self, owner: Owner) -> Box<dyn Any + Send>;
1558
1559 fn check_restore(&self, from: &(dyn Any + Send), owner: Owner) -> Result<(), RestoreError>;
1561
1562 fn restore(&mut self, from: &(dyn Any + Send), owner: Owner) -> Result<(), RestoreError>;
1563
1564 fn release(&mut self, _entity: Entity, _owner: Owner) {}
1565
1566 fn apply(
1568 &mut self,
1569 _command: &ChartCommand,
1570 _poses: &mut Store<Pose<S>>,
1571 _owner: Owner,
1572 ) -> Option<Result<Outcome, Rejection>> {
1573 None
1574 }
1575}
1576
1577#[derive(Clone, Debug)]
1578pub struct Field {
1579 pub kind: FieldKind,
1580 pub op: FieldOp,
1581 pub operands: Vec<Entity>,
1582}
1583
1584#[derive(Clone, Debug, Default)]
1585pub struct FieldProgram {
1586 pub(crate) primitives: Vec<FieldPrimitive>,
1587 pub(crate) program: Vec<u32>,
1588 pub(crate) nodes: Vec<FieldNode>,
1589 pub(crate) stack: u32,
1590 pub(crate) dimension: u32,
1591 pub(crate) kind: FieldKind,
1592}
1593
1594impl FieldProgram {
1595 pub fn evaluate(&self, point: [f32; 4]) -> Result<(f32, FieldKind), FieldError> {
1596 field::evaluate(&self.program, &self.primitives, point).map(|value| (value, self.kind))
1597 }
1598
1599 pub fn evaluate_bounded(
1600 &self,
1601 point: [f32; 4],
1602 tolerance: f32,
1603 ) -> Result<(f32, FieldKind), FieldError> {
1604 field::evaluate_bounded(
1605 &self.program,
1606 &self.primitives,
1607 &self.nodes,
1608 point,
1609 tolerance,
1610 )
1611 .map(|value| (value, self.kind))
1612 }
1613}
1614
1615impl DistanceField for FieldProgram {
1616 fn field_kind(&self) -> FieldKind {
1617 self.kind
1618 }
1619
1620 fn dimension(&self) -> u32 {
1621 self.dimension
1622 }
1623
1624 fn distance(&self, point: [f32; 4]) -> f32 {
1625 field::evaluate_bounded(&self.program, &self.primitives, &self.nodes, point, 0.0)
1626 .unwrap_or(field::FIELD_FAR)
1627 }
1628
1629 fn error_at(&self, point: [f32; 4]) -> f32 {
1630 field::evaluate_error(
1631 &self.program,
1632 &self.primitives,
1633 &self.nodes,
1634 point,
1635 self.kind,
1636 )
1637 .unwrap_or(f32::INFINITY)
1638 }
1639}
1640
1641pub struct DomainSnapshot(pub(crate) Box<dyn Any + Send>);
1642
1643struct TypedSnapshot<S: DomainSpace> {
1644 scene: SceneId,
1645 poses: StoreSnapshot<Pose<S>>,
1646 instances: StoreSnapshot<Instance>,
1647 fields: Option<StoreSnapshot<Field>>,
1648 facilities: Vec<Box<dyn Any + Send>>,
1649 views: Vec<ViewEntry<S>>,
1650 targets: Vec<ViewTarget>,
1651}
1652
1653pub trait Domain: Send + 'static {
1654 fn id(&self) -> DomainId;
1655
1656 fn name(&self) -> &'static str;
1657
1658 fn tracks_changes(&self) -> bool;
1659
1660 fn views(&self) -> &[ViewTarget];
1661
1662 fn view(&self, id: ViewId) -> Option<ViewSummary>;
1663
1664 fn pick(
1665 &self,
1666 view: ViewId,
1667 ray: &ImageRay,
1668 placement: Rigid,
1669 views: &Views,
1670 prepared: &[PreparedGeometry],
1671 ) -> Option<Pick>;
1672
1673 fn image_of(&self, view: ViewId, entity: Entity) -> Option<[f32; 3]>;
1674
1675 fn lift_origin(&self, view: ViewId, ray: &ImageRay) -> Result<ChartPoint, DomainError>;
1676
1677 fn compile_fields(&mut self) -> Result<FieldCost, DomainError>;
1678
1679 fn field_program(&self) -> &FieldProgram;
1680
1681 fn as_any(&self) -> &dyn Any;
1682
1683 fn as_any_mut(&mut self) -> &mut dyn Any;
1684}
1685
1686pub(crate) trait DomainOwner: Domain {
1687 fn retarget(&mut self, view: ViewId, image: ImageSpaceId) -> Result<(), DomainError>;
1688
1689 fn publish(
1690 &self,
1691 view: ViewId,
1692 library: Library<'_>,
1693 into: &mut ViewRecords,
1694 stamp: Stamp,
1695 alpha: f32,
1696 ) -> Result<(), DomainError>;
1697
1698 fn step(&mut self, step: Step) -> Result<(), DomainError>;
1699
1700 fn synchronize(&mut self);
1701
1702 fn boundary(&mut self);
1703
1704 fn release(&mut self, entity: Entity);
1705
1706 fn snapshot(&self) -> DomainSnapshot;
1707
1708 fn check_restore(&self, from: &DomainSnapshot) -> Result<(), RestoreError>;
1709
1710 fn restore(&mut self, from: &DomainSnapshot, scene: SceneId) -> Result<(), RestoreError>;
1711
1712 fn apply(&mut self, command: &ChartCommand) -> Result<Outcome, Rejection>;
1713
1714 fn begin_tick(&mut self);
1715
1716 fn end_tick(&mut self);
1717
1718 fn present(&mut self, alpha: f32);
1719}
1720
1721pub(crate) fn presented_alpha(alpha: f32) -> f32 {
1722 if alpha.is_nan() {
1723 return 1.0;
1724 }
1725 alpha.clamp(0.0, 1.0)
1726}
1727
1728struct TickPose<S: Space> {
1729 entity: Entity,
1730 pose: Pose<S>,
1731 before: Version,
1732 after: Version,
1733}
1734
1735impl<S: Space> Clone for TickPose<S> {
1736 fn clone(&self) -> Self {
1737 *self
1738 }
1739}
1740
1741impl<S: Space> Copy for TickPose<S> {}
1742
1743pub struct TypedDomain<S: DomainSpace> {
1744 id: DomainId,
1745 scene: SceneId,
1746 name: &'static str,
1747 space: S,
1748 poses: Store<Pose<S>>,
1749 instances: Store<Instance>,
1750 fields: Option<Store<Field>>,
1751 views: Vec<ViewEntry<S>>,
1752 targets: Vec<ViewTarget>,
1753 view_revisions: Vec<u32>,
1754 view_stamps: Vec<ViewStyle<S>>,
1755 pub(crate) facilities: Vec<Box<dyn Facility<S>>>,
1756 compiler: FieldCompiler,
1757 before_tick: Vec<TickPose<S>>,
1758 moving: Vec<Entity>,
1759 generation: u64,
1760 presented: f32,
1761}
1762
1763impl<S: DomainSpace> TypedDomain<S> {
1764 pub fn handle(&self) -> DomainHandle<S> {
1765 DomainHandle {
1766 id: self.id,
1767 runtime: self.scene.runtime,
1768 space: PhantomData,
1769 }
1770 }
1771
1772 pub fn space(&self) -> &S {
1773 &self.space
1774 }
1775
1776 pub fn poses(&self) -> &Store<Pose<S>> {
1777 &self.poses
1778 }
1779
1780 pub fn instances(&self) -> &Store<Instance> {
1781 &self.instances
1782 }
1783
1784 pub fn instance_mut(&mut self, entity: Entity) -> Option<&mut Instance> {
1785 self.instances.get_mut(entity)
1786 }
1787
1788 pub fn remove_instance(&mut self, entity: Entity) -> Result<Instance, StoreError> {
1789 self.instances.remove(entity)
1790 }
1791
1792 pub(crate) fn attach_pose(&mut self, entity: Entity, pose: Pose<S>) -> Result<(), Rejection> {
1793 self.check_pose(&pose)?;
1794 self.poses.insert_raw(entity, pose)?;
1795 Ok(())
1796 }
1797
1798 pub(crate) fn attach_instance(
1799 &mut self,
1800 entity: Entity,
1801 instance: Instance,
1802 ) -> Result<(), Rejection> {
1803 if !self.poses.contains(entity) {
1804 return Err(StoreError::Missing(entity).into());
1805 }
1806 put_row(&mut self.instances, entity, instance)
1807 }
1808
1809 pub(crate) fn attach_field(&mut self, entity: Entity, field: Field) -> Result<(), Rejection> {
1810 if !self.poses.contains(entity) {
1811 return Err(StoreError::Missing(entity).into());
1812 }
1813 let fields = self
1814 .fields
1815 .as_mut()
1816 .ok_or(Rejection::Unsupported("fields"))?;
1817 put_row(fields, entity, field)
1818 }
1819
1820 pub fn set_pose(&mut self, entity: Entity, pose: Pose<S>) -> Result<(), DomainError> {
1822 self.check_pose(&pose)?;
1823 self.apply_pose(entity, pose)
1824 }
1825
1826 pub fn teleport(&mut self, entity: Entity, pose: Pose<S>) -> Result<(), DomainError> {
1828 self.check_pose(&pose)?;
1829 self.apply_pose(entity, pose)?;
1830 self.snap(entity);
1831 Ok(())
1832 }
1833
1834 fn snap(&mut self, entity: Entity) {
1835 let Some(dense) = self.poses.dense_index(entity) else {
1836 return;
1837 };
1838 let Some(pose) = self.poses.rows().get(dense).copied() else {
1839 return;
1840 };
1841 if let Some(entry) = self
1842 .before_tick
1843 .get_mut(dense)
1844 .filter(|entry| entry.entity == entity)
1845 {
1846 entry.pose = pose;
1847 }
1848 }
1849
1850 fn interpolates(&self) -> bool {
1851 S::BLENDS && self.poses.is_tracked()
1852 }
1853
1854 pub fn drawn_pose(&self, entity: Entity, alpha: f32) -> Option<Pose<S>> {
1856 self.drawn(entity, presented_alpha(alpha))
1857 }
1858
1859 #[inline]
1860 fn drawn(&self, entity: Entity, alpha: f32) -> Option<Pose<S>> {
1861 let dense = self.poses.dense_index(entity)?;
1862 let pose = *self.poses.rows().get(dense)?;
1863 if alpha >= 1.0 || !self.interpolates() {
1864 return Some(pose);
1865 }
1866 Some(self.blended(entity, dense, pose, alpha))
1867 }
1868
1869 #[inline(never)]
1870 fn blended(&self, entity: Entity, dense: usize, pose: Pose<S>, alpha: f32) -> Pose<S> {
1871 let now = self
1872 .poses
1873 .dense_versions()
1874 .and_then(|(_, versions)| versions.get(dense));
1875 match self.before_tick.get(dense) {
1876 Some(entry)
1877 if entry.entity == entity
1878 && now == Some(&entry.after)
1879 && entry.after != entry.before =>
1880 {
1881 self.space.blend(&entry.pose, &pose, alpha)
1882 }
1883 _ => pose,
1884 }
1885 }
1886
1887 fn apply_pose(&mut self, entity: Entity, pose: Pose<S>) -> Result<(), DomainError> {
1888 if !self.poses.contains(entity) {
1889 return Err(StoreError::Missing(entity).into());
1890 }
1891 for facility in &mut self.facilities {
1892 if let Some(result) = facility.set_pose(entity, pose, &mut self.poses, Owner::new()) {
1893 return result;
1894 }
1895 }
1896 *self
1897 .poses
1898 .get_mut(entity)
1899 .ok_or(StoreError::Missing(entity))? = pose;
1900 Ok(())
1901 }
1902
1903 pub fn set_point(&mut self, entity: Entity, point: S::Point) -> Result<(), DomainError> {
1904 let mut pose = *self.poses.get(entity).ok_or(StoreError::Missing(entity))?;
1905 pose.point = point;
1906 self.set_pose(entity, pose)
1907 }
1908
1909 pub fn set_frame(&mut self, entity: Entity, frame: S::Frame) -> Result<(), DomainError> {
1910 let mut pose = *self.poses.get(entity).ok_or(StoreError::Missing(entity))?;
1911 pose.frame = frame;
1912 self.set_pose(entity, pose)
1913 }
1914
1915 fn check_pose(&self, pose: &Pose<S>) -> Result<(), DomainError> {
1916 self.space
1917 .pose_from_chart(&self.space.chart_pose(pose))
1918 .map(|_| ())
1919 }
1920
1921 pub fn add_view(&mut self, spec: ViewSpec<S>) -> Result<ViewId, DomainError> {
1922 if !self.poses.contains(spec.eye) {
1923 return Err(DomainError::Stale(spec.eye));
1924 }
1925 if let Some(subject) = spec.subject {
1926 if !self.poses.contains(subject) {
1927 return Err(DomainError::Stale(subject));
1928 }
1929 }
1930 let id = ViewId::new(self.views.len());
1931 self.targets.push(ViewTarget {
1932 view: id,
1933 image: spec.image,
1934 });
1935 self.view_revisions.push(0);
1936 self.view_stamps.push(spec.style.clone());
1937 self.views.push(ViewEntry {
1938 eye: spec.eye,
1939 subject: spec.subject,
1940 style: spec.style,
1941 });
1942 Ok(id)
1943 }
1944
1945 pub fn view(&self, id: ViewId) -> Option<&ViewStyle<S>> {
1946 self.views.get(id.index()).map(|view| &view.style)
1947 }
1948
1949 pub fn view_eye(&self, id: ViewId) -> Option<Entity> {
1950 self.views.get(id.index()).map(|view| view.eye)
1951 }
1952
1953 pub fn view_subject(&self, id: ViewId) -> Option<Entity> {
1954 self.views.get(id.index())?.subject
1955 }
1956
1957 pub fn view_mut(&mut self, id: ViewId) -> Option<&mut ViewStyle<S>> {
1958 let spec = self.views.get_mut(id.index())?;
1959 Some(&mut spec.style)
1960 }
1961
1962 pub fn set_view_eye(&mut self, id: ViewId, eye: Entity) -> Result<(), DomainError> {
1963 let index = id.index();
1964 if self.views.get(index).is_none() {
1965 return Err(DomainError::UnknownView(id));
1966 }
1967 if !self.poses.contains(eye) {
1968 return Err(DomainError::Stale(eye));
1969 }
1970 if self.views[index].eye == eye {
1971 return Ok(());
1972 }
1973 self.views[index].eye = eye;
1974 self.view_revisions[index] = self.view_revisions[index].wrapping_add(1);
1975 Ok(())
1976 }
1977
1978 pub fn set_view_subject(
1979 &mut self,
1980 id: ViewId,
1981 subject: Option<Entity>,
1982 ) -> Result<(), DomainError> {
1983 let index = id.index();
1984 if self.views.get(index).is_none() {
1985 return Err(DomainError::UnknownView(id));
1986 }
1987 if let Some(subject) = subject {
1988 if !self.poses.contains(subject) {
1989 return Err(DomainError::Stale(subject));
1990 }
1991 }
1992 if self.views[index].subject == subject {
1993 return Ok(());
1994 }
1995 self.views[index].subject = subject;
1996 self.view_revisions[index] = self.view_revisions[index].wrapping_add(1);
1997 Ok(())
1998 }
1999
2000 fn rebuild_view(
2001 &self,
2002 spec: &ViewEntry<S>,
2003 library: Library<'_>,
2004 into: &mut ViewRecords,
2005 stamp: Stamp,
2006 alpha: f32,
2007 ) {
2008 let ViewRecords {
2009 instances,
2010 segments,
2011 triangles,
2012 refusals,
2013 scratch,
2014 output,
2015 built,
2016 ..
2017 } = into;
2018 segments.clear();
2019 triangles.clear();
2020 *refusals = ViewRefusals::default();
2021 output.clear();
2022 let eye = match self.drawn(spec.eye, alpha) {
2023 Some(eye) if spec.style.enabled => eye,
2024 _ => {
2025 instances.replace(std::iter::empty(), stamp);
2026 *built = stamp;
2027 return;
2028 }
2029 };
2030 let projection = ViewProjection {
2031 space: &self.space,
2032 spec,
2033 eye: &eye,
2034 };
2035 for (entity, instance) in self.instances.iter() {
2036 if spec.subject.is_some_and(|subject| subject != entity) {
2037 continue;
2038 }
2039 let section_start = segments.len();
2040 let triangle_start = triangles.len();
2041 if let Some(pose) = self.drawn(entity, alpha) {
2042 if spec.style.section_edges || spec.style.section_faces {
2043 if let Err(error) = projection
2044 .push_section(&pose, &library, instance, scratch, segments, triangles)
2045 {
2046 refusals.record(entity, error, RefusalSource::Section);
2047 }
2048 }
2049 }
2050 output.push(EntityOutput {
2051 entity,
2052 section_segments: section_start..segments.len(),
2053 line_segments: 0..0,
2054 triangles: triangle_start..triangles.len(),
2055 instance: false,
2056 });
2057 }
2058 let records = self
2059 .instances
2060 .iter()
2061 .filter(|(entity, _)| spec.subject.is_none_or(|subject| subject == *entity))
2062 .zip(output.iter_mut())
2063 .filter_map(|((entity, instance), cached)| {
2064 let line_start = segments.len();
2065 let mut record_refusals = ViewRefusals::default();
2066 let record = self.drawn(entity, alpha).and_then(|pose| {
2067 projection.project_instance(
2068 &pose,
2069 &library,
2070 instance,
2071 entity,
2072 segments,
2073 &mut record_refusals,
2074 )
2075 });
2076 cached.line_segments = line_start..segments.len();
2077 cached.instance = record.is_some();
2078 if record_refusals.count != 0 {
2079 refusals.count = refusals.count.saturating_add(record_refusals.count);
2080 if refusals.first.is_none() {
2081 refusals.first = record_refusals.first;
2082 }
2083 refusals.last = record_refusals.last;
2084 }
2085 record.map(|record| (entity, record))
2086 });
2087 instances.replace(records, stamp);
2088 *built = stamp;
2089 }
2090
2091 fn patch_view(
2092 &self,
2093 spec: &ViewEntry<S>,
2094 library: Library<'_>,
2095 into: &mut ViewRecords,
2096 stamp: Stamp,
2097 alpha: f32,
2098 ) -> bool {
2099 let Some(eye) = self.drawn(spec.eye, alpha) else {
2100 return false;
2101 };
2102 let projection = ViewProjection {
2103 space: &self.space,
2104 spec,
2105 eye: &eye,
2106 };
2107 let ViewRecords {
2108 instances,
2109 segments,
2110 triangles,
2111 scratch,
2112 output,
2113 patch_segments,
2114 patch_triangles,
2115 changed,
2116 built,
2117 ..
2118 } = into;
2119 let mut patched = false;
2120 for &entity in changed.iter() {
2121 if spec.subject.is_some_and(|subject| subject != entity) {
2122 continue;
2123 }
2124 let Some(instance) = self.instances.get(entity) else {
2125 continue;
2126 };
2127 let Some(pose) = self.drawn(entity, alpha) else {
2128 return false;
2129 };
2130 let Some(cached) = output.get(entity).cloned() else {
2131 return false;
2132 };
2133 patch_segments.clear();
2134 patch_triangles.clear();
2135 let mut section_refusals = ViewRefusals::default();
2136 if spec.style.section_edges || spec.style.section_faces {
2137 if let Err(error) = projection.push_section(
2138 &pose,
2139 &library,
2140 instance,
2141 scratch,
2142 patch_segments,
2143 patch_triangles,
2144 ) {
2145 section_refusals.record(entity, error, RefusalSource::Section);
2146 }
2147 }
2148 if patch_segments.len() != cached.section_segments.end - cached.section_segments.start
2149 || patch_triangles.len() != cached.triangles.end - cached.triangles.start
2150 || section_refusals.count != 0
2151 {
2152 return false;
2153 }
2154 segments[cached.section_segments.clone()].copy_from_slice(patch_segments);
2155 triangles[cached.triangles.clone()].copy_from_slice(patch_triangles);
2156
2157 patch_segments.clear();
2158 let mut record_refusals = ViewRefusals::default();
2159 let record = projection.project_instance(
2160 &pose,
2161 &library,
2162 instance,
2163 entity,
2164 patch_segments,
2165 &mut record_refusals,
2166 );
2167 if patch_segments.len() != cached.line_segments.end - cached.line_segments.start
2168 || record.is_some() != cached.instance
2169 || record_refusals.count != 0
2170 {
2171 return false;
2172 }
2173 segments[cached.line_segments.clone()].copy_from_slice(patch_segments);
2174 if let Some(record) = record {
2175 if !instances.update(entity, record) {
2176 return false;
2177 }
2178 }
2179 patched = true;
2180 }
2181 instances.restamp(stamp);
2182 if patched {
2183 *built = stamp;
2184 }
2185 true
2186 }
2187
2188 pub fn fields(&self) -> Option<&Store<Field>> {
2189 self.fields.as_ref()
2190 }
2191
2192 pub fn field_mut(&mut self, entity: Entity) -> Option<&mut Field> {
2193 self.fields.as_mut()?.get_mut(entity)
2194 }
2195
2196 pub fn remove_field(&mut self, entity: Entity) -> Result<Field, StoreError> {
2197 self.fields
2198 .as_mut()
2199 .ok_or(StoreError::Missing(entity))?
2200 .remove(entity)
2201 }
2202
2203 pub fn walk(
2204 &mut self,
2205 entity: Entity,
2206 velocity: S::Vector,
2207 dt: f32,
2208 ) -> Result<(), DomainError> {
2209 let pose = self.poses.get(entity).ok_or(StoreError::Missing(entity))?;
2210 let next = self.space.walk(pose, velocity, dt)?;
2211 self.apply_pose(entity, next)
2212 }
2213
2214 pub fn move_to(&mut self, entity: Entity, point: ChartPoint) -> Result<(), DomainError> {
2215 let target = self.space.point_from_chart(&point)?;
2216 if !self.poses.contains(entity) {
2217 return Err(StoreError::Missing(entity).into());
2218 }
2219 for facility in &mut self.facilities {
2220 if let Some(result) = facility.move_to(entity, target, &mut self.poses, Owner::new()) {
2221 return result;
2222 }
2223 }
2224 let space = &self.space;
2225 let pose = self
2226 .poses
2227 .get_mut(entity)
2228 .ok_or(StoreError::Missing(entity))?;
2229 let next = space.moved(pose, target)?;
2230 *pose = next;
2231 Ok(())
2232 }
2233}
2234
2235#[cfg(feature = "physics")]
2236impl<S> TypedDomain<S>
2237where
2238 S: Homogeneous + loam_physics::PhysicsSpace + Copy,
2239 S::Vector: loam_physics::collision::VectorOps + Default,
2240 S::Point: std::ops::Sub<Output = S::Vector>,
2241{
2242 pub fn spawn_body(
2243 &mut self,
2244 entity: Entity,
2245 body: loam_physics::BodyDef<S>,
2246 ) -> Result<loam_physics::BodyId, DomainError> {
2247 let pose = *self.poses.get(entity).ok_or(StoreError::Missing(entity))?;
2248 let physics = self
2249 .facilities
2250 .iter_mut()
2251 .find_map(|facility| {
2252 (facility.as_mut() as &mut dyn Any).downcast_mut::<crate::physics::Physics<S>>()
2253 })
2254 .ok_or(DomainError::Unsupported("physics"))?;
2255 physics.check_pose(pose)?;
2256 let id = physics.spawn(entity, body);
2257 if let Err(error) = physics.replace_pose(id, pose, &mut self.poses) {
2258 let _ = physics.despawn(entity);
2259 return Err(error.into());
2260 }
2261 Ok(id)
2262 }
2263}
2264
2265impl<S: DomainSpace> Domain for TypedDomain<S> {
2266 fn id(&self) -> DomainId {
2267 self.id
2268 }
2269
2270 fn name(&self) -> &'static str {
2271 self.name
2272 }
2273
2274 fn tracks_changes(&self) -> bool {
2275 self.poses.is_tracked()
2276 }
2277
2278 fn views(&self) -> &[ViewTarget] {
2279 &self.targets
2280 }
2281
2282 fn view(&self, id: ViewId) -> Option<ViewSummary> {
2283 let spec = self.views.get(id.index())?;
2284 let target = self.targets.get(id.index())?;
2285 Some(ViewSummary {
2286 name: spec.style.mapping.name(),
2287 eye: spec.eye,
2288 image: target.image,
2289 ray_lift: spec.style.mapping.ray_lift(),
2290 })
2291 }
2292
2293 fn pick(
2294 &self,
2295 view: ViewId,
2296 ray: &ImageRay,
2297 placement: Rigid,
2298 views: &Views,
2299 prepared: &[PreparedGeometry],
2300 ) -> Option<Pick> {
2301 let spec = self.views.get(view.index())?;
2302 let target = self.targets.get(view.index())?;
2303 if !spec.style.enabled {
2304 return None;
2305 }
2306 let eye = &self.drawn(spec.eye, self.presented)?;
2307 let origin = self.space.origin();
2308 let lifted = spec.style.mapping.lift(eye, ray);
2309 let mut nearest: Option<Pick> = None;
2310 for (entity, instance) in self.instances.iter() {
2311 if spec.subject.is_some_and(|subject| subject != entity) {
2312 continue;
2313 }
2314 let Some(pose) = self.drawn(entity, self.presented) else {
2315 continue;
2316 };
2317 let Some(geometry) = prepared.get(instance.geometry.index()) else {
2318 continue;
2319 };
2320 let Ok(center) = self.space.place(&self.space.prepare(&pose), origin) else {
2321 continue;
2322 };
2323 let radius = geometry.bounding_radius();
2324 let hit = match &lifted {
2325 Some(domain_ray) => {
2326 let reach = self.space.chart_reach(center, radius);
2327 self.space
2328 .hit_ball(domain_ray, center, reach)
2329 .and_then(|entry| match geometry {
2330 PreparedGeometry::Polytope4 { polytope, scale } => {
2331 polytope_entry(&self.space, domain_ray, &pose, *polytope, *scale)
2332 }
2333 _ => Some(entry),
2334 })
2335 .map(|t| self.space.exp(domain_ray.origin, domain_ray.direction * t))
2336 .and_then(|point| {
2337 let image_point = spec.style.mapping.image_point(eye, point)?;
2338 Some((image_point, Some(self.space.chart_point(point))))
2339 })
2340 }
2341 None => spec
2342 .style
2343 .mapping
2344 .image_point(eye, center)
2345 .and_then(|image_center| {
2346 let image_radius = spec.style.mapping.image_radius(eye, center, radius);
2347 let t = view::image_hit(ray, image_center, image_radius)?;
2348 Some((ray.at(t), None))
2349 }),
2350 };
2351 let Some((image_point, hit)) = hit else {
2352 continue;
2353 };
2354 let image_point = placement.apply(image_point);
2355 let Some(depth) = views.depth(image_point) else {
2356 continue;
2357 };
2358 if nearest.is_some_and(|best| best.depth >= depth) {
2359 continue;
2360 }
2361 nearest = Some(Pick {
2362 entity,
2363 domain: self.id,
2364 view,
2365 image: target.image,
2366 image_point,
2367 depth,
2368 hit,
2369 });
2370 }
2371 nearest
2372 }
2373
2374 fn image_of(&self, view: ViewId, entity: Entity) -> Option<[f32; 3]> {
2375 let spec = self.views.get(view.index())?;
2376 if !spec.style.enabled || spec.subject.is_some_and(|subject| subject != entity) {
2377 return None;
2378 }
2379 let eye = self.drawn(spec.eye, self.presented)?;
2380 let pose = self.drawn(entity, self.presented)?;
2381 let relative = self.space.relative(&eye, &pose).ok()?;
2382 spec.style
2383 .mapping
2384 .image_local(&self.space, &eye, &pose, &relative, self.space.origin())
2385 }
2386
2387 fn lift_origin(&self, view: ViewId, ray: &ImageRay) -> Result<ChartPoint, DomainError> {
2388 let spec = self
2389 .views
2390 .get(view.index())
2391 .ok_or(DomainError::UnknownView(view))?;
2392 let eye = self
2393 .drawn(spec.eye, self.presented)
2394 .ok_or(DomainError::Stale(spec.eye))?;
2395 let lifted = spec
2396 .style
2397 .mapping
2398 .lift(&eye, ray)
2399 .ok_or(DomainError::Unsupported(spec.style.mapping.name()))?;
2400 self.space.check(lifted.origin)?;
2401 Ok(self.space.chart_point(lifted.origin))
2402 }
2403
2404 fn compile_fields(&mut self) -> Result<FieldCost, DomainError> {
2405 let fields = self
2406 .fields
2407 .as_ref()
2408 .ok_or(DomainError::Unsupported("fields"))?;
2409 self.compiler.compile(&self.space, fields, &self.poses)
2410 }
2411
2412 fn field_program(&self) -> &FieldProgram {
2413 self.compiler.program()
2414 }
2415
2416 fn as_any(&self) -> &dyn Any {
2417 self
2418 }
2419
2420 fn as_any_mut(&mut self) -> &mut dyn Any {
2421 self
2422 }
2423}
2424
2425impl<S: DomainSpace> DomainOwner for TypedDomain<S> {
2426 fn retarget(&mut self, view: ViewId, image: ImageSpaceId) -> Result<(), DomainError> {
2427 self.views
2428 .get(view.index())
2429 .ok_or(DomainError::UnknownView(view))?;
2430 self.targets[view.index()].image = image;
2431 self.view_revisions[view.index()] = self.view_revisions[view.index()].wrapping_add(1);
2432 Ok(())
2433 }
2434
2435 fn publish(
2436 &self,
2437 view: ViewId,
2438 library: Library<'_>,
2439 into: &mut ViewRecords,
2440 stamp: Stamp,
2441 alpha: f32,
2442 ) -> Result<(), DomainError> {
2443 let spec = self
2444 .views
2445 .get(view.index())
2446 .ok_or(DomainError::UnknownView(view))?;
2447 let revision = *self
2448 .view_revisions
2449 .get(view.index())
2450 .ok_or(DomainError::UnknownView(view))?;
2451 let mut pose_cursor = into.poses;
2452 let pose_changes = self.poses.changes(&mut pose_cursor);
2453 let pose_resync = pose_changes.is_resync();
2454 into.changed.clear();
2455 let mut pose_removed = false;
2456 for change in pose_changes {
2457 match change {
2458 Change::Row(entity, _) => into.changed.push(entity),
2459 Change::Removed(_) => pose_removed = true,
2460 }
2461 }
2462 if self.interpolates() {
2463 let logged = !into.changed.is_empty();
2464 let ticked = into.generation != self.generation;
2465 if ticked {
2466 into.changed.extend_from_slice(&into.merged);
2467 into.merged.clear();
2468 into.merged.extend_from_slice(&self.moving);
2469 into.generation = self.generation;
2470 }
2471 if ticked || alpha != into.alpha {
2472 into.changed.extend_from_slice(&self.moving);
2473 if logged || ticked {
2474 into.changed.sort_unstable();
2475 into.changed.dedup();
2476 }
2477 }
2478 }
2479 into.alpha = alpha;
2480 let mut attachment_cursor = into.attachments;
2481 let mut attachment_changes = self.instances.changes(&mut attachment_cursor);
2482 let attachment_resync = attachment_changes.is_resync();
2483 let attached = attachment_changes.next().is_some();
2484 if !pose_resync
2485 && !pose_removed
2486 && !attachment_resync
2487 && !attached
2488 && into.revision == revision
2489 && !into.changed.contains(&spec.eye)
2490 {
2491 let patched = if into.changed.is_empty() {
2492 into.instances.restamp(stamp);
2493 true
2494 } else if spec.style.enabled && into.refusals.count == 0 {
2495 self.patch_view(spec, library, into, stamp, alpha)
2496 } else {
2497 false
2498 };
2499 if patched {
2500 into.poses = pose_cursor;
2501 into.attachments = attachment_cursor;
2502 return Ok(());
2503 }
2504 }
2505 self.poses.catch_up(&mut into.poses);
2506 self.instances.catch_up(&mut into.attachments);
2507 into.revision = revision;
2508 self.rebuild_view(spec, library, into, stamp, alpha);
2509 Ok(())
2510 }
2511
2512 fn step(&mut self, step: Step) -> Result<(), DomainError> {
2513 for facility in &mut self.facilities {
2514 facility.step(&mut self.poses, step, Owner::new())?;
2515 }
2516 Ok(())
2517 }
2518
2519 fn boundary(&mut self) {
2520 self.synchronize();
2521 self.poses.boundary();
2522 self.instances.boundary();
2523 if let Some(fields) = &mut self.fields {
2524 fields.boundary();
2525 }
2526 }
2527
2528 fn synchronize(&mut self) {
2529 for facility in &mut self.facilities {
2530 facility.synchronize(&mut self.poses, Owner::new());
2531 }
2532 for (index, view) in self.views.iter().enumerate() {
2533 let Some(stamp) = self.view_stamps.get_mut(index) else {
2534 continue;
2535 };
2536 if same_style(&view.style, stamp) {
2537 continue;
2538 }
2539 *stamp = view.style.clone();
2540 if let Some(revision) = self.view_revisions.get_mut(index) {
2541 *revision = revision.wrapping_add(1);
2542 }
2543 }
2544 }
2545
2546 fn release(&mut self, entity: Entity) {
2547 for facility in &mut self.facilities {
2548 facility.release(entity, Owner::new());
2549 }
2550 if let Some(dense) = self.poses.dense_index(entity) {
2551 let last = self.poses.len() - 1;
2552 let moved = self
2553 .before_tick
2554 .get(last)
2555 .copied()
2556 .filter(|entry| Some(entry.entity) == self.poses.entity_at(last));
2557 if let (Some(moved), Some(slot)) = (moved, self.before_tick.get_mut(dense)) {
2558 *slot = moved;
2559 }
2560 }
2561 StoreField::release(&mut self.poses, entity, Owner::new());
2562 StoreField::release(&mut self.instances, entity, Owner::new());
2563 if let Some(fields) = &mut self.fields {
2564 StoreField::release(fields, entity, Owner::new());
2565 }
2566 }
2567
2568 fn snapshot(&self) -> DomainSnapshot {
2569 DomainSnapshot(Box::new(TypedSnapshot::<S> {
2570 scene: self.scene,
2571 poses: StoreField::snapshot(&self.poses),
2572 instances: StoreField::snapshot(&self.instances),
2573 fields: self.fields.as_ref().map(StoreField::snapshot),
2574 facilities: self
2575 .facilities
2576 .iter()
2577 .map(|facility| facility.snapshot(Owner::new()))
2578 .collect(),
2579 views: self.views.clone(),
2580 targets: self.targets.clone(),
2581 }))
2582 }
2583
2584 fn check_restore(&self, from: &DomainSnapshot) -> Result<(), RestoreError> {
2585 let from = from
2586 .0
2587 .downcast_ref::<TypedSnapshot<S>>()
2588 .ok_or(RestoreError::Domain(self.id))?;
2589 if from.facilities.len() != self.facilities.len()
2590 || from.fields.is_some() != self.fields.is_some()
2591 {
2592 return Err(RestoreError::Domain(self.id));
2593 }
2594 for (facility, snapshot) in self.facilities.iter().zip(&from.facilities) {
2595 facility.check_restore(snapshot.as_ref(), Owner::new())?;
2596 }
2597 Ok(())
2598 }
2599
2600 fn restore(&mut self, from: &DomainSnapshot, scene: SceneId) -> Result<(), RestoreError> {
2601 self.check_restore(from)?;
2602 let from = from
2603 .0
2604 .downcast_ref::<TypedSnapshot<S>>()
2605 .ok_or(RestoreError::Domain(self.id))?;
2606 for (facility, snapshot) in self.facilities.iter_mut().zip(&from.facilities) {
2607 facility.restore(snapshot.as_ref(), Owner::new())?;
2608 facility.bind(scene, Owner::new());
2609 }
2610 self.scene = scene;
2611 self.before_tick.clear();
2612 self.moving.clear();
2613 StoreField::restore(&mut self.poses, &from.poses, scene, Owner::new());
2614 StoreField::restore(&mut self.instances, &from.instances, scene, Owner::new());
2615 if let (Some(fields), Some(snapshot)) = (&mut self.fields, &from.fields) {
2616 StoreField::restore(fields, snapshot, scene, Owner::new());
2617 for field in fields.rows_mut_untracked() {
2618 for operand in &mut field.operands {
2619 if operand.scene() == from.scene {
2620 *operand = Entity::new(scene, operand.key());
2621 }
2622 }
2623 }
2624 }
2625 let mut views = from.views.clone();
2626 let mut revisions = Vec::with_capacity(views.len());
2627 for (index, view) in views.iter_mut().enumerate() {
2628 if view.eye.scene() == from.scene {
2629 view.eye = Entity::new(scene, view.eye.key());
2630 }
2631 view.subject = view.subject.map(|subject| {
2632 if subject.scene() == from.scene {
2633 Entity::new(scene, subject.key())
2634 } else {
2635 subject
2636 }
2637 });
2638 revisions.push(
2639 self.view_revisions
2640 .get(index)
2641 .copied()
2642 .unwrap_or(0)
2643 .wrapping_add(1),
2644 );
2645 }
2646 self.views = views;
2647 self.targets.clone_from(&from.targets);
2648 self.view_revisions = revisions;
2649 self.view_stamps = self.views.iter().map(|view| view.style.clone()).collect();
2650 self.compiler.invalidate();
2651 Ok(())
2652 }
2653
2654 fn begin_tick(&mut self) {
2655 self.before_tick.clear();
2656 if !self.interpolates() {
2657 return;
2658 }
2659 let Some((_, versions)) = self.poses.dense_versions() else {
2660 return;
2661 };
2662 self.before_tick.extend(self.poses.iter().zip(versions).map(
2663 |((entity, pose), version)| TickPose {
2664 entity,
2665 pose: *pose,
2666 before: *version,
2667 after: *version,
2668 },
2669 ));
2670 }
2671
2672 fn end_tick(&mut self) {
2673 self.moving.clear();
2674 let Some((keys, versions)) = self.poses.dense_versions() else {
2675 return;
2676 };
2677 for ((entry, key), version) in self.before_tick.iter_mut().zip(keys).zip(versions) {
2678 if *version != entry.before && entry.entity.key() == *key {
2679 entry.after = *version;
2680 self.moving.push(entry.entity);
2681 }
2682 }
2683 self.generation = self.generation.wrapping_add(1);
2684 }
2685
2686 fn present(&mut self, alpha: f32) {
2687 self.presented = alpha;
2688 }
2689
2690 fn apply(&mut self, command: &ChartCommand) -> Result<Outcome, Rejection> {
2691 let owned = self.poses.contains(command.entity());
2692 if !owned && !matches!(command, ChartCommand::Place { .. }) {
2693 return Err(StoreError::Missing(command.entity()).into());
2694 }
2695 match command {
2696 ChartCommand::Place { pose, .. } => {
2697 self.space.pose_from_chart(pose)?;
2698 }
2699 ChartCommand::Walk { tangent, .. } => {
2700 self.space.tangent_from_chart(tangent)?;
2701 }
2702 ChartCommand::Move { point, .. } | ChartCommand::Grab { point, .. } => {
2703 self.space.point_from_chart(point)?;
2704 }
2705 _ => {}
2706 }
2707 if owned {
2708 for facility in &mut self.facilities {
2709 if let Some(outcome) = facility.apply(command, &mut self.poses, Owner::new()) {
2710 return outcome;
2711 }
2712 }
2713 }
2714 match command {
2715 ChartCommand::Place { entity, pose } => {
2716 let placed = self.space.pose_from_chart(pose)?;
2717 put_row(&mut self.poses, *entity, placed)?;
2718 Ok(Outcome::Done)
2719 }
2720 ChartCommand::Attach { entity, instance } => {
2721 put_row(&mut self.instances, *entity, *instance)?;
2722 Ok(Outcome::Done)
2723 }
2724 ChartCommand::Walk {
2725 entity,
2726 tangent,
2727 dt,
2728 } => {
2729 let velocity = self.space.tangent_from_chart(tangent)?;
2730 self.walk(*entity, velocity, *dt)?;
2731 Ok(Outcome::Done)
2732 }
2733 ChartCommand::Move { entity, point } => {
2734 self.move_to(*entity, *point)?;
2735 Ok(Outcome::Done)
2736 }
2737 ChartCommand::Grab { .. } | ChartCommand::Release { .. } => Ok(Outcome::Done),
2738 }
2739 }
2740}
2741
2742pub struct DomainBuilder<S: DomainSpace> {
2743 name: &'static str,
2744 pub(crate) space: S,
2745 tracking: Option<LogCapacity>,
2746 fields: bool,
2747 facilities: Vec<Box<dyn Facility<S>>>,
2748}
2749
2750impl<S: DomainSpace> DomainBuilder<S> {
2751 pub fn new(name: &'static str, space: S) -> Self {
2752 Self {
2753 name,
2754 space,
2755 tracking: None,
2756 fields: false,
2757 facilities: Vec::new(),
2758 }
2759 }
2760
2761 pub fn tracked(mut self, capacity: LogCapacity) -> Self {
2762 self.tracking = Some(capacity);
2763 self
2764 }
2765
2766 pub fn fields(mut self) -> Self {
2767 self.fields = true;
2768 self
2769 }
2770
2771 pub fn facility(mut self, facility: impl Facility<S>) -> Self {
2772 self.facilities.push(Box::new(facility));
2773 self
2774 }
2775
2776 pub(crate) fn build(self, id: DomainId, scene: SceneId) -> TypedDomain<S> {
2777 fn store<T>(tracking: Option<LogCapacity>, scene: SceneId) -> Store<T> {
2778 let mut store = match tracking {
2779 Some(capacity) => Store::tracked(capacity),
2780 None => Store::untracked(),
2781 };
2782 store.bind(scene);
2783 store
2784 }
2785 let mut facilities = self.facilities;
2786 for facility in &mut facilities {
2787 facility.bind(scene, Owner::new());
2788 }
2789 TypedDomain {
2790 id,
2791 scene,
2792 name: self.name,
2793 space: self.space,
2794 poses: store(self.tracking, scene),
2795 instances: store(self.tracking, scene),
2796 fields: self.fields.then(|| store(self.tracking, scene)),
2797 views: Vec::new(),
2798 targets: Vec::new(),
2799 view_revisions: Vec::new(),
2800 view_stamps: Vec::new(),
2801 facilities,
2802 compiler: FieldCompiler::new(),
2803 before_tick: Vec::new(),
2804 moving: Vec::new(),
2805 generation: 0,
2806 presented: 1.0,
2807 }
2808 }
2809}
2810
2811pub struct Domains {
2812 runtime: RuntimeId,
2813 list: Vec<Box<dyn DomainOwner>>,
2814}
2815
2816impl Domains {
2817 pub(crate) fn new(runtime: RuntimeId) -> Self {
2818 Self {
2819 runtime,
2820 list: Vec::new(),
2821 }
2822 }
2823
2824 pub(crate) fn next_id(&self) -> DomainId {
2825 DomainId::new(self.list.len())
2826 }
2827
2828 pub(crate) fn push(&mut self, domain: Box<dyn DomainOwner>) {
2829 self.list.push(domain);
2830 }
2831
2832 pub fn runtime(&self) -> RuntimeId {
2833 self.runtime
2834 }
2835
2836 pub fn len(&self) -> usize {
2837 self.list.len()
2838 }
2839
2840 pub fn is_empty(&self) -> bool {
2841 self.list.is_empty()
2842 }
2843
2844 pub fn get(&self, id: DomainId) -> Option<&dyn Domain> {
2845 self.list.get(id.index()).map(|domain| domain.as_ref() as _)
2846 }
2847
2848 pub(crate) fn facade(&mut self, id: DomainId) -> Option<&mut dyn DomainOwner> {
2849 self.list.get_mut(id.index()).map(|domain| domain.as_mut())
2850 }
2851
2852 pub fn iter(&self) -> impl Iterator<Item = &dyn Domain> {
2853 self.list.iter().map(|domain| domain.as_ref() as _)
2854 }
2855
2856 pub(crate) fn owned(&self) -> impl Iterator<Item = &dyn DomainOwner> {
2857 self.list.iter().map(|domain| domain.as_ref())
2858 }
2859
2860 pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut dyn DomainOwner> {
2861 self.list.iter_mut().map(|domain| domain.as_mut())
2862 }
2863
2864 pub(crate) fn synchronize(&mut self) {
2865 for domain in &mut self.list {
2866 domain.synchronize();
2867 }
2868 }
2869
2870 pub fn typed<S: DomainSpace>(
2871 &mut self,
2872 handle: DomainHandle<S>,
2873 ) -> Result<&mut TypedDomain<S>, DomainError> {
2874 if handle.runtime != self.runtime {
2875 return Err(DomainError::ForeignRuntime);
2876 }
2877 let domain = self
2878 .list
2879 .get_mut(handle.id.index())
2880 .ok_or(DomainError::UnknownDomain(handle.id))?;
2881 domain
2882 .as_any_mut()
2883 .downcast_mut::<TypedDomain<S>>()
2884 .ok_or(DomainError::SpaceMismatch(handle.id))
2885 }
2886
2887 pub fn named<S: DomainSpace>(
2888 &self,
2889 name: &'static str,
2890 ) -> Result<DomainHandle<S>, DomainError> {
2891 let mut matched = self.list.iter().filter(|domain| domain.name() == name);
2892 let Some(domain) = matched.next() else {
2893 return Err(DomainError::UnknownDomainName(name));
2894 };
2895 if matched.next().is_some() {
2896 return Err(DomainError::AmbiguousDomainName(name));
2897 }
2898 domain
2899 .as_any()
2900 .downcast_ref::<TypedDomain<S>>()
2901 .map(TypedDomain::handle)
2902 .ok_or(DomainError::SpaceMismatch(domain.id()))
2903 }
2904
2905 pub fn read<S: DomainSpace>(
2906 &self,
2907 handle: DomainHandle<S>,
2908 ) -> Result<&TypedDomain<S>, DomainError> {
2909 if handle.runtime != self.runtime {
2910 return Err(DomainError::ForeignRuntime);
2911 }
2912 let domain = self
2913 .list
2914 .get(handle.id.index())
2915 .ok_or(DomainError::UnknownDomain(handle.id))?;
2916 domain
2917 .as_any()
2918 .downcast_ref::<TypedDomain<S>>()
2919 .ok_or(DomainError::SpaceMismatch(handle.id))
2920 }
2921
2922 pub fn pick(
2923 &self,
2924 views: &Views,
2925 prepared: &[PreparedGeometry],
2926 ndc: [f32; 2],
2927 ) -> Option<Pick> {
2928 self.nearest(views, prepared, ndc, false)
2929 }
2930
2931 pub fn pick_lifted(
2932 &self,
2933 views: &Views,
2934 prepared: &[PreparedGeometry],
2935 ndc: [f32; 2],
2936 ) -> Option<Pick> {
2937 self.nearest(views, prepared, ndc, true)
2938 }
2939
2940 fn nearest(
2941 &self,
2942 views: &Views,
2943 prepared: &[PreparedGeometry],
2944 ndc: [f32; 2],
2945 lifted_only: bool,
2946 ) -> Option<Pick> {
2947 let mut nearest: Option<Pick> = None;
2948 for domain in self.iter() {
2949 for target in domain.views() {
2950 if lifted_only
2951 && !domain
2952 .view(target.view)
2953 .is_some_and(|summary| summary.ray_lift)
2954 {
2955 continue;
2956 }
2957 let Some(placement) = views.to_root(target.image).and_then(|to| to.rigid()) else {
2958 continue;
2959 };
2960 let Some(ray) = views.ray(target.image, ndc) else {
2961 continue;
2962 };
2963 let Some(pick) = domain.pick(target.view, &ray, placement, views, prepared) else {
2964 continue;
2965 };
2966 if nearest.is_none_or(|best| pick.depth > best.depth) {
2967 nearest = Some(pick);
2968 }
2969 }
2970 }
2971 nearest
2972 }
2973}
2974
2975#[cfg(test)]
2976mod tests {
2977 use loam_shape::polytope::Polytope4;
2978
2979 use super::*;
2980 use crate::command::{Command, SpawnBundle};
2981 use crate::input::Input;
2982 use crate::phase::{Ctx, Phase, Tick};
2983 use crate::session::{Material, Publication, Session, SimConfig};
2984 use crate::store::LogCapacity;
2985 use crate::view::{Klein, Projection4, Section4};
2986
2987 crate::stores! {
2988 #[derive(Default)]
2989 pub struct Probe {
2990 tags: Store<u8>,
2991 }
2992 }
2993
2994 const SEGMENTS: usize = 3;
2995 const EYE_AT: Vec4 = Vec4::new(0.0, 0.0, 0.0, 2.0);
2996 const OBJECT_AT: Vec4 = Vec4::new(0.0, 0.0, -4.0, 0.0);
2997
2998 fn lines() -> PreparedGeometry {
2999 PreparedGeometry::Lines4 {
3000 segments: (0..SEGMENTS)
3001 .map(|i| {
3002 let x = 0.1 + i as f32 * 0.1;
3003 [[x, 0.0, 0.0, 0.0], [-x, 0.0, 0.0, 0.0]]
3004 })
3005 .collect(),
3006 }
3007 }
3008
3009 fn drawn_session(
3010 step: fn(Tick) -> Option<(f32, bool)>,
3011 ) -> (Session<Probe>, DomainHandle<EuclideanR4>, Entity) {
3012 let mut session = Session::new(Probe::default(), SimConfig::default());
3013 let r4 = session
3014 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
3015 let prepared = session.prepare(lines());
3016 let material = session.add_material(Material::flat([1.0; 4]));
3017 let root = session.views().root();
3018 let mover = session.dispatch(|dispatch| {
3019 let eye = dispatch
3020 .spawn(SpawnBundle::new().at(r4, Pose::at(EYE_AT)))
3021 .unwrap();
3022 let mover = dispatch
3023 .spawn(
3024 SpawnBundle::new()
3025 .at(r4, Pose::at(OBJECT_AT))
3026 .instance(Instance::new(prepared, material)),
3027 )
3028 .unwrap();
3029 dispatch
3030 .domains
3031 .typed(r4)
3032 .unwrap()
3033 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
3034 .unwrap();
3035 mover
3036 });
3037 session.system(
3038 Phase::Simulation,
3039 "move",
3040 move |ctx: Ctx<'_, Probe>| -> Result<(), DomainError> {
3041 let Some((x, teleport)) = step(ctx.step.tick) else {
3042 return Ok(());
3043 };
3044 let domain = ctx.domains.typed(r4)?;
3045 let pose = Pose::at(OBJECT_AT + Vec4::X * x);
3046 if teleport {
3047 domain.teleport(mover, pose)
3048 } else {
3049 domain.set_pose(mover, pose)
3050 }
3051 },
3052 );
3053 (session, r4, mover)
3054 }
3055
3056 fn drawn_x(session: &mut Session<Probe>, publication: &mut Publication, alpha: f32) -> f32 {
3057 session.publish_at(publication, alpha).unwrap();
3058 publication.views[0].records.instances.rows()[0]
3059 .pose
3060 .coordinates[0]
3061 - OBJECT_AT.x
3062 }
3063
3064 #[test]
3065 fn a_moving_body_is_drawn_between_its_last_two_ticks() {
3066 let (mut session, _, _) = drawn_session(|tick| Some((tick.0 as f32 + 1.0, false)));
3067 let mut publication = Publication::default();
3068 session.tick().unwrap();
3069 assert_eq!(drawn_x(&mut session, &mut publication, 0.25), 0.25);
3070 assert_eq!(drawn_x(&mut session, &mut publication, 0.75), 0.75);
3071 assert_eq!(drawn_x(&mut session, &mut publication, 1.0), 1.0);
3072 session.tick().unwrap();
3073 assert_eq!(drawn_x(&mut session, &mut publication, 0.5), 1.5);
3074 }
3075
3076 #[test]
3077 fn jumps_are_drawn_at_their_destination_not_streaked_across_the_tick() {
3078 let (mut session, r4, mover) = drawn_session(|_| Some((10.0, true)));
3079 let mut publication = Publication::default();
3080 session.tick().unwrap();
3081 assert_eq!(drawn_x(&mut session, &mut publication, 0.25), 10.0);
3082 session
3083 .domains_mut()
3084 .typed(r4)
3085 .unwrap()
3086 .set_pose(mover, Pose::at(OBJECT_AT + Vec4::X * 5.0))
3087 .unwrap();
3088 assert_eq!(drawn_x(&mut session, &mut publication, 0.5), 5.0);
3089 }
3090
3091 #[test]
3092 fn a_body_that_stops_is_redrawn_at_rest_after_the_next_tick() {
3093 let (mut session, _, _) = drawn_session(|tick| (tick.0 == 0).then_some((1.0, false)));
3094 let mut publication = Publication::default();
3095 session.tick().unwrap();
3096 assert_eq!(drawn_x(&mut session, &mut publication, 0.5), 0.5);
3097 session.tick().unwrap();
3098 assert_eq!(drawn_x(&mut session, &mut publication, 0.5), 1.0);
3099 }
3100
3101 fn moving(tick: Tick) -> Option<(f32, bool)> {
3102 Some((tick.0 as f32 + 1.0, false))
3103 }
3104
3105 fn assert_cached_matches_fresh(
3106 session: &mut Session<Probe>,
3107 cached: &mut Publication,
3108 alpha: f32,
3109 ) {
3110 session.publish_at(cached, alpha).unwrap();
3111 let mut fresh = Publication::default();
3112 session.publish_at(&mut fresh, alpha).unwrap();
3113 let rows =
3114 |publication: &Publication| publication.views[0].records.instances.rows().to_vec();
3115 assert_eq!(rows(cached), rows(&fresh));
3116 }
3117
3118 #[test]
3119 fn a_command_move_between_ticks_is_drawn_at_once_and_the_cache_converges() {
3120 let (mut session, r4, mover) = drawn_session(|_| None);
3121 let mut publication = Publication::default();
3122 session.tick().unwrap();
3123 drawn_x(&mut session, &mut publication, 0.5);
3124 session.submit(Command::Chart(
3125 r4.id(),
3126 ChartCommand::Move {
3127 entity: mover,
3128 point: EuclideanR4.chart_point(OBJECT_AT + Vec4::X),
3129 },
3130 ));
3131 session.boundary(Input::default()).unwrap();
3132 assert_eq!(drawn_x(&mut session, &mut publication, 0.5), 1.0);
3133 session.tick().unwrap();
3134 assert_cached_matches_fresh(&mut session, &mut publication, 0.5);
3135 }
3136
3137 #[test]
3138 fn the_drawn_pose_is_the_published_instance_pose() {
3139 let (mut session, r4, mover) = drawn_session(moving);
3140 let mut publication = Publication::default();
3141 session.tick().unwrap();
3142 session.publish_at(&mut publication, 0.3).unwrap();
3143 let drawn = session.domains().read(r4).unwrap().drawn_pose(mover, 0.3);
3144 assert_eq!(
3145 drawn.map(|pose| EuclideanR4.chart_pose(&pose)),
3146 Some(publication.views[0].records.instances.rows()[0].pose)
3147 );
3148 }
3149
3150 #[test]
3151 fn image_of_reads_the_drawn_pose_not_the_tick_pose() {
3152 let (mut session, r4, mover) = drawn_session(moving);
3153 let mut publication = Publication::default();
3154 session.tick().unwrap();
3155 session.publish_at(&mut publication, 0.5).unwrap();
3156 let published = &publication.views[0];
3157 let domain = session.domains().get(r4.id()).unwrap();
3158 assert_eq!(
3159 domain.image_of(published.target.view, mover),
3160 Some(published.records.instances.rows()[0].image_point)
3161 );
3162 }
3163
3164 #[test]
3165 fn a_body_swapped_into_a_despawned_row_keeps_its_blend() {
3166 let (mut session, r4, doomed) = drawn_session(|_| None);
3167 let prepared = session.prepare(lines());
3168 let material = session.add_material(Material::flat([1.0; 4]));
3169 let swapped = session
3170 .dispatch(|dispatch| {
3171 dispatch.spawn(
3172 SpawnBundle::new()
3173 .at(r4, Pose::at(OBJECT_AT))
3174 .instance(Instance::new(prepared, material)),
3175 )
3176 })
3177 .unwrap();
3178 session.system(
3179 Phase::Simulation,
3180 "rise",
3181 move |ctx: Ctx<'_, Probe>| -> Result<(), DomainError> {
3182 let y = ctx.step.tick.0 as f32 + 1.0;
3183 ctx.domains
3184 .typed(r4)?
3185 .set_pose(swapped, Pose::at(OBJECT_AT + Vec4::Y * y))
3186 },
3187 );
3188 let mut publication = Publication::default();
3189 let drawn_y = |session: &mut Session<Probe>, publication: &mut Publication, alpha| {
3190 session.publish_at(publication, alpha).unwrap();
3191 let rows = publication.views[0].records.instances.rows();
3192 let record = rows.iter().find(|record| record.entity == swapped).unwrap();
3193 record.pose.coordinates[1] - OBJECT_AT.y
3194 };
3195 session.tick().unwrap();
3196 assert_eq!(drawn_y(&mut session, &mut publication, 0.25), 0.25);
3197 session
3198 .dispatch(|dispatch| dispatch.despawn(doomed))
3199 .unwrap();
3200 assert_eq!(drawn_y(&mut session, &mut publication, 0.75), 0.75);
3201 }
3202
3203 #[test]
3204 fn a_rotor_sign_flip_between_ticks_is_drawn_without_a_turn() {
3205 let (mut session, r4, mover) = drawn_session(|_| None);
3206 let turned = Rotor4::from_rotation_arc(Vec4::X, Vec4::new(1.0, 1.0, 0.0, 1.0).normalize());
3207 session
3208 .domains_mut()
3209 .typed(r4)
3210 .unwrap()
3211 .set_frame(mover, turned)
3212 .unwrap();
3213 session.system(
3214 Phase::Simulation,
3215 "flip",
3216 move |ctx: Ctx<'_, Probe>| -> Result<(), DomainError> {
3217 let domain = ctx.domains.typed(r4)?;
3218 let frame = domain
3219 .poses()
3220 .get(mover)
3221 .ok_or(DomainError::Stale(mover))?
3222 .frame;
3223 let minus = Rotor4 {
3224 s: -1.0,
3225 ..Rotor4::IDENTITY
3226 };
3227 domain.set_frame(mover, frame * minus)
3228 },
3229 );
3230 let mut publication = Publication::default();
3231 session.tick().unwrap();
3232 session.publish_at(&mut publication, 0.5).unwrap();
3233 let drawn = publication.views[0].records.instances.rows()[0].pose.frame;
3234 let expected = EuclideanR4.chart_pose(&Pose {
3235 point: OBJECT_AT,
3236 frame: turned,
3237 });
3238 for (column, expected) in drawn.iter().zip(expected.frame) {
3239 for (value, expected) in column.iter().zip(expected) {
3240 assert!((value - expected).abs() < 1e-5, "{drawn:?}");
3241 }
3242 }
3243 }
3244
3245 #[test]
3246 fn a_nan_alpha_draws_the_tick_pose() {
3247 let (mut session, _, _) = drawn_session(moving);
3248 let mut publication = Publication::default();
3249 session.tick().unwrap();
3250 assert_eq!(drawn_x(&mut session, &mut publication, f32::NAN), 1.0);
3251 }
3252
3253 #[test]
3254 fn a_moving_h3_body_is_not_rebuilt_between_ticks() {
3255 let mut session = Session::new(Probe::default(), SimConfig::default());
3256 let h3 = session.register_domain(
3257 DomainBuilder::new("h3", HyperbolicH3).tracked(LogCapacity::default()),
3258 );
3259 let stub = session.prepare(PreparedGeometry::Lines3 {
3260 segments: vec![[[0.1, 0.0, 0.0], [-0.1, 0.0, 0.0]]],
3261 });
3262 let material = session.add_material(Material::flat([1.0; 4]));
3263 let root = session.views().root();
3264 let body = session.dispatch(|dispatch| {
3265 let eye = dispatch
3266 .spawn(SpawnBundle::new().at(h3, Pose::at(Vec3::ZERO)))
3267 .unwrap();
3268 dispatch
3269 .domains
3270 .typed(h3)
3271 .unwrap()
3272 .add_view(ViewSpec::new(root, eye, Klein))
3273 .unwrap();
3274 dispatch
3275 .spawn(
3276 SpawnBundle::new()
3277 .at(h3, Pose::at(Vec3::new(0.0, 0.0, -0.5)))
3278 .instance(Instance::new(stub, material)),
3279 )
3280 .unwrap()
3281 });
3282 session.system(
3283 Phase::Simulation,
3284 "drift",
3285 move |ctx: Ctx<'_, Probe>| -> Result<(), DomainError> {
3286 ctx.domains.typed(h3)?.walk(body, Vec3::X, ctx.step.dt)
3287 },
3288 );
3289 let mut publication = Publication::default();
3290 session.tick().unwrap();
3291 session.publish_at(&mut publication, 0.2).unwrap();
3292 let built = publication.views[0].records.built();
3293 session.publish_at(&mut publication, 0.6).unwrap();
3294 assert_eq!(publication.views[0].records.built(), built);
3295 }
3296
3297 #[test]
3298 fn a_second_publication_redraws_a_stopped_body_at_rest() {
3299 let (mut session, _, _) = drawn_session(|tick| (tick.0 == 0).then_some((1.0, false)));
3300 let mut first = Publication::default();
3301 let mut second = Publication::default();
3302 session.tick().unwrap();
3303 drawn_x(&mut session, &mut first, 0.5);
3304 drawn_x(&mut session, &mut second, 0.5);
3305 session.tick().unwrap();
3306 assert_eq!(drawn_x(&mut session, &mut first, 0.5), 1.0);
3307 assert_eq!(drawn_x(&mut session, &mut second, 0.5), 1.0);
3308 }
3309
3310 #[test]
3311 fn a_still_world_never_rebuilds_its_view_between_ticks() {
3312 let (mut session, _, _) = drawn_session(|_| None);
3313 let mut publication = Publication::default();
3314 session.publish(&mut publication).unwrap();
3315 let built = publication.views[0].records.built;
3316 for alpha in [0.2, 0.6, 0.9] {
3317 session.tick().unwrap();
3318 drawn_x(&mut session, &mut publication, alpha);
3319 assert_eq!(publication.views[0].records.built, built);
3320 }
3321 }
3322
3323 #[test]
3324 fn a_restamp_only_publish_keeps_the_content_revision() {
3325 let (mut session, _, _) = drawn_session(|_| None);
3326 let mut publication = Publication::default();
3327 session.publish(&mut publication).unwrap();
3328 let content = publication.content();
3329 session.tick().unwrap();
3330 session.publish_at(&mut publication, 0.5).unwrap();
3331 assert_eq!(publication.content(), content);
3332 }
3333
3334 #[test]
3335 fn stale_view_identity_edits_leave_live_references_installed() {
3336 let mut session = Session::new(Probe::default(), SimConfig::default());
3337 let r4 = session.register_domain(DomainBuilder::new("r4", EuclideanR4));
3338 let root = session.views().root();
3339 let (eye, subject, stale, view) = session.dispatch(|dispatch| {
3340 let eye = dispatch
3341 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
3342 .unwrap();
3343 let subject = dispatch
3344 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
3345 .unwrap();
3346 let stale = dispatch
3347 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
3348 .unwrap();
3349 let view = dispatch
3350 .domains
3351 .typed(r4)
3352 .unwrap()
3353 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }).subject(subject))
3354 .unwrap();
3355 (eye, subject, stale, view)
3356 });
3357 session
3358 .dispatch(|dispatch| dispatch.despawn(stale))
3359 .unwrap();
3360
3361 let domain = session.domains_mut().typed(r4).unwrap();
3362 let revision = domain.view_revisions[view.index()];
3363 assert_eq!(domain.set_view_eye(view, eye), Ok(()));
3364 assert_eq!(domain.set_view_subject(view, Some(subject)), Ok(()));
3365 assert_eq!(domain.view_revisions[view.index()], revision);
3366 assert_eq!(
3367 domain.set_view_eye(view, stale),
3368 Err(DomainError::Stale(stale))
3369 );
3370 assert_eq!(
3371 domain.set_view_subject(view, Some(stale)),
3372 Err(DomainError::Stale(stale))
3373 );
3374 assert_eq!(domain.view_eye(view), Some(eye));
3375 assert_eq!(domain.view_subject(view), Some(subject));
3376 }
3377
3378 #[test]
3379 fn despawned_view_subject_stays_selected_and_publishes_an_empty_view() {
3380 let mut session = Session::new(Probe::default(), SimConfig::default());
3381 let r4 = session
3382 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
3383 let prepared = session.prepare(lines());
3384 let material = session.add_material(Material::flat([1.0; 4]));
3385 let root = session.views().root();
3386 let (subject, view) = session.dispatch(|dispatch| {
3387 let eye = dispatch
3388 .spawn(SpawnBundle::new().at(r4, Pose::at(EYE_AT)))
3389 .unwrap();
3390 let subject = dispatch
3391 .spawn(
3392 SpawnBundle::new()
3393 .at(r4, Pose::at(OBJECT_AT))
3394 .instance(Instance::new(prepared, material)),
3395 )
3396 .unwrap();
3397 dispatch
3398 .spawn(
3399 SpawnBundle::new()
3400 .at(r4, Pose::at(Vec4::new(1.0, 0.0, -4.0, 0.0)))
3401 .instance(Instance::new(prepared, material)),
3402 )
3403 .unwrap();
3404 let view = dispatch
3405 .domains
3406 .typed(r4)
3407 .unwrap()
3408 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }).subject(subject))
3409 .unwrap();
3410 (subject, view)
3411 });
3412 let mut publication = Publication::default();
3413 session.publish(&mut publication).unwrap();
3414 assert_eq!(publication.views[0].records.instances.rows().len(), 1);
3415
3416 session
3417 .dispatch(|dispatch| dispatch.despawn(subject))
3418 .unwrap();
3419 session.publish(&mut publication).unwrap();
3420 assert!(publication.views[0].records.instances.rows().is_empty());
3421 assert_eq!(
3422 session
3423 .domains_mut()
3424 .typed(r4)
3425 .unwrap()
3426 .set_view_subject(view, Some(subject)),
3427 Err(DomainError::Stale(subject))
3428 );
3429 assert_eq!(
3430 session.domains().read(r4).unwrap().view_subject(view),
3431 Some(subject)
3432 );
3433 }
3434
3435 #[test]
3436 fn cached_output_ranges_match_fresh_publication_across_view_lifecycle_changes() {
3437 let mut session = Session::new(Probe::default(), SimConfig::default());
3438 let r4 = session
3439 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
3440 let prepared = session.prepare(lines());
3441 let section_geometry = session.prepare(PreparedGeometry::Polytope4 {
3442 polytope: Polytope4::Tesseract,
3443 scale: 0.25,
3444 });
3445 let material = session.add_material(Material::flat([1.0; 4]));
3446 let root = session.views().root();
3447 let (edited, removed, alternate_eye, view, section_view) = session.dispatch(|dispatch| {
3448 let eye = dispatch
3449 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
3450 .unwrap();
3451 let alternate_eye = dispatch
3452 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::new(2.0, 0.0, 0.0, 0.0))))
3453 .unwrap();
3454 let instance = || Instance::new(prepared, material);
3455 let edited = dispatch
3456 .spawn(
3457 SpawnBundle::new()
3458 .at(r4, Pose::at(OBJECT_AT))
3459 .instance(Instance::new(section_geometry, material).sectioned(material)),
3460 )
3461 .unwrap();
3462 let removed = dispatch
3463 .spawn(
3464 SpawnBundle::new()
3465 .at(r4, Pose::at(Vec4::new(1.0, 0.0, -4.0, 0.0)))
3466 .instance(instance()),
3467 )
3468 .unwrap();
3469 let domain = dispatch.domains.typed(r4).unwrap();
3470 let view = domain
3471 .add_view(ViewSpec::new(root, eye, Projection4 { focal: 2.0 }))
3472 .unwrap();
3473 let mut section_spec = ViewSpec::new(root, eye, Section4 { w: 0.0 }).subject(edited);
3474 section_spec.edges = false;
3475 let section_view = domain.add_view(section_spec).unwrap();
3476 (edited, removed, alternate_eye, view, section_view)
3477 });
3478 let snapshot = session.snapshot().unwrap();
3479 let mut publication = Publication::default();
3480 let publish_and_compare = |session: &mut Session<Probe>, publication: &mut Publication| {
3481 session.publish(publication).unwrap();
3482 let mut fresh = Publication::default();
3483 session.publish(&mut fresh).unwrap();
3484 assert_eq!(publication.views.len(), fresh.views.len());
3485 for (cached, fresh) in publication.views.iter().zip(&fresh.views) {
3486 assert_eq!(cached.domain, fresh.domain);
3487 assert_eq!(cached.target, fresh.target);
3488 assert_eq!(cached.placement, fresh.placement);
3489 assert_eq!(
3490 cached.records.instances.rows(),
3491 fresh.records.instances.rows()
3492 );
3493 assert_eq!(cached.records.segments(), fresh.records.segments());
3494 assert_eq!(cached.records.triangles(), fresh.records.triangles());
3495 assert_eq!(cached.records.refusals(), fresh.records.refusals());
3496 }
3497 };
3498 publish_and_compare(&mut session, &mut publication);
3499 let initial_segments = publication.views[1].records.segments().len();
3500 let initial_triangles = publication.views[1].records.triangles().len();
3501 assert!(initial_segments > 0);
3502 assert!(initial_triangles > 0);
3503
3504 session
3505 .domains_mut()
3506 .typed(r4)
3507 .unwrap()
3508 .set_pose(edited, Pose::at(Vec4::new(0.5, 0.0, -4.0, 0.0)))
3509 .unwrap();
3510 publish_and_compare(&mut session, &mut publication);
3511 assert_eq!(
3512 publication.views[1].records.segments().len(),
3513 initial_segments
3514 );
3515 assert_eq!(
3516 publication.views[1].records.triangles().len(),
3517 initial_triangles
3518 );
3519
3520 session
3521 .domains_mut()
3522 .typed(r4)
3523 .unwrap()
3524 .set_pose(edited, Pose::at(Vec4::new(0.5, 0.0, -4.0, 1.0)))
3525 .unwrap();
3526 publish_and_compare(&mut session, &mut publication);
3527 assert!(publication.views[1].records.segments().is_empty());
3528 assert!(publication.views[1].records.triangles().is_empty());
3529 session
3530 .domains_mut()
3531 .typed(r4)
3532 .unwrap()
3533 .set_pose(edited, Pose::at(Vec4::new(0.5, 0.0, -4.0, 0.0)))
3534 .unwrap();
3535 publish_and_compare(&mut session, &mut publication);
3536 assert_eq!(
3537 publication.views[1].records.segments().len(),
3538 initial_segments
3539 );
3540 assert_eq!(
3541 publication.views[1].records.triangles().len(),
3542 initial_triangles
3543 );
3544
3545 session
3546 .domains_mut()
3547 .typed(r4)
3548 .unwrap()
3549 .view_mut(section_view)
3550 .unwrap()
3551 .set_mapping(Section4 { w: 0.5 });
3552 publish_and_compare(&mut session, &mut publication);
3553 assert!(publication.views[1].records.triangles().is_empty());
3554 assert_ne!(
3555 publication.views[1].records.triangles().len(),
3556 initial_triangles
3557 );
3558
3559 session
3560 .domains_mut()
3561 .typed(r4)
3562 .unwrap()
3563 .set_pose(edited, Pose::at(Vec4::new(0.5, 0.0, -4.0, 3.0)))
3564 .unwrap();
3565 publish_and_compare(&mut session, &mut publication);
3566 session
3567 .domains_mut()
3568 .typed(r4)
3569 .unwrap()
3570 .set_view_eye(view, alternate_eye)
3571 .unwrap();
3572 publish_and_compare(&mut session, &mut publication);
3573 session
3574 .dispatch(|dispatch| dispatch.despawn(removed))
3575 .unwrap();
3576 publish_and_compare(&mut session, &mut publication);
3577 assert!(publication.views[0]
3578 .records
3579 .instances
3580 .rows()
3581 .iter()
3582 .all(|record| record.entity != removed));
3583 session.restore(&snapshot).unwrap();
3584 publish_and_compare(&mut session, &mut publication);
3585 assert_eq!(
3586 publication.views[1].records.triangles().len(),
3587 initial_triangles
3588 );
3589 }
3590
3591 #[test]
3592 fn explicit_space_errors_are_reported_but_projection_clips_are_not() {
3593 let mut session = Session::new(Probe::default(), SimConfig::default());
3594 let r4 = session
3595 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
3596 let bad_x = session.prepare(PreparedGeometry::Lines4 {
3597 segments: vec![[[f32::NAN, 0.0, 0.0, 0.0], [0.0; 4]]],
3598 });
3599 let bad_y = session.prepare(PreparedGeometry::Lines4 {
3600 segments: vec![[[0.0, f32::MAX, 0.0, 0.0], [0.0; 4]]],
3601 });
3602 let empty = session.prepare(PreparedGeometry::Lines4 {
3603 segments: Vec::new(),
3604 });
3605 let material = session.add_material(Material::flat([1.0; 4]));
3606 let root = session.views().root();
3607 let (first, last, refusal_view, clipped_view) = session.dispatch(|dispatch| {
3608 let eye = dispatch
3609 .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
3610 .unwrap();
3611 let first = dispatch
3612 .spawn(
3613 SpawnBundle::new()
3614 .at(r4, Pose::at(Vec4::new(0.0, 0.0, -3.0, 0.0)))
3615 .instance(Instance::new(bad_x, material)),
3616 )
3617 .unwrap();
3618 let last = dispatch
3619 .spawn(
3620 SpawnBundle::new()
3621 .at(r4, Pose::at(Vec4::new(0.0, f32::MAX, -4.0, 0.0)))
3622 .instance(Instance::new(bad_y, material)),
3623 )
3624 .unwrap();
3625 let clipped = dispatch
3626 .spawn(
3627 SpawnBundle::new()
3628 .at(r4, Pose::at(Vec4::new(0.0, 0.0, -3.0, 3.0)))
3629 .instance(Instance::new(empty, material)),
3630 )
3631 .unwrap();
3632 let domain = dispatch.domains.typed(r4).unwrap();
3633 let refusal_view = domain
3634 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
3635 .unwrap();
3636 let mut clipped_spec =
3637 ViewSpec::new(root, eye, Projection4 { focal: 2.0 }).subject(clipped);
3638 clipped_spec.edges = false;
3639 clipped_spec.section_edges = false;
3640 clipped_spec.section_faces = false;
3641 let clipped_view = domain.add_view(clipped_spec).unwrap();
3642 (first, last, refusal_view, clipped_view)
3643 });
3644 let mut publication = Publication::default();
3645 session.publish(&mut publication).unwrap();
3646 let refusal_records = &publication
3647 .views
3648 .iter()
3649 .find(|published| published.target.view == refusal_view)
3650 .unwrap()
3651 .records;
3652 assert_eq!(
3653 refusal_records.refusals(),
3654 crate::view::ViewRefusals {
3655 count: 2,
3656 first: Some(crate::view::ViewRefusal {
3657 entity: first,
3658 error: DomainError::InvalidCoordinate("x"),
3659 source: RefusalSource::Segment,
3660 }),
3661 last: Some(crate::view::ViewRefusal {
3662 entity: last,
3663 error: DomainError::InvalidCoordinate("y"),
3664 source: RefusalSource::Segment,
3665 }),
3666 }
3667 );
3668 let clipped_records = &publication
3669 .views
3670 .iter()
3671 .find(|published| published.target.view == clipped_view)
3672 .unwrap()
3673 .records;
3674 assert_eq!(clipped_records.refusals().count, 0);
3675 assert!(clipped_records.instances.rows().is_empty());
3676 let before = (
3677 refusal_records.built(),
3678 refusal_records.instances.rows().len(),
3679 refusal_records.segments().len(),
3680 refusal_records.triangles().len(),
3681 refusal_records.refusals(),
3682 );
3683 session.publish(&mut publication).unwrap();
3684 let refusal_records = &publication
3685 .views
3686 .iter()
3687 .find(|published| published.target.view == refusal_view)
3688 .unwrap()
3689 .records;
3690 assert_eq!(
3691 (
3692 refusal_records.built(),
3693 refusal_records.instances.rows().len(),
3694 refusal_records.segments().len(),
3695 refusal_records.triangles().len(),
3696 refusal_records.refusals(),
3697 ),
3698 before
3699 );
3700 }
3701}