Skip to main content

pfx_editor_viewport/
gizmo.rs

1use egui::{Align2, Color32, CornerRadius, Painter, Pos2, Rect, Shape, Stroke, StrokeKind, Vec2};
2use glam::{DMat3, DMat4, DQuat, DVec3};
3use pfx_editor_style::Theme;
4use pfx_load::scene::Scene;
5
6use crate::camera::wide;
7use crate::lens::{Lens, Ray};
8use crate::pick::matrix;
9
10pub const REACH: f32 = 80.0;
11pub const SLACK: f32 = 6.0;
12const BOX: f32 = 5.0;
13const RING_STEPS: usize = 64;
14const START: f64 = 0.2;
15const PLANE: [f64; 2] = [0.25, 0.45];
16const LEAST_SCALE: f64 = 0.001;
17const LEAST_AREA: f32 = 16.0;
18const AXES: [DVec3; 3] = [DVec3::X, DVec3::Y, DVec3::Z];
19const NAMES: [&str; 3] = ["x", "y", "z"];
20pub const CARD: &str = "a card faces the camera; its rotate is ignored";
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum Mode {
24    #[default]
25    Move,
26    Rotate,
27    Scale,
28}
29
30impl Mode {
31    pub fn word(self) -> &'static str {
32        match self {
33            Mode::Move => "move",
34            Mode::Rotate => "rotate",
35            Mode::Scale => "scale",
36        }
37    }
38
39    pub fn key(self) -> &'static str {
40        match self {
41            Mode::Move => "at",
42            Mode::Rotate => "rotate",
43            Mode::Scale => "scale",
44        }
45    }
46
47    fn index(self) -> usize {
48        match self {
49            Mode::Move => 0,
50            Mode::Rotate => 1,
51            Mode::Scale => 2,
52        }
53    }
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum Space {
58    #[default]
59    World,
60    Local,
61}
62
63impl Space {
64    pub fn word(self) -> &'static str {
65        match self {
66            Space::World => "world",
67            Space::Local => "local",
68        }
69    }
70
71    pub fn other(self) -> Space {
72        match self {
73            Space::World => Space::Local,
74            Space::Local => Space::World,
75        }
76    }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq)]
80pub struct Snap {
81    pub distance: f64,
82    pub degrees: f64,
83    pub scale: f64,
84}
85
86impl Default for Snap {
87    fn default() -> Snap {
88        Snap {
89            distance: 0.01,
90            degrees: 5.0,
91            scale: 0.05,
92        }
93    }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum Handle {
98    Axis(usize),
99    Plane(usize),
100    Ring(usize),
101    Centre,
102}
103
104pub fn rotation(degrees: DVec3) -> DMat4 {
105    let radians = degrees * (std::f64::consts::PI / 180.0);
106    DMat4::from_rotation_z(radians.z)
107        * DMat4::from_rotation_y(radians.y)
108        * DMat4::from_rotation_x(radians.x)
109}
110
111#[derive(Clone, Debug, PartialEq)]
112pub struct Pose {
113    pub name: String,
114    pub at: DVec3,
115    pub rotate: DVec3,
116    pub scale: DVec3,
117    pub parent: DMat4,
118    pub locks: [Option<String>; 3],
119}
120
121impl Pose {
122    pub fn of(scene: &Scene, index: usize) -> Option<Pose> {
123        let object = scene.objects.get(index)?;
124        let parent = object
125            .parent
126            .as_deref()
127            .and_then(|name| scene.object(name))
128            .map_or(DMat4::IDENTITY, |parent| matrix(parent.model));
129        let mut locks: [Option<String>; 3] = Default::default();
130        if object.face_camera {
131            locks[1] = Some(CARD.to_string());
132        }
133        Some(Pose {
134            name: object.name.clone(),
135            at: DVec3::from_array(object.at.map(wide)),
136            rotate: DVec3::from_array(object.rotate.map(wide)),
137            scale: DVec3::from_array(object.scale.map(wide)),
138            parent,
139            locks,
140        })
141    }
142
143    pub fn local(&self) -> DMat4 {
144        DMat4::from_translation(self.at) * rotation(self.rotate) * DMat4::from_scale(self.scale)
145    }
146
147    pub fn world(&self) -> DMat4 {
148        self.parent * self.local()
149    }
150
151    pub fn centre(&self) -> DVec3 {
152        self.world().transform_point3(DVec3::ZERO)
153    }
154
155    pub fn lock(&self, mode: Mode) -> Option<&str> {
156        self.locks[mode.index()].as_deref()
157    }
158}
159
160pub fn poses(scene: &Scene) -> Vec<Pose> {
161    (0..scene.objects.len())
162        .filter_map(|index| Pose::of(scene, index))
163        .collect()
164}
165
166pub fn axes(pose: &Pose, mode: Mode, space: Space) -> [DVec3; 3] {
167    if mode != Mode::Scale && space == Space::World {
168        return AXES;
169    }
170    let linear = DMat3::from_mat4(pose.world());
171    std::array::from_fn(|k| linear.col(k).try_normalize().unwrap_or(AXES[k]))
172}
173
174fn length(lens: &Lens, centre: DVec3) -> f64 {
175    f64::from(REACH) * lens.metres_per_point(centre)
176}
177
178#[derive(Clone, Debug, PartialEq)]
179struct Part {
180    handle: Handle,
181    points: Vec<Pos2>,
182    closed: bool,
183}
184
185fn parts(lens: &Lens, pose: &Pose, mode: Mode, space: Space) -> Vec<Part> {
186    let centre = pose.centre();
187    let axes = axes(pose, mode, space);
188    let reach = length(lens, centre);
189    let mut parts = Vec::new();
190    let mut add = |handle: Handle, points: &[DVec3], closed: bool| {
191        let projected: Option<Vec<Pos2>> =
192            points.iter().map(|point| lens.project(*point)).collect();
193        if let Some(points) = projected
194            && (!closed || points.len() != 4 || area(&points) >= LEAST_AREA)
195        {
196            parts.push(Part {
197                handle,
198                points,
199                closed,
200            });
201        }
202    };
203    match mode {
204        Mode::Move => {
205            for k in 0..3 {
206                let (i, j) = ((k + 1) % 3, (k + 2) % 3);
207                let corner = |a: f64, b: f64| centre + (axes[i] * a + axes[j] * b) * reach;
208                add(
209                    Handle::Plane(k),
210                    &[
211                        corner(PLANE[0], PLANE[0]),
212                        corner(PLANE[1], PLANE[0]),
213                        corner(PLANE[1], PLANE[1]),
214                        corner(PLANE[0], PLANE[1]),
215                    ],
216                    true,
217                );
218            }
219            for (k, axis) in axes.iter().enumerate() {
220                add(
221                    Handle::Axis(k),
222                    &[centre + *axis * reach * START, centre + *axis * reach],
223                    false,
224                );
225            }
226        }
227        Mode::Rotate => {
228            for k in 0..3 {
229                let (u, v) = (axes[(k + 1) % 3], axes[(k + 2) % 3]);
230                let ring: Vec<DVec3> = (0..RING_STEPS)
231                    .map(|step| {
232                        let angle = std::f64::consts::TAU * step as f64 / RING_STEPS as f64;
233                        centre + (u * angle.cos() + v * angle.sin()) * reach
234                    })
235                    .collect();
236                add(Handle::Ring(k), &ring, true);
237            }
238        }
239        Mode::Scale => {
240            for (k, axis) in axes.iter().enumerate() {
241                add(Handle::Axis(k), &[centre, centre + *axis * reach], false);
242            }
243        }
244    }
245    parts
246}
247
248fn area(points: &[Pos2]) -> f32 {
249    let twice: f32 = points
250        .iter()
251        .zip(points.iter().cycle().skip(1))
252        .map(|(a, b)| a.x * b.y - b.x * a.y)
253        .sum();
254    twice.abs() / 2.0
255}
256
257fn segment_distance(at: Pos2, a: Pos2, b: Pos2) -> f32 {
258    let ab = b - a;
259    let t = if ab.length_sq() > 0.0 {
260        ((at - a).dot(ab) / ab.length_sq()).clamp(0.0, 1.0)
261    } else {
262        0.0
263    };
264    (at - (a + ab * t)).length()
265}
266
267fn inside(at: Pos2, points: &[Pos2]) -> bool {
268    let mut sign = 0.0_f32;
269    for (index, a) in points.iter().enumerate() {
270        let b = points[(index + 1) % points.len()];
271        let cross = (b - *a).x * (at - *a).y - (b - *a).y * (at - *a).x;
272        if cross != 0.0 {
273            if sign != 0.0 && cross.signum() != sign {
274                return false;
275            }
276            sign = cross.signum();
277        }
278    }
279    true
280}
281
282fn distance(part: &Part, at: Pos2) -> f32 {
283    if part.closed && part.points.len() == 4 && inside(at, &part.points) {
284        return 0.0;
285    }
286    let count = if part.closed {
287        part.points.len()
288    } else {
289        part.points.len() - 1
290    };
291    (0..count)
292        .map(|index| {
293            segment_distance(
294                at,
295                part.points[index],
296                part.points[(index + 1) % part.points.len()],
297            )
298        })
299        .fold(f32::INFINITY, f32::min)
300}
301
302pub fn hit(lens: &Lens, pose: &Pose, mode: Mode, space: Space, at: Pos2) -> Option<Handle> {
303    if mode == Mode::Scale
304        && let Some(centre) = lens.project(pose.centre())
305        && (at.x - centre.x).abs().max((at.y - centre.y).abs()) <= BOX + 2.0 + SLACK
306    {
307        return Some(Handle::Centre);
308    }
309    let mut best: Option<(f32, Handle)> = None;
310    for part in parts(lens, pose, mode, space) {
311        let gap = distance(&part, at);
312        if gap <= SLACK && best.is_none_or(|(nearest, _)| gap < nearest) {
313            best = Some((gap, part.handle));
314        }
315    }
316    best.map(|(_, handle)| handle)
317}
318
319fn closest_on_line(point: DVec3, direction: DVec3, ray: &Ray) -> Option<f64> {
320    let b = direction.dot(ray.direction);
321    let denominator = 1.0 - b * b;
322    if denominator < 1e-9 {
323        return None;
324    }
325    let w = point - ray.origin;
326    Some((b * ray.direction.dot(w) - direction.dot(w)) / denominator)
327}
328
329fn on_plane(point: DVec3, normal: DVec3, ray: &Ray) -> Option<DVec3> {
330    let facing = ray.direction.dot(normal);
331    if facing.abs() < 1e-9 {
332        return None;
333    }
334    let distance = (point - ray.origin).dot(normal) / facing;
335    Some(ray.at(distance))
336}
337
338fn snapped(value: f64, step: f64) -> f64 {
339    if step > 0.0 {
340        (value / step).round() * step
341    } else {
342        value
343    }
344}
345
346pub fn euler(rotation: DMat3) -> DVec3 {
347    let r20 = rotation.col(0).z;
348    let (x, y, z) = if r20.abs() < 1.0 - 1e-12 {
349        (
350            rotation.col(1).z.atan2(rotation.col(2).z),
351            (-r20).asin(),
352            rotation.col(0).y.atan2(rotation.col(0).x),
353        )
354    } else {
355        (
356            0.0,
357            (-r20.clamp(-1.0, 1.0)).asin(),
358            (-rotation.col(1).x).atan2(rotation.col(1).y),
359        )
360    };
361    DVec3::new(x.to_degrees(), y.to_degrees(), z.to_degrees())
362}
363
364fn nearest_turn(angle: f64, near: f64) -> f64 {
365    angle + ((near - angle) / 360.0).round() * 360.0
366}
367
368pub fn turn(pose: &Pose, axis: DVec3, degrees: f64) -> DVec3 {
369    let spin = DQuat::from_axis_angle(axis, degrees.to_radians());
370    let (_, parent, _) = pose.parent.to_scale_rotation_translation();
371    let local = parent.inverse() * spin * parent;
372    let before = DMat3::from_mat4(rotation(pose.rotate));
373    let after = euler(DMat3::from_quat(local) * before);
374    DVec3::from_array(std::array::from_fn(|k| {
375        nearest_turn(after[k], pose.rotate[k])
376    }))
377}
378
379#[derive(Clone, Debug)]
380pub struct Drag {
381    pub object: usize,
382    pub handle: Handle,
383    pub mode: Mode,
384    pub start: Pose,
385    pub pose: Pose,
386    axes: [DVec3; 3],
387    lens: Lens,
388    origin: Pos2,
389    last: Pos2,
390    turned: f64,
391}
392
393impl Drag {
394    pub fn begin(
395        object: usize,
396        pose: &Pose,
397        mode: Mode,
398        space: Space,
399        handle: Handle,
400        origin: Pos2,
401        lens: Lens,
402    ) -> Drag {
403        Drag {
404            object,
405            handle,
406            mode,
407            start: pose.clone(),
408            pose: pose.clone(),
409            axes: axes(pose, mode, space),
410            lens,
411            origin,
412            last: origin,
413            turned: 0.0,
414        }
415    }
416
417    pub fn to(&mut self, at: Pos2, snap: Option<Snap>) {
418        let mut pose = self.start.clone();
419        match self.mode {
420            Mode::Move => pose.at = self.moved(at, snap),
421            Mode::Rotate => pose.rotate = self.turned(at, snap),
422            Mode::Scale => pose.scale = self.scaled(at, snap),
423        }
424        self.pose = pose;
425    }
426
427    fn moved(&self, at: Pos2, snap: Option<Snap>) -> DVec3 {
428        let centre = self.start.centre();
429        let shift = match self.handle {
430            Handle::Axis(k) => {
431                let axis = self.axes[k];
432                let from = self.lens.ray(self.on_axis(self.origin, axis));
433                let to = self.lens.ray(self.on_axis(at, axis));
434                match (
435                    closest_on_line(centre, axis, &from),
436                    closest_on_line(centre, axis, &to),
437                ) {
438                    (Some(a), Some(b)) => axis * (b - a),
439                    _ => DVec3::ZERO,
440                }
441            }
442            Handle::Plane(k) => {
443                let normal = self.axes[k];
444                match (
445                    on_plane(centre, normal, &self.lens.ray(self.origin)),
446                    on_plane(centre, normal, &self.lens.ray(at)),
447                ) {
448                    (Some(a), Some(b)) => b - a,
449                    _ => DVec3::ZERO,
450                }
451            }
452            Handle::Ring(_) | Handle::Centre => DVec3::ZERO,
453        };
454        let moved = self.start.parent.inverse().transform_point3(centre + shift);
455        let Some(snap) = snap else {
456            return moved;
457        };
458        let before = self.start.at;
459        DVec3::from_array(std::array::from_fn(|k| {
460            before[k] + snapped(moved[k] - before[k], snap.distance)
461        }))
462    }
463
464    fn turned(&mut self, at: Pos2, snap: Option<Snap>) -> DVec3 {
465        let Handle::Ring(k) = self.handle else {
466            return self.start.rotate;
467        };
468        let centre = self.start.centre();
469        if let Some(middle) = self.lens.project(centre) {
470            let (a, b) = (self.last - middle, at - middle);
471            if a.length() > 1.0 && b.length() > 1.0 {
472                let cross = f64::from(a.x * b.y - a.y * b.x);
473                let dot = f64::from(a.dot(b));
474                self.turned -= cross.atan2(dot);
475            }
476        }
477        self.last = at;
478        let axis = self.axes[k];
479        let sign = if axis.dot(self.lens.toward_eye(centre)) >= 0.0 {
480            1.0
481        } else {
482            -1.0
483        };
484        let mut degrees = (self.turned * sign).to_degrees();
485        if let Some(snap) = snap {
486            degrees = snapped(degrees, snap.degrees);
487        }
488        turn(&self.start, axis, degrees)
489    }
490
491    fn scaled(&self, at: Pos2, snap: Option<Snap>) -> DVec3 {
492        let moved = at - self.origin;
493        let centre = self.start.centre();
494        let factor = match self.handle {
495            Handle::Axis(k) => {
496                let reach = length(&self.lens, centre);
497                match (
498                    self.lens.project(centre),
499                    self.lens.project(centre + self.axes[k] * reach),
500                ) {
501                    (Some(a), Some(b)) if (b - a).length() > 1.0 => {
502                        1.0 + f64::from(moved.dot(b - a) / (b - a).length_sq())
503                    }
504                    _ => 1.0,
505                }
506            }
507            Handle::Centre => 1.0 + f64::from((moved.x - moved.y) / REACH),
508            Handle::Plane(_) | Handle::Ring(_) => 1.0,
509        };
510        let factor = factor.max(LEAST_SCALE);
511        let mut scale = self.start.scale;
512        let touched: Vec<usize> = match self.handle {
513            Handle::Axis(k) => vec![k],
514            _ => vec![0, 1, 2],
515        };
516        for k in touched {
517            scale[k] *= factor;
518            if let Some(snap) = snap {
519                let size = snapped(scale[k].abs(), snap.scale).max(snap.scale);
520                scale[k] = size.copysign(self.start.scale[k]);
521            }
522        }
523        scale
524    }
525
526    pub fn label(&self) -> String {
527        let what = match self.handle {
528            Handle::Axis(k) => format!(" along {}", NAMES[k]),
529            Handle::Ring(k) => format!(" about {}", NAMES[k]),
530            Handle::Plane(k) => format!(" in {}{}", NAMES[(k + 1) % 3], NAMES[(k + 2) % 3]),
531            Handle::Centre => String::new(),
532        };
533        format!("{} {}{what}", self.mode.word(), self.start.name)
534    }
535
536    fn on_axis(&self, at: Pos2, axis: DVec3) -> Pos2 {
537        let centre = self.start.centre();
538        let reach = length(&self.lens, centre);
539        match (
540            self.lens.project(centre),
541            self.lens.project(centre + axis * reach),
542        ) {
543            (Some(a), Some(b)) if (b - a).length() > 1.0 => {
544                let direction = (b - a).normalized();
545                a + direction * (at - a).dot(direction)
546            }
547            _ => at,
548        }
549    }
550}
551
552#[derive(Clone, Debug, PartialEq)]
553pub struct Finished {
554    pub object: usize,
555    pub label: String,
556    pub start: Pose,
557    pub pose: Pose,
558}
559
560#[derive(Clone, Debug, Default)]
561pub struct Gizmo {
562    pub mode: Mode,
563    pub space: Space,
564    pub snap: Snap,
565    poses: Vec<Pose>,
566    drag: Option<Drag>,
567    held: Option<(usize, Pose)>,
568    hover: Option<Handle>,
569    swallow: bool,
570}
571
572impl Gizmo {
573    pub fn set_poses(&mut self, poses: Vec<Pose>) {
574        self.poses = poses;
575        self.held = None;
576    }
577
578    pub fn poses(&self) -> &[Pose] {
579        &self.poses
580    }
581
582    pub fn dragging(&self) -> Option<&Drag> {
583        self.drag.as_ref()
584    }
585
586    pub fn swallowing(&self) -> bool {
587        self.swallow
588    }
589
590    pub fn hover(&mut self, handle: Option<Handle>) {
591        self.hover = handle;
592    }
593
594    pub fn hovered(&self) -> Option<Handle> {
595        self.hover
596    }
597
598    pub fn shown(&self, object: usize) -> Option<&Pose> {
599        match (&self.drag, &self.held) {
600            (Some(drag), _) if drag.object == object => Some(&drag.pose),
601            (_, Some((held, pose))) if *held == object => Some(pose),
602            _ => self.poses.get(object),
603        }
604    }
605
606    pub fn hit(&self, lens: &Lens, object: usize, at: Pos2) -> Option<Handle> {
607        let pose = self.shown(object)?;
608        hit(lens, pose, self.mode, self.space, at)
609    }
610
611    pub fn begin(&mut self, object: usize, at: Pos2, lens: Lens) -> bool {
612        let Some(pose) = self.shown(object).cloned() else {
613            return false;
614        };
615        if pose.lock(self.mode).is_some() {
616            return false;
617        }
618        let Some(handle) = hit(&lens, &pose, self.mode, self.space, at) else {
619            return false;
620        };
621        self.drag = Some(Drag::begin(
622            object, &pose, self.mode, self.space, handle, at, lens,
623        ));
624        self.held = None;
625        true
626    }
627
628    pub fn to(&mut self, at: Pos2, snap: bool) {
629        let snap = snap.then_some(self.snap);
630        if let Some(drag) = &mut self.drag {
631            drag.to(at, snap);
632        }
633    }
634
635    pub fn finish(&mut self) -> Option<Finished> {
636        self.swallow = false;
637        let drag = self.drag.take()?;
638        if drag.pose == drag.start {
639            return None;
640        }
641        self.held = Some((drag.object, drag.pose.clone()));
642        Some(Finished {
643            object: drag.object,
644            label: drag.label(),
645            start: drag.start,
646            pose: drag.pose,
647        })
648    }
649
650    pub fn cancel(&mut self) -> bool {
651        let cancelled = self.drag.take().is_some();
652        self.swallow |= cancelled;
653        cancelled
654    }
655
656    pub fn drop_held(&mut self) {
657        self.held = None;
658    }
659
660    pub fn preview(&self) -> Option<(usize, &Pose)> {
661        match (&self.drag, &self.held) {
662            (Some(drag), _) => Some((drag.object, &drag.pose)),
663            (None, Some((object, pose))) => Some((*object, pose)),
664            (None, None) => None,
665        }
666    }
667
668    pub fn draw(&self, painter: &Painter, lens: &Lens, object: usize, theme: &Theme) {
669        let Some(pose) = self.shown(object) else {
670            return;
671        };
672        let lock = pose.lock(self.mode);
673        let active = self.drag.as_ref().map(|drag| drag.handle);
674        let lit = active.or(self.hover);
675        let tones = colours(theme);
676        let colour = |handle: Handle, k: usize| {
677            if lock.is_some() {
678                theme.dim
679            } else if lit == Some(handle) {
680                theme.accent_bright
681            } else {
682                tones[k]
683            }
684        };
685        let Some(centre) = lens.project(pose.centre()) else {
686            return;
687        };
688        let font = theme.font(theme.size_code);
689        for part in parts(lens, pose, self.mode, self.space) {
690            match part.handle {
691                Handle::Plane(k) => {
692                    let tone = colour(part.handle, k);
693                    painter.add(Shape::convex_polygon(
694                        part.points.clone(),
695                        tone.gamma_multiply(0.3),
696                        Stroke::new(theme.stroke, tone),
697                    ));
698                }
699                Handle::Axis(k) => {
700                    let tone = colour(part.handle, k);
701                    let (from, end) = (part.points[0], part.points[1]);
702                    painter.line_segment([from, end], Stroke::new(2.0_f32, tone));
703                    let direction = (end - centre).normalized();
704                    if self.mode == Mode::Move {
705                        let side = Vec2::new(-direction.y, direction.x) * 5.0;
706                        painter.add(Shape::convex_polygon(
707                            vec![end + direction * 12.0, end + side, end - side],
708                            tone,
709                            Stroke::NONE,
710                        ));
711                    } else {
712                        painter.rect_filled(
713                            Rect::from_center_size(end, Vec2::splat(BOX * 2.0)),
714                            CornerRadius::ZERO,
715                            tone,
716                        );
717                    }
718                    painter.text(
719                        end + direction * 22.0,
720                        Align2::CENTER_CENTER,
721                        NAMES[k],
722                        font.clone(),
723                        tone,
724                    );
725                }
726                Handle::Ring(k) => {
727                    let tone = colour(part.handle, k);
728                    let mut points = part.points.clone();
729                    points.push(points[0]);
730                    painter.add(Shape::line(points, Stroke::new(2.0_f32, tone)));
731                }
732                Handle::Centre => {}
733            }
734        }
735        let middle = if lock.is_some() {
736            theme.dim
737        } else if self.mode == Mode::Scale && lit == Some(Handle::Centre) {
738            theme.accent_bright
739        } else {
740            theme.accent
741        };
742        if self.mode == Mode::Scale {
743            painter.rect_stroke(
744                Rect::from_center_size(centre, Vec2::splat(BOX * 2.0 + 4.0)),
745                CornerRadius::ZERO,
746                Stroke::new(1.5_f32, middle),
747                StrokeKind::Middle,
748            );
749        }
750        painter.circle_filled(centre, 3.0, middle);
751        if let Some(reason) = lock {
752            let text = painter.layout(
753                reason.to_string(),
754                theme.font(theme.size_small),
755                theme.text2,
756                (lens.image().width() * 0.5).max(160.0),
757            );
758            let at = centre + Vec2::new(14.0, 14.0);
759            let band = Rect::from_min_size(at, text.size() + Vec2::new(16.0, 8.0));
760            painter.rect_filled(band, CornerRadius::same(theme.radius), theme.well);
761            painter.rect_stroke(
762                band,
763                CornerRadius::same(theme.radius),
764                Stroke::new(theme.stroke, theme.line),
765                StrokeKind::Inside,
766            );
767            painter.galley(at + Vec2::new(8.0, 4.0), text, theme.text2);
768        }
769    }
770
771    pub fn badge(&self, painter: &Painter, image: Rect, theme: &Theme) {
772        let space = if self.mode == Mode::Scale {
773            Space::Local
774        } else {
775            self.space
776        };
777        let text = painter.layout_no_wrap(
778            format!("{} ยท {}", self.mode.word(), space.word()),
779            theme.font(theme.size_small),
780            theme.accent,
781        );
782        let size = text.size() + Vec2::new(16.0, 8.0);
783        let band = Rect::from_min_size(
784            Pos2::new(
785                image.right() - theme.section_gap - size.x,
786                image.top() + theme.section_gap,
787            ),
788            size,
789        );
790        painter.rect_filled(band, CornerRadius::same(theme.radius), theme.well);
791        painter.galley(band.min + Vec2::new(8.0, 4.0), text, theme.accent);
792    }
793}
794
795pub fn colours(theme: &Theme) -> [Color32; 3] {
796    [theme.error, theme.ok, theme.secondary]
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802    use crate::camera::Eye;
803
804    fn front() -> Eye {
805        Eye {
806            position: [0.0, 0.0, 4.0],
807            target: [0.0, 0.0, 0.0],
808            up: [0.0, 1.0, 0.0],
809            fov_y_deg: 90.0,
810            shift: [0.0, 0.0],
811            ortho_height: None,
812        }
813    }
814
815    fn image() -> Rect {
816        Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(800.0, 400.0))
817    }
818
819    fn pose(at: [f64; 3]) -> Pose {
820        Pose {
821            name: "thing".into(),
822            at: DVec3::from_array(at),
823            rotate: DVec3::ZERO,
824            scale: DVec3::ONE,
825            parent: DMat4::IDENTITY,
826            locks: Default::default(),
827        }
828    }
829
830    fn close(a: DVec3, b: DVec3) -> bool {
831        (a - b).length() < 1e-9
832    }
833
834    fn snap() -> Option<Snap> {
835        Some(Snap::default())
836    }
837
838    #[test]
839    fn the_rotation_is_the_engines_model() {
840        for degrees in [[10.0, 20.0, 30.0], [-45.0, 80.0, 170.0], [90.0, 0.0, -90.0]] {
841            let at = [0.5_f32, -1.0, 2.0];
842            let scale = [1.0_f32, 2.0, 0.5];
843            let engine = matrix(pfx_load::scene::model(
844                at,
845                degrees.map(|v: f64| v as f32),
846                scale,
847            ));
848            let ours = Pose {
849                at: DVec3::new(0.5, -1.0, 2.0),
850                rotate: DVec3::from_array(degrees),
851                scale: DVec3::new(1.0, 2.0, 0.5),
852                ..pose([0.0; 3])
853            }
854            .local();
855            assert!(ours.abs_diff_eq(engine, 1e-5), "{degrees:?}");
856        }
857    }
858
859    #[test]
860    fn the_x_arrow_projects_where_the_hand_says() {
861        let lens = Lens::new(&front(), image());
862        let thing = pose([0.0, 0.0, 0.0]);
863        let reach = length(&lens, thing.centre());
864        assert!((reach - 80.0 * 8.0 / 400.0).abs() < 1e-12);
865        let found = parts(&lens, &thing, Mode::Move, Space::World);
866        let x = found
867            .iter()
868            .find(|part| part.handle == Handle::Axis(0))
869            .unwrap();
870        assert!((x.points[1] - Pos2::new(480.0, 200.0)).length() < 1e-3);
871        assert!((x.points[0] - Pos2::new(416.0, 200.0)).length() < 1e-3);
872    }
873
874    #[test]
875    fn hits_find_the_handle_within_the_slack() {
876        let lens = Lens::new(&front(), image());
877        let thing = pose([0.0, 0.0, 0.0]);
878        let at = |x, y| hit(&lens, &thing, Mode::Move, Space::World, Pos2::new(x, y));
879        assert_eq!(at(470.0, 204.0), Some(Handle::Axis(0)));
880        assert_eq!(at(470.0, 210.0), None);
881        assert_eq!(at(400.0, 130.0), Some(Handle::Axis(1)));
882        assert_eq!(at(428.0, 172.0), Some(Handle::Plane(2)));
883        assert_eq!(at(300.0, 300.0), None);
884        let ring = |x, y| hit(&lens, &thing, Mode::Rotate, Space::World, Pos2::new(x, y));
885        assert_eq!(
886            ring(400.0 + 80.0 * 0.6, 200.0 - 80.0 * 0.8),
887            Some(Handle::Ring(2))
888        );
889        let scale = |x, y| hit(&lens, &thing, Mode::Scale, Space::World, Pos2::new(x, y));
890        assert_eq!(scale(402.0, 199.0), Some(Handle::Centre));
891        assert_eq!(scale(478.0, 202.0), Some(Handle::Axis(0)));
892    }
893
894    #[test]
895    fn a_drag_of_n_points_along_x_moves_by_n_times_the_metres_per_point() {
896        let lens = Lens::new(&front(), image());
897        let thing = pose([0.0, 0.0, 0.0]);
898        let mut drag = Drag::begin(
899            0,
900            &thing,
901            Mode::Move,
902            Space::World,
903            Handle::Axis(0),
904            Pos2::new(460.0, 200.0),
905            lens,
906        );
907        drag.to(Pos2::new(510.0, 230.0), None);
908        let metres = 50.0 * 8.0 / 400.0;
909        assert!(close(drag.pose.at, DVec3::new(metres, 0.0, 0.0)));
910        assert_eq!(drag.label(), "move thing along x");
911    }
912
913    #[test]
914    fn a_plane_drag_follows_the_pointer_in_the_plane() {
915        let lens = Lens::new(&front(), image());
916        let thing = pose([0.0, 0.0, 0.0]);
917        let mut drag = Drag::begin(
918            0,
919            &thing,
920            Mode::Move,
921            Space::World,
922            Handle::Plane(2),
923            Pos2::new(428.0, 172.0),
924            lens,
925        );
926        drag.to(Pos2::new(438.0, 152.0), None);
927        let mpp = 8.0 / 400.0;
928        assert!(close(drag.pose.at, DVec3::new(10.0 * mpp, 20.0 * mpp, 0.0)));
929    }
930
931    #[test]
932    fn a_child_moves_in_its_parents_frame() {
933        let lens = Lens::new(&front(), image());
934        let mut child = pose([0.1, 0.0, 0.0]);
935        child.parent = rotation(DVec3::new(0.0, 0.0, 90.0)) * DMat4::from_scale(DVec3::splat(2.0));
936        let centre = child.centre();
937        assert!(close(centre, DVec3::new(0.0, 0.2, 0.0)));
938        let start = lens
939            .project(centre + DVec3::X * length(&lens, centre) * 0.6)
940            .unwrap();
941        let mut drag = Drag::begin(
942            1,
943            &child,
944            Mode::Move,
945            Space::World,
946            Handle::Axis(0),
947            start,
948            lens,
949        );
950        drag.to(start + Vec2::new(20.0, 0.0), None);
951        let metres = 20.0 * 8.0 / 400.0;
952        assert!(close(
953            drag.pose.centre(),
954            centre + DVec3::new(metres, 0.0, 0.0)
955        ));
956        assert!(close(drag.pose.at, DVec3::new(0.1, -metres / 2.0, 0.0)));
957    }
958
959    #[test]
960    fn a_ring_drag_turns_by_the_angle_swept_around_the_centre() {
961        let lens = Lens::new(&front(), image());
962        let thing = pose([0.0, 0.0, 0.0]);
963        let mut drag = Drag::begin(
964            0,
965            &thing,
966            Mode::Rotate,
967            Space::World,
968            Handle::Ring(2),
969            Pos2::new(480.0, 200.0),
970            lens,
971        );
972        drag.to(Pos2::new(457.0, 143.0), None);
973        drag.to(Pos2::new(400.0, 120.0), None);
974        assert!(close(drag.pose.rotate, DVec3::new(0.0, 0.0, 90.0)));
975        let mut snapped = Drag::begin(
976            0,
977            &thing,
978            Mode::Rotate,
979            Space::World,
980            Handle::Ring(2),
981            Pos2::new(480.0, 200.0),
982            lens,
983        );
984        snapped.to(Pos2::new(480.0, 190.0), snap());
985        assert!(close(snapped.pose.rotate, DVec3::new(0.0, 0.0, 5.0)));
986    }
987
988    #[test]
989    fn a_ring_seen_from_behind_turns_the_other_way() {
990        let mut back = front();
991        back.position = [0.0, 0.0, -4.0];
992        let lens = Lens::new(&back, image());
993        let thing = pose([0.0, 0.0, 0.0]);
994        let start = lens.project(DVec3::new(0.16, 0.0, 0.0)).unwrap();
995        let mut drag = Drag::begin(
996            0,
997            &thing,
998            Mode::Rotate,
999            Space::World,
1000            Handle::Ring(2),
1001            start,
1002            lens,
1003        );
1004        drag.to(Pos2::new(400.0, 120.0), None);
1005        assert!(close(drag.pose.rotate, DVec3::new(0.0, 0.0, 90.0)));
1006    }
1007
1008    #[test]
1009    fn local_rotation_turns_about_the_objects_own_axis() {
1010        let mut thing = pose([0.0, 0.0, 0.0]);
1011        thing.rotate = DVec3::new(0.0, 0.0, -90.0);
1012        let local = axes(&thing, Mode::Rotate, Space::Local);
1013        assert!(close(local[0], DVec3::new(0.0, -1.0, 0.0)));
1014        let turned = turn(&thing, local[0], 30.0);
1015        let expected = rotation(DVec3::new(30.0, 0.0, -90.0));
1016        assert!(rotation(turned).abs_diff_eq(expected, 1e-9), "{turned:?}");
1017        let world = turn(&thing, DVec3::X, 30.0);
1018        let expected = rotation(DVec3::new(30.0, 0.0, 0.0)) * rotation(DVec3::new(0.0, 0.0, -90.0));
1019        assert!(rotation(world).abs_diff_eq(expected, 1e-9));
1020    }
1021
1022    #[test]
1023    fn euler_round_trips_the_rotation() {
1024        for degrees in [
1025            [10.0, 20.0, 30.0],
1026            [-45.0, 80.0, 170.0],
1027            [90.0, 0.0, -90.0],
1028            [0.0, 90.0, 30.0],
1029            [0.0, -90.0, 0.0],
1030        ] {
1031            let turned = rotation(DVec3::from_array(degrees));
1032            let back = euler(DMat3::from_mat4(turned));
1033            assert!(
1034                rotation(back).abs_diff_eq(turned, 1e-9),
1035                "{degrees:?} -> {back:?}"
1036            );
1037        }
1038    }
1039
1040    #[test]
1041    fn scale_handles_grow_by_the_dragged_share_of_the_reach() {
1042        let lens = Lens::new(&front(), image());
1043        let thing = pose([0.0, 0.0, 0.0]);
1044        let mut drag = Drag::begin(
1045            0,
1046            &thing,
1047            Mode::Scale,
1048            Space::World,
1049            Handle::Axis(0),
1050            Pos2::new(478.0, 200.0),
1051            lens,
1052        );
1053        drag.to(Pos2::new(518.0, 230.0), None);
1054        assert!(close(drag.pose.scale, DVec3::new(1.5, 1.0, 1.0)));
1055        let mut uniform = Drag::begin(
1056            0,
1057            &thing,
1058            Mode::Scale,
1059            Space::World,
1060            Handle::Centre,
1061            Pos2::new(400.0, 200.0),
1062            lens,
1063        );
1064        uniform.to(Pos2::new(420.0, 180.0), None);
1065        assert!(close(uniform.pose.scale, DVec3::splat(1.5)));
1066        uniform.to(Pos2::new(408.0, 200.0), snap());
1067        assert!(close(uniform.pose.scale, DVec3::splat(1.1)));
1068        uniform.to(Pos2::new(0.0, 400.0), None);
1069        assert!(close(uniform.pose.scale, DVec3::splat(LEAST_SCALE)));
1070    }
1071
1072    #[test]
1073    fn snapping_moves_in_whole_steps_from_where_it_was() {
1074        let lens = Lens::new(&front(), image());
1075        let thing = pose([0.0375, 0.0, 0.0]);
1076        let start = lens.project(thing.centre() + DVec3::X * 0.8).unwrap();
1077        let mut drag = Drag::begin(
1078            0,
1079            &thing,
1080            Mode::Move,
1081            Space::World,
1082            Handle::Axis(0),
1083            start,
1084            lens,
1085        );
1086        drag.to(start + Vec2::new(1.32, 0.0), None);
1087        assert!((drag.pose.at.x - (0.0375 + 0.0264)).abs() < 1e-6);
1088        drag.to(start + Vec2::new(1.32, 0.0), snap());
1089        assert!(close(drag.pose.at, DVec3::new(0.0375 + 0.03, 0.0, 0.0)));
1090        let fine = Snap {
1091            distance: 0.001,
1092            ..Snap::default()
1093        };
1094        drag.to(start + Vec2::new(1.32, 0.0), Some(fine));
1095        assert!(close(drag.pose.at, DVec3::new(0.0375 + 0.026, 0.0, 0.0)));
1096    }
1097
1098    #[test]
1099    fn a_card_locks_its_rotate_with_a_reason() {
1100        let mut card = pose([0.0, 0.0, 0.0]);
1101        card.locks[1] = Some(CARD.to_string());
1102        let mut gizmo = Gizmo::default();
1103        gizmo.set_poses(vec![card]);
1104        gizmo.mode = Mode::Rotate;
1105        let lens = Lens::new(&front(), image());
1106        assert!(!gizmo.begin(0, Pos2::new(480.0, 200.0), lens));
1107        gizmo.mode = Mode::Move;
1108        assert!(gizmo.begin(0, Pos2::new(470.0, 200.0), lens));
1109    }
1110
1111    #[test]
1112    fn a_gizmo_holds_its_preview_after_release_until_new_poses_come() {
1113        let mut gizmo = Gizmo::default();
1114        gizmo.set_poses(vec![pose([0.5, 0.0, 0.0])]);
1115        let lens = Lens::new(&front(), image());
1116        let centre = lens.project(DVec3::new(0.5, 0.0, 0.0)).unwrap();
1117        let start = centre + Vec2::new(60.0, 0.0);
1118        assert!(gizmo.begin(0, start, lens));
1119        gizmo.to(start + Vec2::new(25.0, 0.0), false);
1120        assert!((gizmo.preview().unwrap().1.at.x - 1.0).abs() < 1e-9);
1121        let finished = gizmo.finish().unwrap();
1122        assert_eq!(finished.label, "move thing along x");
1123        assert!((finished.pose.at.x - 1.0).abs() < 1e-9);
1124        assert!((gizmo.shown(0).unwrap().at.x - 1.0).abs() < 1e-9);
1125        gizmo.set_poses(vec![pose([0.5, 0.0, 0.0])]);
1126        assert_eq!(gizmo.preview(), None);
1127        assert!(gizmo.begin(0, start, lens));
1128        gizmo.to(start + Vec2::new(25.0, 0.0), false);
1129        assert!(gizmo.cancel());
1130        assert!(gizmo.swallowing());
1131        assert_eq!(gizmo.preview(), None);
1132        assert!(gizmo.finish().is_none());
1133        assert!(!gizmo.swallowing());
1134    }
1135}