1use crate::domain::{DomainError, DomainId};
2use crate::entity::Entity;
3use crate::store::StoreError;
4use crate::view::{ImageRay, ImageSpaceId, Placement, Vec3, ViewId};
5
6#[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 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 pub velocity: [f32; 3],
84}
85
86const PLANE_EPSILON: f32 = 1e-6;
87const VELOCITY_WINDOW_SECONDS: f64 = 0.1;
88const 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}