Skip to main content

mirage_engine/mesh/
instance.rs

1use core::marker::PhantomData;
2use core::time::Duration;
3
4use crate::animation::{AnimationStates, Animator, Running};
5use crate::math::{Mat3, Mat4, Vec3, Vec4};
6use crate::mesh::{Animation, Clip, Frame, Mesh, Part, Posing};
7use crate::surface_style::{NoSurfaceStyles, Styled, SurfaceStyle, SurfaceStyleId, SurfaceStyles};
8use crate::{Holds, Material, Transform, View};
9
10/// The fade of a draw until it sets one: its slots keep the alpha they
11/// resolved to.
12const OPAQUE: f32 = 1.0;
13
14/// A draw as the engine records it: a mesh, its position, its turn, the
15/// part of its textures it samples, the seat of the style it is drawn
16/// with, the fade over its slots, the pose it is drawn in, and the
17/// materials that override its slot defaults.
18#[derive(Clone, Debug)]
19pub(crate) struct Draw<M> {
20    mesh: M,
21    transform: Transform,
22    facing: Facing,
23    roll: f32,
24    frame: Frame,
25    style: Option<Styled>,
26    fade: f32,
27    posed: Option<Running>,
28    paints: Paints,
29}
30
31impl<M> Draw<M> {
32    /// The same draw with its mesh turned into the set `T` that holds it.
33    pub(crate) fn into_set<T: From<M>>(self) -> Draw<T> {
34        let Self {
35            mesh,
36            transform,
37            facing,
38            roll,
39            frame,
40            style,
41            fade,
42            posed,
43            paints,
44        } = self;
45        Draw {
46            mesh: mesh.into(),
47            transform,
48            facing,
49            roll,
50            frame,
51            style,
52            fade,
53            posed,
54            paints,
55        }
56    }
57
58    /// The same draw keyed by `mesh`, the id the game thread resolved its
59    /// mesh to, which is how it crosses to the display thread.
60    pub(crate) fn keyed<T>(self, mesh: T) -> Draw<T> {
61        let Self {
62            mesh: _,
63            transform,
64            facing,
65            roll,
66            frame,
67            style,
68            fade,
69            posed,
70            paints,
71        } = self;
72        Draw {
73            mesh,
74            transform,
75            facing,
76            roll,
77            frame,
78            style,
79            fade,
80            posed,
81            paints,
82        }
83    }
84
85    pub(crate) fn mesh(&self) -> &M {
86        &self.mesh
87    }
88
89    /// The draw's position and turn, seen from `view`.
90    pub(crate) fn placement(&self, view: View) -> Placement {
91        Placement {
92            transform: self.facing.applied(self.transform, view, self.roll),
93            faced: self.faced(),
94        }
95    }
96
97    /// Where the draw is positioned, whatever turn a view applies to it.
98    pub(crate) fn anchor(&self) -> Vec3 {
99        self.transform.matrix().w_axis.truncate()
100    }
101
102    /// The part of its textures the draw samples.
103    pub(crate) fn window(&self) -> Frame {
104        self.frame
105    }
106
107    /// Whether the draw is turned by the camera rather than by its own
108    /// transform.
109    pub(crate) fn faced(&self) -> bool {
110        self.facing != Facing::AsPlaced
111    }
112
113    /// The style the draw is drawn with, or nothing where none was set.
114    pub(crate) fn styled(&self) -> Option<Styled> {
115        self.style
116    }
117
118    /// The pose the draw holds at `now`, read over the clips of the mesh
119    /// drawn; nothing where it is drawn at the rest of every joint.
120    pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Option<Posing> {
121        Some(self.posed?.posing(now, clips))
122    }
123
124    /// The material a slot the draw covers resolves to: the draw's write
125    /// to the part at `part`, else its write to every slot, else the
126    /// slot's own `default`, faded by the draw's own alpha.
127    pub(crate) fn resolved(&self, part: Option<u32>, default: Material) -> Material {
128        part.and_then(|part| self.paints.of(part))
129            .or(self.paints.every)
130            .unwrap_or(default)
131            .faded(self.fade)
132    }
133}
134
135/// A draw a game builds and submits: a mesh's position, its turn, the
136/// part of its textures it samples, the style it is drawn with, the fade
137/// over its slots, and any materials that override its slot defaults.
138///
139/// `S` is the game's style set, which [`surface_style`](Instance::surface_style) proves a
140/// style against. It is [`NoSurfaceStyles`](crate::NoSurfaceStyles) by
141/// default, the style set of a game with no style of its own; a game with
142/// styles of its own writes `Instance<M, Looks>` wherever it names the
143/// type — `Looks` is the set `examples/sprite-adventure.rs` declares.
144#[must_use = "an instance is only drawn once FrameContext::draw takes it"]
145#[derive(Debug)]
146pub struct Instance<M, S: SurfaceStyles = NoSurfaceStyles> {
147    draw: Draw<M>,
148    styles: PhantomData<S>,
149}
150
151impl<M, S: SurfaceStyles> Instance<M, S> {
152    /// A draw of `mesh` placed by `transform`, with nothing else set.
153    pub(crate) fn new(mesh: M, transform: Transform) -> Self {
154        Self {
155            draw: Draw {
156                mesh,
157                transform,
158                facing: Facing::AsPlaced,
159                roll: 0.0,
160                frame: Frame::default(),
161                style: None,
162                fade: OPAQUE,
163                posed: None,
164                paints: Paints::default(),
165            },
166            styles: PhantomData,
167        }
168    }
169
170    /// Moves the instance to `transform`, in place of the one it has.
171    pub fn at(mut self, transform: impl Into<Transform>) -> Self {
172        self.draw.transform = transform.into();
173        self
174    }
175
176    /// Turns the draw to face the frame's camera, in place of the turn its
177    /// transform holds.
178    ///
179    /// The transform's sizes are still the draw's size, and its position
180    /// still places it. The last of the two facing calls is the one used.
181    pub fn billboard(mut self) -> Self {
182        self.draw.facing = Facing::Billboard;
183        self
184    }
185
186    /// Turns the draw about `+Y` alone to face the frame's camera — a
187    /// sprite upright on the ground, however far the camera looks down at
188    /// it.
189    ///
190    /// Keeps the transform's sizes and position, like
191    /// [`billboard`](Instance::billboard).
192    pub fn upright(mut self) -> Self {
193        self.draw.facing = Facing::Upright;
194        self
195    }
196
197    /// Turns the draw `radians` within the view plane, counter-clockwise
198    /// from the camera's viewpoint.
199    ///
200    /// Required if you want a billboarded draw turned around: a draw turned
201    /// by [`upright`](Instance::upright) or by its own transform ignores it,
202    /// with no such turn left free.
203    pub fn roll(mut self, radians: f32) -> Self {
204        self.draw.roll = radians;
205        self
206    }
207
208    /// Draws with the WGSL of `T`, in the pass that style declares,
209    /// instead of with the built-in look.
210    ///
211    /// Takes a style of [`Game::SurfaceStyles`](crate::Game::SurfaceStyles)
212    /// and no other. The last call is the one used. To read the seat of
213    /// `T` the call turns `T::default()` into the set and drops that
214    /// value's fields; the values the WGSL reads come from
215    /// [`set_surface_style`](crate::FrameContext::set_surface_style).
216    pub fn surface_style<T: SurfaceStyle>(mut self) -> Self
217    where
218        S: Holds<T>,
219    {
220        let seat = SurfaceStyleId(S::from(T::default()).seat());
221        self.draw.style = Some(Styled::at::<T>(seat));
222        self
223    }
224
225    /// Draws the mesh in the pose `animator` holds, in place of the rest of
226    /// every joint.
227    ///
228    /// Takes a machine typed by this mesh and no other, and reads it at the
229    /// instant the frame draws. A mesh with no joints is drawn as it is,
230    /// and the last call is the one used.
231    pub fn posed<P: Part, A: AnimationStates>(mut self, animator: &Animator<M, A>) -> Self
232    where
233        M: Mesh<P, A::Clip>,
234    {
235        self.draw.posed = Some(animator.running());
236        self
237    }
238
239    /// Draws the mesh in the pose `posing` holds, at the fixed times it
240    /// states.
241    ///
242    /// The engine's own tests of what a pose draws are the only caller: a
243    /// game poses a draw through [`posed`](Self::posed), which takes no
244    /// time of its own.
245    #[cfg(all(test, feature = "offscreen"))]
246    pub(crate) fn posed_by(mut self, posing: Posing) -> Self {
247        self.draw.posed = Some(Running::stopped(posing));
248        self
249    }
250
251    /// Samples `frame` of every texture the mesh draws with; the whole of
252    /// each by default.
253    pub fn frame(mut self, frame: Frame) -> Self {
254        self.draw.frame = frame;
255        self
256    }
257
258    /// Draws every slot of the mesh with `material` instead of its default,
259    /// the ones no part names too.
260    ///
261    /// The last write to a slot is the one used, so a
262    /// [`material_of`](Instance::material_of) after this call writes one
263    /// part again, and one before it is replaced. A material holds no maps;
264    /// the maps beside a slot's color are the mesh's own, since each binds GPU
265    /// state a draw does not change (see [`Slot`](crate::mesh::Slot)).
266    pub fn material(mut self, material: Material) -> Self {
267        self.draw.paints.every(material);
268        self
269    }
270
271    /// Draws the slot `part` selects with `material` instead of the mesh's
272    /// default for it.
273    ///
274    /// Takes a part of the mesh's own vocabulary and no other. The last
275    /// write to a slot is the one used.
276    pub fn material_of<P: Part, C: Clip>(mut self, part: P, material: Material) -> Self
277    where
278        M: Mesh<P, C>,
279    {
280        self.draw.paints.one(part.index(), material);
281        self
282    }
283
284    /// Scales the tint alpha of every slot the draw covers by `alpha`,
285    /// clamped to `0.0..=1.0`, leaving the rest of each material as it is.
286    ///
287    /// Required if you want to fade a mesh and repaint none of it: the fade
288    /// applies once [`material`](Instance::material) overrides have resolved,
289    /// so every slot fades the same. Under `1.0` the draw blends in the
290    /// transparent pass and blocks that same fraction of every light it is
291    /// within, as a tint alpha under `1.0` does on its own: a fading draw's
292    /// shadow fades with the draw instead of dropping away, and one faded to
293    /// `0.0` casts nothing. A styled draw keeps its own pass and casts as that
294    /// pass does. The fade scales what an additive draw adds, its
295    /// [`emissive`](Material::emissive) light too, and the alpha a cutout draw
296    /// drops texels from, so one faded past `0.5` keeps none of them. The last
297    /// call is the one used.
298    pub fn faded(mut self, alpha: f32) -> Self {
299        self.draw.fade = alpha.clamp(0.0, OPAQUE);
300        self
301    }
302
303    /// The same draw as a draw of the game's set `T`, which
304    /// [`FrameContext::draw`](crate::FrameContext::draw) takes as it takes any
305    /// mesh the set holds.
306    ///
307    /// Required if you want one variable to hold a draw of either of two
308    /// mesh types.
309    pub fn into_set<T: From<M>>(self) -> Instance<T, S> {
310        Instance {
311            draw: self.draw.into_set(),
312            styles: PhantomData,
313        }
314    }
315
316    /// The draw as the engine records it: the style set proved the style
317    /// at [`surface_style`](Instance::surface_style), so the seat it took is all the engine
318    /// reads past this call.
319    pub(crate) fn record(self) -> Draw<M> {
320        self.draw
321    }
322}
323
324impl<M: Clone, S: SurfaceStyles> Clone for Instance<M, S> {
325    fn clone(&self) -> Self {
326        Self {
327            draw: self.draw.clone(),
328            styles: PhantomData,
329        }
330    }
331}
332
333/// A draw's turn: as its transform sets, or towards the frame's camera.
334#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335enum Facing {
336    AsPlaced,
337    Billboard,
338    Upright,
339}
340
341impl Facing {
342    /// The turn `transform` takes for a frame viewed from `view`: whatever
343    /// turn this facing chooses, `roll` within it where the facing leaves
344    /// that free, over the sizes and position the transform holds.
345    fn applied(self, transform: Transform, view: View, roll: f32) -> Transform {
346        let Some(turn) = self.turn(view, roll) else {
347            return transform;
348        };
349
350        let model = transform.matrix();
351        let sized = |axis: Vec3, column: Vec4| (axis * column.truncate().length()).extend(0.0);
352        Transform::from(Mat4::from_cols(
353            sized(turn.x_axis, model.x_axis),
354            sized(turn.y_axis, model.y_axis),
355            sized(turn.z_axis, model.z_axis),
356            model.w_axis,
357        ))
358    }
359
360    /// The turn this facing applies to a draw, or nothing where the draw
361    /// keeps its own.
362    fn turn(self, view: View, roll: f32) -> Option<Mat3> {
363        match self {
364            Self::AsPlaced => None,
365            // The view plane's own `+Z` faces the camera, so a turn about it
366            // is counter-clockwise from the camera's viewpoint.
367            Self::Billboard => {
368                Some(view_plane(looking(view)?, view.up()) * Mat3::from_rotation_z(roll))
369            }
370            Self::Upright => Some(standing(looking(view)?)),
371        }
372    }
373}
374
375/// A draw as one view places it: the turn that view applied to it, and
376/// whether a facing rather than the draw's own transform chose it.
377///
378/// [`Draw::placement`] is the only call that returns one, so a draw is
379/// recorded against a view and never against a bare transform.
380#[derive(Clone, Copy, Debug, PartialEq)]
381pub(crate) struct Placement {
382    transform: Transform,
383    faced: bool,
384}
385
386impl Placement {
387    /// The matrix the draw is placed by.
388    pub(crate) fn transform(self) -> Transform {
389        self.transform
390    }
391
392    /// Whether the view turned the draw rather than its own transform.
393    pub(crate) fn faced(self) -> bool {
394        self.faced
395    }
396}
397
398/// The direction the camera looks, or nothing where it looks at where it
399/// already is.
400fn looking(view: View) -> Option<Vec3> {
401    (view.target() - view.eye()).try_normalize()
402}
403
404/// A turn whose `+Z` faces the camera and whose `+Y` is the camera's own up.
405fn view_plane(looking: Vec3, up: Vec3) -> Mat3 {
406    let across = looking
407        .cross(up)
408        .try_normalize()
409        .unwrap_or_else(|| looking.cross(aside(looking)).normalize());
410
411    Mat3::from_cols(across, across.cross(looking), -looking)
412}
413
414/// A turn about `+Y` alone, as far towards the camera as that leaves it.
415fn standing(looking: Vec3) -> Mat3 {
416    let back = Vec3::new(-looking.x, 0.0, -looking.z)
417        .try_normalize()
418        .unwrap_or(Vec3::Z);
419
420    Mat3::from_cols(Vec3::Y.cross(back), Vec3::Y, back)
421}
422
423/// An up `looking` is not parallel to, so that a view plane is total.
424fn aside(looking: Vec3) -> Vec3 {
425    if looking.y.abs() > 0.99 {
426        Vec3::Z
427    } else {
428        Vec3::Y
429    }
430}
431
432/// A draw's material overrides: one for every slot, and one per part at
433/// the part's index, the later write to a slot winning.
434#[derive(Clone, Debug, Default)]
435struct Paints {
436    every: Option<Material>,
437    parts: Vec<Option<Material>>,
438}
439
440impl Paints {
441    /// Writes every slot, which replaces every write before this one.
442    fn every(&mut self, material: Material) {
443        self.every = Some(material);
444        self.parts.clear();
445    }
446
447    fn one(&mut self, part: u32, material: Material) {
448        let at = part as usize;
449        if at >= self.parts.len() {
450            self.parts.resize(at + 1, None);
451        }
452        self.parts[at] = Some(material);
453    }
454
455    /// The material written to the part at `part` alone, absent where none
456    /// was.
457    fn of(&self, part: u32) -> Option<Material> {
458        self.parts.get(part as usize).copied().flatten()
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use crate::math::Quat;
466    use crate::mesh::{Cube, MeshData, Slot};
467    use crate::{Assets, Catalog, Color};
468
469    /// A camera up and back from the origin, looking at it, so that a
470    /// billboarded draw and an upright one differ.
471    const DIVING: View = View::look_at(Vec3::new(0.0, 5.0, 5.0), Vec3::ZERO);
472
473    /// The roll a draw is turned by until it sets one.
474    const STILL: f32 = 0.0;
475
476    /// A quarter of a turn of it, which takes one axis of a billboarded
477    /// draw onto the next.
478    const QUARTER: f32 = core::f32::consts::FRAC_PI_2;
479
480    const GOLD: Material = Material::lit(Color::rgb(1.0, 0.8, 0.2));
481    const RED: Material = Material::lit(Color::rgb(1.0, 0.0, 0.0));
482
483    /// A mesh of two parts, named by hand.
484    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
485    struct Lantern;
486
487    impl Catalog for Lantern {
488        fn catalog() -> Vec<Self> {
489            vec![Self]
490        }
491    }
492
493    impl Mesh<LanternPart> for Lantern {
494        fn build(&self, assets: &Assets) -> MeshData<LanternPart> {
495            let cube = Cube.build(assets);
496            let half = cube.indices().len() as u32 / 2;
497            MeshData::in_parts(cube.vertices().to_vec(), cube.indices().to_vec(), |_| {
498                Slot::new(half, Material::default())
499            })
500        }
501    }
502
503    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
504    enum LanternPart {
505        Frame,
506        Glass,
507    }
508
509    impl Part for LanternPart {
510        fn from_name(_name: &str) -> Option<Self> {
511            None
512        }
513
514        fn all() -> Vec<Self> {
515            vec![Self::Frame, Self::Glass]
516        }
517
518        fn index(&self) -> u32 {
519            *self as u32
520        }
521    }
522
523    #[test]
524    fn the_last_write_to_a_part_is_the_one_a_slot_resolves_to() {
525        let refined = Lantern
526            .at::<NoSurfaceStyles>(Vec3::ZERO)
527            .material(GOLD)
528            .material_of(LanternPart::Glass, RED)
529            .record();
530        let replaced = Lantern
531            .at::<NoSurfaceStyles>(Vec3::ZERO)
532            .material_of(LanternPart::Glass, RED)
533            .material(GOLD)
534            .record();
535        let glass = Some(LanternPart::Glass.index());
536        let frame = Some(LanternPart::Frame.index());
537
538        assert_eq!(refined.resolved(glass, Material::default()), RED);
539        assert_eq!(refined.resolved(frame, Material::default()), GOLD);
540        assert_eq!(replaced.resolved(glass, Material::default()), GOLD);
541        assert_eq!(replaced.resolved(frame, Material::default()), GOLD);
542    }
543
544    #[test]
545    fn an_anonymous_slot_takes_the_write_to_every_slot_and_no_write_to_a_part() {
546        let draw = Lantern
547            .at::<NoSurfaceStyles>(Vec3::ZERO)
548            .material(GOLD)
549            .material_of(LanternPart::Glass, RED)
550            .record();
551
552        assert_eq!(draw.resolved(None, Material::default()), GOLD);
553        assert_eq!(
554            Cube.at::<NoSurfaceStyles>(Vec3::ZERO)
555                .record()
556                .resolved(None, RED),
557            RED,
558            "and a slot no draw wrote to keeps its default"
559        );
560    }
561
562    /// A draw turned every which way and a different size along each axis,
563    /// so facing has a turn of its own to drop and sizes to keep.
564    fn turned() -> Transform {
565        Transform::from_scale_rotation_translation(
566            Vec3::new(1.0, 2.0, 3.0),
567            Quat::from_rotation_x(0.7) * Quat::from_rotation_y(1.1),
568            Vec3::new(4.0, 5.0, 6.0),
569        )
570    }
571
572    /// The turned, scaled axes of a transform.
573    fn columns(transform: Transform) -> [Vec3; 3] {
574        let model = transform.matrix();
575        [model.x_axis, model.y_axis, model.z_axis].map(Vec4::truncate)
576    }
577
578    #[test]
579    fn a_billboarded_draw_stands_across_the_direction_the_camera_looks() {
580        for eye in [Vec3::new(0.0, 0.0, 3.0), Vec3::new(3.0, 4.0, -5.0)] {
581            let view = View::look_at(eye, Vec3::ZERO);
582            let ahead = (view.target() - view.eye()).normalize();
583            let [across, up, out] = columns(Facing::Billboard.applied(turned(), view, STILL));
584
585            assert!(across.dot(ahead).abs() < 1e-5, "{across} leans out of view");
586            assert!(up.dot(ahead).abs() < 1e-5, "{up} leans out of view");
587            assert!(
588                out.normalize().abs_diff_eq(-ahead, 1e-5),
589                "{out} faces away"
590            );
591        }
592    }
593
594    #[test]
595    fn an_upright_draw_keeps_the_way_up_and_turns_about_it_alone() {
596        let view = View::look_at(Vec3::new(3.0, 9.0, 3.0), Vec3::ZERO);
597        let [across, up, out] = columns(Facing::Upright.applied(turned(), view, STILL));
598
599        assert!(up.abs_diff_eq(Vec3::Y * 2.0, 1e-5), "{up} left the way up");
600        assert!(
601            across.y.abs() < 1e-5 && out.y.abs() < 1e-5,
602            "and stood level"
603        );
604        assert!(
605            out.normalize()
606                .abs_diff_eq(Vec3::new(3.0, 0.0, 3.0).normalize(), 1e-5),
607            "{out} does not face the camera"
608        );
609    }
610
611    #[test]
612    fn facing_keeps_the_sizes_and_the_position_the_transform_gave_a_draw() {
613        for (facing, roll) in [
614            (Facing::Billboard, STILL),
615            (Facing::Billboard, QUARTER),
616            (Facing::Upright, STILL),
617        ] {
618            let faced = facing.applied(turned(), DIVING, roll);
619            let sizes = columns(faced).map(|column| column.length());
620
621            assert!(
622                sizes
623                    .iter()
624                    .zip(columns(turned()))
625                    .all(|(kept, column)| (kept - column.length()).abs() < 1e-5),
626                "{sizes:?} are not the sizes the transform carried"
627            );
628            assert_eq!(faced.matrix().w_axis, turned().matrix().w_axis);
629            assert_ne!(columns(faced), columns(turned()), "and the turn is gone");
630        }
631    }
632
633    #[test]
634    fn a_camera_straight_overhead_leaves_an_upright_draw_standing() {
635        let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO).with_up(Vec3::NEG_Z);
636        let [across, up, out] = columns(Facing::Upright.applied(Transform::IDENTITY, view, STILL));
637
638        assert_eq!(up, Vec3::Y);
639        assert!(across.is_finite() && out.is_finite(), "{across} {out}");
640        assert!(out.y.abs() < 1e-5, "so it is seen edge-on from up there");
641    }
642
643    #[test]
644    fn a_billboard_stands_even_where_the_camera_looks_along_its_own_way_up() {
645        let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO);
646        let [across, up, out] =
647            columns(Facing::Billboard.applied(Transform::IDENTITY, view, STILL));
648
649        assert!(across.is_finite() && up.is_finite(), "{across} {up}");
650        assert!(out.abs_diff_eq(Vec3::Y, 1e-5), "{out} does not face back");
651    }
652
653    #[test]
654    fn a_quarter_of_a_roll_takes_a_billboards_across_onto_the_way_up() {
655        let view = View::look_at(Vec3::Z * 4.0, Vec3::ZERO);
656        let [across, up, out] =
657            columns(Facing::Billboard.applied(Transform::IDENTITY, view, QUARTER));
658
659        assert!(
660            across.abs_diff_eq(Vec3::Y, 1e-5),
661            "{across} is not the way the camera is up"
662        );
663        assert!(up.abs_diff_eq(Vec3::NEG_X, 1e-5), "{up} followed it around");
664        assert!(out.abs_diff_eq(Vec3::Z, 1e-5), "{out} left the view plane");
665    }
666
667    #[test]
668    fn a_rolled_billboard_stands_in_the_view_plane_however_far_it_is_turned() {
669        let view = View::look_at(Vec3::new(3.0, 4.0, -5.0), Vec3::ZERO);
670        let ahead = (view.target() - view.eye()).normalize();
671
672        for roll in [0.3, 2.0, -1.7, 100.0] {
673            let [across, up, out] =
674                columns(Facing::Billboard.applied(turned(), view, roll)).map(Vec3::normalize);
675
676            assert!(across.dot(up).abs() < 1e-5, "{across} leans onto {up}");
677            assert!(
678                across.dot(ahead).abs() < 1e-5 && up.dot(ahead).abs() < 1e-5,
679                "{across} or {up} leans out of view"
680            );
681            assert!(out.abs_diff_eq(-ahead, 1e-5), "{out} faces away");
682        }
683    }
684
685    #[test]
686    fn only_a_billboarded_draw_is_turned_by_the_roll_it_asks_for() {
687        for facing in [Facing::AsPlaced, Facing::Upright] {
688            assert_eq!(
689                facing.applied(turned(), DIVING, QUARTER),
690                facing.applied(turned(), DIVING, STILL),
691                "a turn of its own is a turn roll has no say in"
692            );
693        }
694        assert_ne!(
695            Facing::Billboard.applied(turned(), DIVING, QUARTER),
696            Facing::Billboard.applied(turned(), DIVING, STILL),
697            "where a billboarded draw leaves it free"
698        );
699    }
700
701    /// The placement a draw takes as `DIVING` places it.
702    fn placed(instance: Instance<Cube>) -> Placement {
703        instance.record().placement(DIVING)
704    }
705
706    #[test]
707    fn a_draw_is_rolled_whichever_way_round_it_asked_to_be_billboarded() {
708        let cube = Cube.at::<NoSurfaceStyles>(turned());
709
710        assert_eq!(
711            placed(cube.clone().roll(QUARTER).billboard()),
712            placed(cube.clone().billboard().roll(QUARTER))
713        );
714        assert_eq!(
715            placed(cube.clone()),
716            placed(cube.roll(QUARTER)),
717            "and a draw the camera never turned is left where it was"
718        );
719    }
720
721    #[test]
722    fn a_camera_that_looks_nowhere_leaves_a_faced_draw_where_it_was() {
723        let view = View::look_at(Vec3::Y, Vec3::Y);
724
725        for facing in [Facing::Billboard, Facing::Upright] {
726            assert_eq!(facing.applied(turned(), view, STILL), turned());
727        }
728    }
729
730    #[test]
731    fn the_last_facing_a_draw_asks_for_is_the_one_it_is_turned_by() {
732        let cube = Cube.at::<NoSurfaceStyles>(turned());
733
734        assert_eq!(
735            placed(cube.clone().billboard().upright()),
736            placed(cube.clone().upright())
737        );
738        assert_eq!(
739            placed(cube.clone().upright().billboard()),
740            placed(cube.clone().billboard())
741        );
742        assert_ne!(
743            placed(cube.clone().upright()),
744            placed(cube.clone().billboard())
745        );
746        assert!(
747            !cube.record().faced(),
748            "and a draw asks for neither by default"
749        );
750    }
751}