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}