Skip to main content

loam_runtime/
domain.rs

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/// Coordinates and a frame in one chart of the domain; unused entries are zero.
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct ChartPose {
100    pub chart: ChartId,
101    pub coordinates: [f32; 4],
102    /// Column-major, matching glam's `Mat4` and WGSL's `mat4x4<f32>`.
103    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    /// The perimeter's line material; `None` publishes no section for a geometry that could be cut.
167    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    /// False when `blend` returns `to`, so publication draws tick poses and skips the blend bookkeeping.
286    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    /// Reads the leading coordinates the space has and ignores the rest.
297    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    /// The pose drawn a fraction `t` of the way through a tick that moved `from` to `to`.
310    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    /// Arc length along `ray` into the metric ball of `radius` around `center`: zero from inside, `None` on a miss.
319    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    // Helgason, Differential Geometry, Lie Groups, and Symmetric Spaces, 1978, Ch. IV ยง3: the transvection along a geodesic is parallel transport along it.
488    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
540// Shepperd, Quaternion from Rotation Matrix, J. Guidance and Control 1(3), 1978.
541fn 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    // Geodesic R0 exp(t log(R0^-1 R1)), the SO(4) slerp (Shoemake 1985, section 3).
593    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    // An Iso3H frame has no geodesic blend yet, so H3 draws each tick's pose until it does.
699    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        // Ratcliffe, Foundations of Hyperbolic Manifolds, 2006, ยง3.2: cosh d(x, y) = -โŸจx, yโŸฉ on the hyperboloid.
748        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    // A Mat3 frame has no geodesic blend yet, so blended spaces draw each tick's pose until it does.
939    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
1096// Haines, "Fast Ray-Convex Polyhedron Intersection", Graphics Gems II (1991).
1097fn 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
1522/// Engine-owned work inside one domain: `step` runs in the simulation phase's domain-step entry, `release` at dispatch before the stores forget the entity, and `apply` gets first offer of a chart command.
1523pub 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    /// Refuses whatever `restore` would refuse, changing nothing; the session calls it on every facility before the first one restores.
1560    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    /// `Some` claims the command with its outcome; `None` leaves it to the domain.
1567    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    /// Inside a tick the move is drawn blended across that tick; outside one it is drawn at once.
1821    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    /// Moves without drawing the poses in between, for a jump such as a respawn.
1827    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    /// The pose `publish_at(alpha)` draws: blended across the last tick when only that tick wrote the pose, otherwise the current pose.
1855    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}