Skip to main content

bevy_lagrange/
events.rs

1//! Events for camera animations and zoom operations.
2//!
3//! Events are organized by feature. Each group starts with the **trigger** event
4//! (fire with `commands.trigger(...)`) followed by the **fired** events it produces
5//! (observe with `.add_observer(...)`).
6//!
7//! # Common patterns
8//!
9//! **Duration** — several events accept a `duration` field. When set to
10//! `Duration::ZERO` the operation completes instantly — the camera snaps to its
11//! final position and only the **operation-level** begin/end events fire (see
12//! [instant paths](#instant-operations) below). When `duration > Duration::ZERO`
13//! the operation animates over time through [`PlayAnimation`], so the full nested
14//! event sequence fires.
15//!
16//! **Easing** — events that animate also accept an `easing` field
17//! ([`EaseFunction`]) that controls the interpolation curve. This only has an effect
18//! when `duration > Duration::ZERO`.
19//!
20//! # Event ordering
21//!
22//! Events nest from outermost (operation-level) to innermost (move-level). Every
23//! animated path goes through [`PlayAnimation`], so [`AnimationBegin`]/[`AnimationEnd`]
24//! and [`CameraMoveBegin`]/[`CameraMoveEnd`] fire for **all** animated operations —
25//! including [`ZoomToFit`] and [`AnimateToFit`].
26//!
27//! ## `PlayAnimation` — normal completion
28//!
29//! ```text
30//! AnimationBegin → CameraMoveBegin → CameraMoveEnd → … → AnimationEnd
31//! ```
32//!
33//! ## `ZoomToFit` (animated) — normal completion
34//!
35//! `Zoom*` events wrap the animation lifecycle:
36//!
37//! ```text
38//! ZoomBegin → AnimationBegin → CameraMoveBegin → CameraMoveEnd → AnimationEnd → ZoomEnd
39//! ```
40//!
41//! ## `AnimateToFit` (animated) — normal completion
42//!
43//! No extra wrapping events — uses `source: AnimationSource::AnimateToFit` to
44//! distinguish from a plain [`PlayAnimation`]:
45//!
46//! ```text
47//! AnimationBegin → CameraMoveBegin → CameraMoveEnd → AnimationEnd
48//! ```
49//!
50//! ## Instant operations
51//!
52//! When `duration` is `Duration::ZERO`, the animation system is bypassed entirely.
53//! Only the operation-level events fire — no [`AnimationBegin`]/[`AnimationEnd`] or
54//! [`CameraMoveBegin`]/[`CameraMoveEnd`].
55//!
56//! ### `ZoomToFit` (instant)
57//!
58//! ```text
59//! ZoomBegin → ZoomEnd
60//! ```
61//!
62//! ### `AnimateToFit` (instant)
63//!
64//! Fires animation-level events (to notify observers) but no camera-move-level events:
65//!
66//! ```text
67//! AnimationBegin → AnimationEnd
68//! ```
69//!
70//! ## User input interruption ([`CameraInputInterruptBehavior`](crate::CameraInputInterruptBehavior))
71//!
72//! When the user physically moves the camera during an animation:
73//!
74//! - **`Ignore`** (default) — temporarily disables camera input and continues animating:
75//!
76//!   ```text
77//!   … (no interrupt lifecycle event)
78//!   ```
79//!
80//! - **`Cancel`** — stops where it is:
81//!
82//!   ```text
83//!   … → AnimationCancelled → ZoomCancelled (if zoom)
84//!   ```
85//!
86//! - **`Complete`** — jumps to the final position:
87//!
88//!   ```text
89//!   … → AnimationEnd → ZoomEnd (if zoom)
90//!   ```
91//!
92//! ## Animation conflict ([`AnimationConflictPolicy`](crate::AnimationConflictPolicy))
93//!
94//! When a new animation request arrives while one is already in-flight:
95//!
96//! - **`LastWins`** (default) — cancels the in-flight animation, then starts the new one.
97//!   `AnimationCancelled` always fires; `ZoomCancelled` additionally fires if the in-flight
98//!   operation is a zoom:
99//!
100//!   ```text
101//!   AnimationCancelled → ZoomCancelled (if zoom) → AnimationBegin (new) → …
102//!   ```
103//!
104//! - **`FirstWins`** — rejects the incoming request. No zoom lifecycle events fire — the rejection
105//!   is detected before `ZoomBegin`:
106//!
107//!   ```text
108//!   AnimationRejected
109//!   ```
110//!
111//!   The [`AnimationRejected::source`] field identifies what was rejected
112//!   ([`AnimationSource::PlayAnimation`], [`AnimationSource::ZoomToFit`], or
113//!   [`AnimationSource::AnimateToFit`]).
114//!
115//! # Emitted event data
116//!
117//! Reference of data carried by events — for comparison purposes.
118//!
119//! | Event                    | `camera` | `target` | `margin` | `duration` | `easing` | `source` | `camera_move` |
120//! |--------------------------|-----------------|-----------------|----------|------------|----------|----------|---------------|
121//! | [`ZoomBegin`]            | yes             | yes             | yes      | yes        | yes      | —        | —             |
122//! | [`ZoomEnd`]              | yes             | yes             | yes      | yes        | yes      | —        | —             |
123//! | [`ZoomCancelled`]        | yes             | yes             | yes      | yes        | yes      | —        | —             |
124//! | [`AnimationBegin`]       | yes             | —               | —        | —          | —        | yes      | —             |
125//! | [`AnimationEnd`]         | yes             | —               | —        | —          | —        | yes      | —             |
126//! | [`AnimationCancelled`]   | yes             | —               | —        | —          | —        | yes      | yes           |
127//! | [`AnimationRejected`]    | yes             | —               | —        | —          | —        | yes      | —             |
128//! | [`CameraMoveBegin`]      | yes             | —               | —        | —          | —        | —        | yes           |
129//! | [`CameraMoveEnd`]        | yes             | —               | —        | —          | —        | —        | yes           |
130
131use std::collections::VecDeque;
132use std::time::Duration;
133
134use bevy::math::curve::easing::EaseFunction;
135use bevy::prelude::*;
136
137use super::animation::CameraMove;
138
139/// Context for a zoom-to-fit operation.
140///
141/// Passed through [`PlayAnimation`] so that `on_play_animation` can fire
142/// [`ZoomBegin`] and insert
143/// [`ZoomAnimationMarker`](super::components::ZoomAnimationMarker) at the
144/// single point where conflict resolution has already completed.
145#[derive(Clone, Reflect)]
146pub struct ZoomContext {
147    /// The entity being framed.
148    pub target:   Entity,
149    /// The margin from the triggering [`ZoomToFit`].
150    pub margin:   f32,
151    /// The duration from the triggering [`ZoomToFit`].
152    pub duration: Duration,
153    /// The easing curve from the triggering [`ZoomToFit`].
154    pub easing:   EaseFunction,
155}
156
157/// Identifies which event triggered an animation lifecycle.
158///
159/// Carried by [`AnimationBegin`], [`AnimationEnd`], [`AnimationCancelled`], and
160/// [`AnimationRejected`] so observers know whether the animation originated from
161/// [`PlayAnimation`], [`ZoomToFit`], or [`AnimateToFit`].
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
163pub enum AnimationSource {
164    /// Animation was triggered by [`PlayAnimation`].
165    PlayAnimation,
166    /// Animation was triggered by [`ZoomToFit`].
167    ZoomToFit,
168    /// Animation was triggered by [`AnimateToFit`].
169    AnimateToFit,
170    /// Animation was triggered by [`LookAt`].
171    LookAt,
172    /// Animation was triggered by [`LookAtAndZoomToFit`].
173    LookAtAndZoomToFit,
174}
175
176/// `ZoomToFit` — frames a target entity in the camera view.
177///
178/// The camera's viewing angle stays the same.
179/// The camera's yaw and pitch stay fixed. Only the focus and radius change so
180/// that the target fills the viewport with the requested margin. Because the
181/// viewing angle is preserved, the camera *translates* to a new position rather
182/// than rotating — if the target is off to the side, the view slides over to it.
183///
184/// # See also
185///
186/// - [`LookAt`] — keeps the camera in place and *rotates* to face the target (no framing / radius
187///   adjustment).
188/// - [`LookAtAndZoomToFit`] — *rotates* to face the target and adjusts radius to frame it. Use this
189///   when you want the camera to turn toward the target instead of sliding.
190/// - [`AnimateToFit`] — frames the target from a caller-specified viewing angle.
191///
192/// # Fields
193///
194/// - `camera` — the entity with a `OrbitCam` component.
195/// - `target` — the entity to frame; must have a `Mesh3d` (direct or on descendants).
196/// - `margin` — total fraction of the screen to leave as space between the target's screen-space
197///   bounding box and the screen edge, split equally across both sides of the constraining
198///   dimension (e.g. `0.25` → ~12.5% each side).
199/// - `duration` — see module-level docs on **Duration**.
200/// - `easing` — see module-level docs on **Easing**.
201///
202/// Animated zooms route through [`PlayAnimation`], so the full event sequence is
203/// `ZoomBegin` → `AnimationBegin` → `CameraMoveBegin` → `CameraMoveEnd` →
204/// `AnimationEnd` → `ZoomEnd`. See the [module-level event ordering](self#event-ordering)
205/// docs for interruption and conflict scenarios.
206#[derive(EntityEvent, Reflect)]
207#[reflect(Event, FromReflect)]
208pub struct ZoomToFit {
209    /// The camera entity to zoom.
210    #[event_target]
211    pub camera:   Entity,
212    /// The entity to frame.
213    pub target:   Entity,
214    /// Fraction of screen to leave as margin.
215    pub margin:   f32,
216    /// Animation duration (`ZERO` for instant).
217    pub duration: Duration,
218    /// Easing curve for the animation.
219    pub easing:   EaseFunction,
220}
221
222impl ZoomToFit {
223    /// Creates a new `ZoomToFit` event with default margin, instant duration, and cubic-out easing.
224    #[must_use]
225    pub const fn new(camera: Entity, target: Entity) -> Self {
226        Self {
227            camera,
228            target,
229            margin: 0.1,
230            duration: Duration::ZERO,
231            easing: EaseFunction::CubicOut,
232        }
233    }
234
235    /// Sets the margin.
236    #[must_use]
237    pub const fn margin(mut self, margin: f32) -> Self {
238        self.margin = margin;
239        self
240    }
241
242    /// Sets the animation duration.
243    #[must_use]
244    pub const fn duration(mut self, duration: Duration) -> Self {
245        self.duration = duration;
246        self
247    }
248
249    /// Sets the easing function.
250    #[must_use]
251    pub const fn easing(mut self, easing: EaseFunction) -> Self {
252        self.easing = easing;
253        self
254    }
255}
256
257/// `ZoomBegin` — emitted when a [`ZoomToFit`] operation begins.
258#[derive(EntityEvent, Reflect)]
259#[reflect(Event, FromReflect)]
260pub struct ZoomBegin {
261    /// The camera that is zooming.
262    #[event_target]
263    pub camera:   Entity,
264    /// The entity being framed.
265    pub target:   Entity,
266    /// The margin from the triggering [`ZoomToFit`].
267    pub margin:   f32,
268    /// The duration from the triggering [`ZoomToFit`].
269    pub duration: Duration,
270    /// The easing curve from the triggering [`ZoomToFit`].
271    pub easing:   EaseFunction,
272}
273
274/// `ZoomEnd` — emitted when a [`ZoomToFit`] operation completes (both animated and instant).
275#[derive(EntityEvent, Reflect)]
276#[reflect(Event, FromReflect)]
277pub struct ZoomEnd {
278    /// The camera that finished zooming.
279    #[event_target]
280    pub camera:   Entity,
281    /// The entity that was framed.
282    pub target:   Entity,
283    /// The margin from the triggering [`ZoomToFit`].
284    pub margin:   f32,
285    /// The duration from the triggering [`ZoomToFit`].
286    pub duration: Duration,
287    /// The easing curve from the triggering [`ZoomToFit`].
288    pub easing:   EaseFunction,
289}
290
291/// `ZoomCancelled` — emitted when a [`ZoomToFit`] animation is cancelled before completion.
292///
293/// The camera stays at its current position — no snap to final.
294///
295/// Cancellation happens in two scenarios:
296/// - **User input** — the user physically moves the camera while
297///   [`CameraInputInterruptBehavior::Cancel`](crate::CameraInputInterruptBehavior::Cancel) is
298///   active.
299/// - **Animation conflict** — a new animation request arrives while
300///   [`AnimationConflictPolicy::LastWins`](crate::AnimationConflictPolicy::LastWins) is active,
301///   cancelling the in-flight zoom.
302#[derive(EntityEvent, Reflect)]
303#[reflect(Event, FromReflect)]
304pub struct ZoomCancelled {
305    /// The camera whose zoom was cancelled.
306    #[event_target]
307    pub camera:   Entity,
308    /// The entity that was being framed.
309    pub target:   Entity,
310    /// The margin from the triggering [`ZoomToFit`].
311    pub margin:   f32,
312    /// The duration from the triggering [`ZoomToFit`].
313    pub duration: Duration,
314    /// The easing curve from the triggering [`ZoomToFit`].
315    pub easing:   EaseFunction,
316}
317
318/// `PlayAnimation` — plays a queued sequence of [`CameraMove`] steps.
319///
320/// Fires `AnimationBegin` → (`CameraMoveBegin` → `CameraMoveEnd`) × N → `AnimationEnd`.
321/// See the [module-level event ordering](self#event-ordering) docs for interruption and
322/// conflict scenarios.
323#[derive(EntityEvent, Reflect)]
324#[reflect(Event, FromReflect)]
325pub struct PlayAnimation {
326    /// The camera entity to animate.
327    #[event_target]
328    pub camera:       Entity,
329    /// The queue of camera movements.
330    pub camera_moves: VecDeque<CameraMove>,
331    /// The source of this animation.
332    pub source:       AnimationSource,
333    /// Optional zoom context when this animation originates from [`ZoomToFit`].
334    pub zoom_context: Option<ZoomContext>,
335}
336
337impl PlayAnimation {
338    /// Creates a new `PlayAnimation` event.
339    #[must_use]
340    pub fn new(camera: Entity, camera_moves: impl IntoIterator<Item = CameraMove>) -> Self {
341        Self {
342            camera,
343            camera_moves: camera_moves.into_iter().collect(),
344            source: AnimationSource::PlayAnimation,
345            zoom_context: None,
346        }
347    }
348
349    /// Sets the animation source.
350    #[must_use]
351    pub const fn source(mut self, source: AnimationSource) -> Self {
352        self.source = source;
353        self
354    }
355
356    /// Sets the zoom context (implies `AnimationSource::ZoomToFit`).
357    #[must_use]
358    pub const fn zoom_context(mut self, ctx: ZoomContext) -> Self {
359        self.zoom_context = Some(ctx);
360        self.source = AnimationSource::ZoomToFit;
361        self
362    }
363}
364
365/// `AnimationBegin` — emitted when a `CameraMoveList` begins processing.
366#[derive(EntityEvent, Reflect)]
367#[reflect(Event, FromReflect)]
368pub struct AnimationBegin {
369    /// The camera being animated.
370    #[event_target]
371    pub camera: Entity,
372    /// Whether this animation originated from [`PlayAnimation`], [`ZoomToFit`], or
373    /// [`AnimateToFit`].
374    pub source: AnimationSource,
375}
376
377/// `AnimationEnd` — emitted when a `CameraMoveList` finishes all its queued moves.
378#[derive(EntityEvent, Reflect)]
379#[reflect(Event, FromReflect)]
380pub struct AnimationEnd {
381    /// The camera that finished animating.
382    #[event_target]
383    pub camera: Entity,
384    /// Whether this animation originated from [`PlayAnimation`], [`ZoomToFit`], or
385    /// [`AnimateToFit`].
386    pub source: AnimationSource,
387}
388
389/// `AnimationCancelled` — emitted when an animation is cancelled before completion.
390///
391/// Applies to [`PlayAnimation`], [`ZoomToFit`], or [`AnimateToFit`].
392/// The camera stays at its current position — no snap to final.
393#[derive(EntityEvent, Reflect)]
394#[reflect(Event, FromReflect)]
395pub struct AnimationCancelled {
396    /// The camera whose animation was cancelled.
397    #[event_target]
398    pub camera:      Entity,
399    /// Whether this animation originated from [`PlayAnimation`], [`ZoomToFit`], or
400    /// [`AnimateToFit`].
401    pub source:      AnimationSource,
402    /// The [`CameraMove`] that was in progress when cancelled.
403    pub camera_move: CameraMove,
404}
405
406/// `AnimationRejected` — emitted when an incoming animation request is rejected.
407///
408/// This occurs because
409/// [`AnimationConflictPolicy::FirstWins`](crate::AnimationConflictPolicy::FirstWins) is
410/// active and an animation is already in-flight.
411#[derive(EntityEvent, Reflect)]
412#[reflect(Event, FromReflect)]
413pub struct AnimationRejected {
414    /// The camera that rejected the animation.
415    #[event_target]
416    pub camera: Entity,
417    /// The [`AnimationSource`] of the rejected request.
418    pub source: AnimationSource,
419}
420
421/// `CameraMoveBegin` — emitted when an individual [`CameraMove`] begins.
422#[derive(EntityEvent, Reflect)]
423#[reflect(Event, FromReflect)]
424pub struct CameraMoveBegin {
425    /// The camera being animated.
426    #[event_target]
427    pub camera:      Entity,
428    /// The [`CameraMove`] step that is starting.
429    pub camera_move: CameraMove,
430}
431
432/// `CameraMoveEnd` — emitted when an individual [`CameraMove`] completes.
433#[derive(EntityEvent, Reflect)]
434#[reflect(Event, FromReflect)]
435pub struct CameraMoveEnd {
436    /// The camera that finished this move step.
437    #[event_target]
438    pub camera:      Entity,
439    /// The [`CameraMove`] step that completed.
440    pub camera_move: CameraMove,
441}
442
443/// `AnimateToFit` — animates the camera to a caller-specified orientation.
444///
445/// Frames a target entity in view.
446/// You specify the exact yaw and pitch the camera should end up at, and the
447/// system computes the radius needed to frame the target from that angle.
448///
449/// # See also
450///
451/// - [`LookAtAndZoomToFit`] — like `AnimateToFit` but the yaw/pitch are automatically back-solved
452///   from the camera's current position, so you don't specify them.
453/// - [`ZoomToFit`] — keeps the current viewing angle, only adjusts focus and radius.
454/// - [`LookAt`] — rotates to face the target without framing.
455#[derive(EntityEvent, Reflect)]
456#[reflect(Event, FromReflect)]
457pub struct AnimateToFit {
458    /// The camera entity.
459    #[event_target]
460    pub camera:   Entity,
461    /// The entity to frame.
462    pub target:   Entity,
463    /// Final yaw in radians.
464    pub yaw:      f32,
465    /// Final pitch in radians.
466    pub pitch:    f32,
467    /// Fraction of screen to leave as margin.
468    pub margin:   f32,
469    /// Animation duration (`ZERO` for instant).
470    pub duration: Duration,
471    /// Easing curve for the animation.
472    pub easing:   EaseFunction,
473}
474
475impl AnimateToFit {
476    /// Creates a new `AnimateToFit` with default parameters.
477    #[must_use]
478    pub const fn new(camera: Entity, target: Entity) -> Self {
479        Self {
480            camera,
481            target,
482            yaw: 0.0,
483            pitch: 0.0,
484            margin: 0.1,
485            duration: Duration::ZERO,
486            easing: EaseFunction::CubicOut,
487        }
488    }
489
490    /// Sets the target yaw.
491    #[must_use]
492    pub const fn yaw(mut self, yaw: f32) -> Self {
493        self.yaw = yaw;
494        self
495    }
496
497    /// Sets the target pitch.
498    #[must_use]
499    pub const fn pitch(mut self, pitch: f32) -> Self {
500        self.pitch = pitch;
501        self
502    }
503
504    /// Sets the margin.
505    #[must_use]
506    pub const fn margin(mut self, margin: f32) -> Self {
507        self.margin = margin;
508        self
509    }
510
511    /// Sets the animation duration.
512    #[must_use]
513    pub const fn duration(mut self, duration: Duration) -> Self {
514        self.duration = duration;
515        self
516    }
517
518    /// Sets the easing function.
519    #[must_use]
520    pub const fn easing(mut self, easing: EaseFunction) -> Self {
521        self.easing = easing;
522        self
523    }
524}
525
526/// `LookAt` — rotates the camera in place to face a target entity.
527///
528/// The camera stays at its current world position and turns to look at the target.
529/// The orbit pivot re-anchors to the target entity's [`GlobalTransform`] translation,
530/// and yaw/pitch/radius are back-solved so the camera does not move — only its
531/// orientation changes.
532///
533/// # See also
534///
535/// - [`LookAtAndZoomToFit`] — same rotation, but also adjusts radius to frame the target in view.
536/// - [`ZoomToFit`] — keeps the viewing angle, moves the camera to frame the target.
537/// - [`AnimateToFit`] — frames the target from a caller-specified viewing angle.
538#[derive(EntityEvent, Reflect)]
539#[reflect(Event, FromReflect)]
540pub struct LookAt {
541    /// The camera entity.
542    #[event_target]
543    pub camera:   Entity,
544    /// The entity to look at.
545    pub target:   Entity,
546    /// Animation duration (`ZERO` for instant).
547    pub duration: Duration,
548    /// Easing curve for the animation.
549    pub easing:   EaseFunction,
550}
551
552impl LookAt {
553    /// Creates a new `LookAt` with instant duration and cubic-out easing.
554    #[must_use]
555    pub const fn new(camera: Entity, target: Entity) -> Self {
556        Self {
557            camera,
558            target,
559            duration: Duration::ZERO,
560            easing: EaseFunction::CubicOut,
561        }
562    }
563
564    /// Sets the animation duration.
565    #[must_use]
566    pub const fn duration(mut self, duration: Duration) -> Self {
567        self.duration = duration;
568        self
569    }
570
571    /// Sets the easing function.
572    #[must_use]
573    pub const fn easing(mut self, easing: EaseFunction) -> Self {
574        self.easing = easing;
575        self
576    }
577}
578
579/// `LookAtAndZoomToFit` — rotates the camera to face a target entity and frames it.
580///
581/// Adjusts the radius to frame the target in view, all in one fluid motion.
582/// Combines [`LookAt`] (turn in place) with [`ZoomToFit`] (frame the target).
583/// The yaw and pitch are back-solved from the camera's current world position
584/// relative to the target's bounds center — you don't specify them.
585///
586/// # See also
587///
588/// - [`LookAt`] — same rotation without the zoom-to-fit radius adjustment.
589/// - [`ZoomToFit`] — keeps the viewing angle, moves the camera to frame the target.
590/// - [`AnimateToFit`] — frames the target from a caller-specified viewing angle.
591#[derive(EntityEvent, Reflect)]
592#[reflect(Event, FromReflect)]
593pub struct LookAtAndZoomToFit {
594    /// The camera entity.
595    #[event_target]
596    pub camera:   Entity,
597    /// The entity to frame.
598    pub target:   Entity,
599    /// Fraction of screen to leave as margin.
600    pub margin:   f32,
601    /// Animation duration (`ZERO` for instant).
602    pub duration: Duration,
603    /// Easing curve for the animation.
604    pub easing:   EaseFunction,
605}
606
607impl LookAtAndZoomToFit {
608    /// Creates a new `LookAtAndZoomToFit` with default parameters.
609    #[must_use]
610    pub const fn new(camera: Entity, target: Entity) -> Self {
611        Self {
612            camera,
613            target,
614            margin: 0.1,
615            duration: Duration::ZERO,
616            easing: EaseFunction::CubicOut,
617        }
618    }
619
620    /// Sets the margin.
621    #[must_use]
622    pub const fn margin(mut self, margin: f32) -> Self {
623        self.margin = margin;
624        self
625    }
626
627    /// Sets the animation duration.
628    #[must_use]
629    pub const fn duration(mut self, duration: Duration) -> Self {
630        self.duration = duration;
631        self
632    }
633
634    /// Sets the easing function.
635    #[must_use]
636    pub const fn easing(mut self, easing: EaseFunction) -> Self {
637        self.easing = easing;
638        self
639    }
640}
641
642/// Sets the debug overlay target without triggering a zoom.
643///
644/// Only useful with the `fit_overlay` feature enabled. This lets you point the
645/// debug overlay (`FitOverlay`) at a specific entity so you can inspect its
646/// screen-space bounds before (or without) triggering [`ZoomToFit`].
647///
648/// You do not need to call this when using [`ZoomToFit`], [`AnimateToFit`], or
649/// [`LookAtAndZoomToFit`] — those events set the fit target automatically.
650#[derive(EntityEvent, Reflect)]
651#[reflect(Event, FromReflect)]
652pub struct SetFitTarget {
653    /// The camera entity.
654    #[event_target]
655    pub camera: Entity,
656    /// The entity whose bounds to visualize.
657    pub target: Entity,
658}
659
660impl SetFitTarget {
661    /// Creates a new `SetFitTarget` event.
662    #[must_use]
663    pub const fn new(camera: Entity, target: Entity) -> Self { Self { camera, target } }
664}