Skip to main content

loam_runtime/
bridge.rs

1use crate::domain::{DomainError, DomainId};
2use crate::entity::Entity;
3use crate::store::StoreError;
4use crate::view::{ImageRay, ImageSpaceId, Placement, Vec3, ViewId};
5
6/// Linked from its anchor to the source view's eye; releasing either entity unlinks it and unplaces `image` inside that despawn.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct Bridge {
9    pub source: DomainId,
10    pub view: ViewId,
11    pub image: ImageSpaceId,
12}
13
14pub struct BridgeSpec {
15    pub anchor: Entity,
16    pub into: ImageSpaceId,
17    pub source: DomainId,
18    pub view: ViewId,
19    pub placement: Placement,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum BridgeError {
24    UnknownDomain(DomainId),
25    UnknownView(ViewId),
26    UnknownImage(ImageSpaceId),
27    Nonlinear(&'static str),
28    Domain(DomainError),
29    Link(StoreError),
30}
31
32impl From<DomainError> for BridgeError {
33    fn from(error: DomainError) -> Self {
34        Self::Domain(error)
35    }
36}
37
38impl From<StoreError> for BridgeError {
39    fn from(error: StoreError) -> Self {
40        Self::Link(error)
41    }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum DragError {
46    NoPick,
47    NotGrabbed,
48    /// The pointer ray runs parallel to the drag plane, so no point on it is nearer than another.
49    Ambiguous(&'static str),
50    Domain(DomainError),
51}
52
53impl From<DomainError> for DragError {
54    fn from(error: DomainError) -> Self {
55        Self::Domain(error)
56    }
57}
58
59pub(crate) const VELOCITY_SAMPLES: usize = 12;
60
61#[derive(Clone, Copy, Debug, PartialEq)]
62pub struct Drag {
63    pub entity: Entity,
64    pub domain: DomainId,
65    pub view: ViewId,
66    pub image: ImageSpaceId,
67    pub(crate) plane: [f32; 3],
68    pub(crate) normal: [f32; 3],
69    pub(crate) depth: f32,
70    pub(crate) center: [f32; 3],
71    pub(crate) at: [f32; 3],
72    pub(crate) samples: [([f32; 3], f64); VELOCITY_SAMPLES],
73    pub(crate) sampled: usize,
74    pub(crate) opened: f64,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq)]
78pub struct DragRelease {
79    pub entity: Entity,
80    pub domain: DomainId,
81    pub image: ImageSpaceId,
82    /// In the view's image space, per second.
83    pub velocity: [f32; 3],
84}
85
86const PLANE_EPSILON: f32 = 1e-6;
87const VELOCITY_WINDOW_SECONDS: f64 = 0.1;
88// Keeps the ring from filling with one frame of high-rate pointer events.
89const SAMPLE_SPACING_SECONDS: f64 = 0.01;
90
91impl Drag {
92    pub fn image_point(&self) -> [f32; 3] {
93        self.at
94    }
95
96    pub(crate) fn plane_at(&self, eye: [f32; 3], forward: [f32; 3]) -> [f32; 3] {
97        (Vec3::from(eye) + Vec3::from(forward) * self.depth).to_array()
98    }
99
100    pub(crate) fn meet(ray: &ImageRay, plane: [f32; 3], normal: [f32; 3]) -> Option<[f32; 3]> {
101        let normal = Vec3::from(normal);
102        let along = Vec3::from(ray.direction).dot(normal);
103        if along.abs() <= PLANE_EPSILON {
104            return None;
105        }
106        Some(ray.at((Vec3::from(plane) - Vec3::from(ray.origin)).dot(normal) / along))
107    }
108
109    pub(crate) fn moved(&self, at: [f32; 3]) -> [f32; 3] {
110        (Vec3::from(self.center) + Vec3::from(at) - Vec3::from(self.plane)).to_array()
111    }
112
113    pub(crate) fn sample(&mut self, at: [f32; 3], time: f64) {
114        if time < self.newest_time() {
115            self.at = at;
116            return;
117        }
118        if time - self.opened < SAMPLE_SPACING_SECONDS {
119            self.samples[self.sampled.saturating_sub(1) % VELOCITY_SAMPLES] = (at, time);
120        } else {
121            self.samples[self.sampled % VELOCITY_SAMPLES] = (at, time);
122            self.sampled += 1;
123            self.opened = time;
124        }
125        self.at = at;
126    }
127
128    pub(crate) fn newest_time(&self) -> f64 {
129        self.samples[self.sampled.saturating_sub(1) % VELOCITY_SAMPLES].1
130    }
131
132    pub(crate) fn velocity(&self) -> [f32; 3] {
133        let held = self.sampled.min(VELOCITY_SAMPLES);
134        if held < 2 {
135            return [0.0; 3];
136        }
137        let newest = self.samples[(self.sampled - 1) % VELOCITY_SAMPLES];
138        let mut oldest = newest;
139        for back in 1..held {
140            let sample = self.samples[(self.sampled - 1 - back) % VELOCITY_SAMPLES];
141            if back > 1 && newest.1 - sample.1 > VELOCITY_WINDOW_SECONDS {
142                break;
143            }
144            oldest = sample;
145        }
146        let elapsed = (newest.1 - oldest.1) as f32;
147        if elapsed <= 0.0 {
148            return [0.0; 3];
149        }
150        ((Vec3::from(newest.0) - Vec3::from(oldest.0)) / elapsed).to_array()
151    }
152
153    pub(crate) fn released(&self, velocity: [f32; 3]) -> DragRelease {
154        DragRelease {
155            entity: self.entity,
156            domain: self.domain,
157            image: self.image,
158            velocity,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use loam_math::{EuclideanR4, Iso3};
166    use loam_time::alloc::bytes_allocated_by;
167
168    use super::*;
169    use crate::command::SpawnBundle;
170    use crate::domain::{DomainBuilder, Instance, Pose};
171    use crate::session::{Material, PreparedGeometry, Session, SimConfig};
172    use crate::store::LogCapacity;
173    use crate::view::{Rigid, Section4, Vec4, ViewSpec};
174
175    crate::stores! {
176        #[derive(Default)]
177        pub struct Probe {
178            tags: Store<u8>,
179        }
180    }
181
182    #[test]
183    fn a_warmed_pick_through_two_bridges_allocates() {
184        let mut session = Session::new(Probe::default(), SimConfig::default());
185        let r4 = session
186            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
187        let stub = session.prepare(PreparedGeometry::Lines4 {
188            segments: vec![[[0.5, 0.0, 0.0, 0.0], [-0.5, 0.0, 0.0, 0.0]]],
189        });
190        let material = session.add_material(Material::flat([1.0; 4]));
191        let root = session.views().root();
192        let (near, far, anchor) = session.dispatch(|d| {
193            let eye = d
194                .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
195                .unwrap();
196            d.spawn(
197                SpawnBundle::new()
198                    .at(r4, Pose::at(Vec4::new(1.0, 0.0, -4.0, 0.0)))
199                    .instance(Instance::new(stub, material)),
200            )
201            .unwrap();
202            let anchor = d.spawn(SpawnBundle::new()).unwrap();
203            let domain = d.domains.typed(r4).unwrap();
204            let near = domain
205                .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
206                .unwrap();
207            let far = domain
208                .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
209                .unwrap();
210            (near, far, anchor)
211        });
212        let placement = Placement::Rigid(Rigid {
213            pose: Iso3::from_translation(Vec3::new(0.0, 0.0, -0.5)),
214            scale: 0.25,
215        });
216        let outer = session
217            .bridge(BridgeSpec {
218                anchor,
219                into: root,
220                source: r4.id(),
221                view: near,
222                placement,
223            })
224            .unwrap();
225        let middle = session.bridges().get(outer).unwrap().data.image;
226        session
227            .bridge(BridgeSpec {
228                anchor,
229                into: middle,
230                source: r4.id(),
231                view: far,
232                placement,
233            })
234            .unwrap();
235        for _ in 0..8 {
236            assert!(session.pick([0.4330127, 0.0]).is_some());
237        }
238
239        let bytes = bytes_allocated_by(|| {
240            for _ in 0..16 {
241                assert!(session.pick([0.4330127, 0.0]).is_some());
242            }
243        })
244        .expect("the counting allocator is installed");
245        assert_eq!(
246            bytes, 0,
247            "16 warmed picks through two bridges asked the allocator for {bytes} bytes"
248        );
249    }
250}