Skip to main content

pfx_editor/
snap.rs

1use std::collections::BTreeMap;
2
3use egui::Pos2;
4use glam::{DMat3, DMat4, DQuat, DVec3};
5use pfx_editor_viewport::gizmo::euler;
6use pfx_editor_viewport::lens::{slab, triangle};
7use pfx_editor_viewport::pick::matrix;
8use pfx_editor_viewport::{Lens, Ray};
9use pfx_load::scene::Scene;
10
11pub const STEP: f64 = 0.5;
12pub const LEAST_STEP: f64 = 0.01;
13pub const MOST_STEP: f64 = 100.0;
14pub const VERTEX_POINTS: f32 = 12.0;
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Snapping {
18    pub grid: bool,
19    pub step: f64,
20    pub surface: bool,
21    pub align: bool,
22    pub vertex: bool,
23}
24
25impl Default for Snapping {
26    fn default() -> Snapping {
27        Snapping {
28            grid: false,
29            step: STEP,
30            surface: true,
31            align: false,
32            vertex: false,
33        }
34    }
35}
36
37impl Snapping {
38    pub fn set_step(&mut self, step: f64) {
39        if step.is_finite() {
40            self.step = step.clamp(LEAST_STEP, MOST_STEP);
41        }
42    }
43
44    pub fn active(&self) -> bool {
45        self.grid || self.vertex || self.align || !self.surface
46    }
47}
48
49pub fn grid(point: DVec3, step: f64, axes: [bool; 3]) -> DVec3 {
50    DVec3::from_array(std::array::from_fn(|k| {
51        if axes[k] && step > 0.0 {
52            (point[k] / step).round() * step
53        } else {
54            point[k]
55        }
56    }))
57}
58
59pub fn across(normal: DVec3) -> [bool; 3] {
60    std::array::from_fn(|k| normal[k].abs() < 0.9)
61}
62
63pub fn upright(normal: DVec3) -> DVec3 {
64    let Some(normal) = normal.try_normalize() else {
65        return DVec3::ZERO;
66    };
67    let turn = DQuat::from_rotation_arc(DVec3::Y, normal);
68    let degrees = euler(DMat3::from_quat(turn));
69    DVec3::from_array(degrees.to_array().map(|value| {
70        let rounded = (value * 1e6).round() / 1e6;
71        if rounded == 0.0 { 0.0 } else { rounded }
72    }))
73}
74
75#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct Hit {
77    pub object: usize,
78    pub point: DVec3,
79    pub normal: DVec3,
80    pub distance: f64,
81}
82
83#[derive(Clone, Debug)]
84struct Part {
85    object: usize,
86    draw: usize,
87    model: DMat4,
88    inverse: DMat4,
89    low: DVec3,
90    high: DVec3,
91}
92
93#[derive(Clone, Debug)]
94pub struct Surfaces {
95    draws: pfx_load::scene::Draws,
96    parts: Vec<Part>,
97}
98
99fn local_bounds(positions: &[[f32; 3]]) -> (DVec3, DVec3) {
100    let mut low = DVec3::INFINITY;
101    let mut high = DVec3::NEG_INFINITY;
102    for p in positions {
103        let p = DVec3::new(f64::from(p[0]), f64::from(p[1]), f64::from(p[2]));
104        low = low.min(p);
105        high = high.max(p);
106    }
107    if low.is_finite() && high.is_finite() {
108        (low, high)
109    } else {
110        (DVec3::ZERO, DVec3::ZERO)
111    }
112}
113
114pub fn corners(low: DVec3, high: DVec3) -> [DVec3; 8] {
115    std::array::from_fn(|k| {
116        DVec3::new(
117            if k & 1 == 0 { low.x } else { high.x },
118            if k & 2 == 0 { low.y } else { high.y },
119            if k & 4 == 0 { low.z } else { high.z },
120        )
121    })
122}
123
124impl Surfaces {
125    pub fn of(scene: &Scene) -> Surfaces {
126        let names: BTreeMap<&str, usize> = scene
127            .objects
128            .iter()
129            .enumerate()
130            .map(|(index, object)| (object.name.as_str(), index))
131            .collect();
132        let draws = scene.draws();
133        let mut extents: BTreeMap<[u8; 32], (DVec3, DVec3)> = BTreeMap::new();
134        let parts = draws
135            .items
136            .iter()
137            .enumerate()
138            .filter(|(_, draw)| draw.shadow.drawn())
139            .filter_map(|(index, draw)| {
140                let object = *names.get(draw.object.as_str())?;
141                let (low, high) = *extents
142                    .entry(draw.geometry.hash)
143                    .or_insert_with(|| local_bounds(&draw.geometry.positions));
144                let model = matrix(draw.model);
145                Some(Part {
146                    object,
147                    draw: index,
148                    inverse: model.inverse(),
149                    model,
150                    low,
151                    high,
152                })
153            })
154            .collect();
155        Surfaces { draws, parts }
156    }
157
158    pub fn cast(&self, ray: &Ray) -> Option<Hit> {
159        self.cast_except(ray, &[])
160    }
161
162    pub fn cast_except(&self, ray: &Ray, skip: &[usize]) -> Option<Hit> {
163        let mut nearest: Option<Hit> = None;
164        for part in &self.parts {
165            if skip.contains(&part.object) {
166                continue;
167            }
168            let local = Ray {
169                origin: part.inverse.transform_point3(ray.origin),
170                direction: part.inverse.transform_vector3(ray.direction),
171            };
172            let Some(entry) = slab(&local, part.low, part.high) else {
173                continue;
174            };
175            if nearest.is_some_and(|best| entry > best.distance) {
176                continue;
177            }
178            let geometry = &self.draws.items[part.draw].geometry;
179            let corner = |index: u32| {
180                let p = geometry.positions[index as usize];
181                DVec3::new(f64::from(p[0]), f64::from(p[1]), f64::from(p[2]))
182            };
183            for face in geometry.indices.chunks_exact(3) {
184                let three = [corner(face[0]), corner(face[1]), corner(face[2])];
185                let Some(distance) = triangle(&local, three) else {
186                    continue;
187                };
188                if nearest.is_some_and(|best| distance >= best.distance) {
189                    continue;
190                }
191                let face_normal = (three[1] - three[0]).cross(three[2] - three[0]);
192                let world = part.inverse.transpose().transform_vector3(face_normal);
193                let Some(mut normal) = world.try_normalize() else {
194                    continue;
195                };
196                if normal.dot(ray.direction) > 0.0 {
197                    normal = -normal;
198                }
199                nearest = Some(Hit {
200                    object: part.object,
201                    point: part.model.transform_point3(local.at(distance)),
202                    normal,
203                    distance,
204                });
205            }
206        }
207        nearest
208    }
209
210    pub fn vertex(&self, lens: &Lens, at: Pos2, reach: f32, skip: &[usize]) -> Option<DVec3> {
211        let mut best: Option<(f32, DVec3)> = None;
212        for part in &self.parts {
213            if skip.contains(&part.object) {
214                continue;
215            }
216            let projected: Vec<Pos2> = corners(part.low, part.high)
217                .iter()
218                .filter_map(|corner| lens.project(part.model.transform_point3(*corner)))
219                .collect();
220            if projected.len() < 8 {
221                continue;
222            }
223            let rect = egui::Rect::from_points(&projected).expand(reach);
224            if !rect.contains(at) {
225                continue;
226            }
227            let geometry = &self.draws.items[part.draw].geometry;
228            for p in &geometry.positions {
229                let world = part.model.transform_point3(DVec3::new(
230                    f64::from(p[0]),
231                    f64::from(p[1]),
232                    f64::from(p[2]),
233                ));
234                let Some(seen) = lens.project(world) else {
235                    continue;
236                };
237                let away = seen.distance(at);
238                if away <= reach && best.is_none_or(|(near, _)| away < near) {
239                    best = Some((away, world));
240                }
241            }
242        }
243        best.map(|(_, point)| point)
244    }
245
246    pub fn boxes(&self, object: usize) -> Vec<[DVec3; 8]> {
247        self.parts
248            .iter()
249            .filter(|part| part.object == object)
250            .map(|part| corners(part.low, part.high).map(|at| part.model.transform_point3(at)))
251            .collect()
252    }
253}
254
255pub fn ground(ray: &Ray) -> Option<DVec3> {
256    if ray.direction.y.abs() < 1e-9 {
257        return None;
258    }
259    let distance = -ray.origin.y / ray.direction.y;
260    (distance > 0.0).then(|| ray.at(distance))
261}
262
263pub fn plane(ray: &Ray, origin: DVec3, normal: DVec3) -> Option<DVec3> {
264    let facing = ray.direction.dot(normal);
265    if facing.abs() < 1e-9 {
266        return None;
267    }
268    let distance = (origin - ray.origin).dot(normal) / facing;
269    (distance > 0.0).then(|| ray.at(distance))
270}
271
272pub fn lift(bounds: [[f32; 3]; 2], rotate: DVec3, normal: DVec3) -> DVec3 {
273    let turn = pfx_editor_viewport::gizmo::rotation(rotate);
274    let low = DVec3::from_array(bounds[0].map(f64::from));
275    let high = DVec3::from_array(bounds[1].map(f64::from));
276    let below = corners(low, high)
277        .iter()
278        .map(|corner| -turn.transform_point3(*corner).dot(normal))
279        .fold(f64::NEG_INFINITY, f64::max);
280    if below.is_finite() {
281        normal * below
282    } else {
283        DVec3::ZERO
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn grid_snapping_rounds_only_the_axes_asked() {
293        let point = DVec3::new(1.26, 0.74, -0.24);
294        assert_eq!(grid(point, 0.5, [true; 3]), DVec3::new(1.5, 0.5, -0.0));
295        assert_eq!(
296            grid(point, 0.5, across(DVec3::Y)),
297            DVec3::new(1.5, 0.74, -0.0)
298        );
299        assert_eq!(grid(point, 0.0, [true; 3]), point);
300        let mut snapping = Snapping::default();
301        snapping.set_step(0.0);
302        assert_eq!(snapping.step, LEAST_STEP);
303        snapping.set_step(f64::NAN);
304        assert_eq!(snapping.step, LEAST_STEP);
305        assert!(!Snapping::default().active());
306    }
307
308    #[test]
309    fn upright_turns_y_onto_the_normal() {
310        assert_eq!(upright(DVec3::Y), DVec3::ZERO);
311        let turned = upright(DVec3::X);
312        let up = pfx_editor_viewport::gizmo::rotation(turned).transform_vector3(DVec3::Y);
313        assert!((up - DVec3::X).length() < 1e-9, "{turned} {up}");
314        let slope = DVec3::new(0.0, 1.0, 1.0).normalize();
315        let up = pfx_editor_viewport::gizmo::rotation(upright(slope)).transform_vector3(DVec3::Y);
316        assert!((up - slope).length() < 1e-6);
317    }
318
319    #[test]
320    fn the_ground_and_a_plane_meet_a_ray() {
321        let ray = Ray {
322            origin: DVec3::new(0.0, 2.0, 0.0),
323            direction: DVec3::new(0.0, -1.0, 1.0).normalize(),
324        };
325        let hit = ground(&ray).unwrap();
326        assert!((hit - DVec3::new(0.0, 0.0, 2.0)).length() < 1e-9);
327        let up = Ray {
328            direction: DVec3::Y,
329            ..ray
330        };
331        assert!(ground(&up).is_none());
332        let wall = plane(&ray, DVec3::new(0.0, 0.0, 1.0), DVec3::Z).unwrap();
333        assert!((wall - DVec3::new(0.0, 1.0, 1.0)).length() < 1e-9);
334    }
335
336    #[test]
337    fn lift_sets_a_box_on_the_surface() {
338        let unit = [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]];
339        let up = lift(unit, DVec3::ZERO, DVec3::Y);
340        assert!((up - DVec3::new(0.0, 0.5, 0.0)).length() < 1e-9);
341        let standing = [[-0.5, 0.0, -0.5], [0.5, 1.0, 0.5]];
342        assert!(lift(standing, DVec3::ZERO, DVec3::Y).length() < 1e-9);
343        let side = lift(standing, upright(DVec3::X), DVec3::X);
344        assert!(side.length() < 1e-9, "{side}");
345    }
346}