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}