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}