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