use glam::DVec2;
use renamite_animation::{FrameRate, LoopMode, PlayState, Playback};
use renamite_io_ren::RenFile;
use renamite_machine::{InputValue, MachineId, MachineInstance, PointerEventKind};
use renamite_model::{
CompId, NodeId, Overrides, PropPath, Scene, Value, evaluate_with, pick_selectable_with_leaf,
};
pub use renamite_model::{nodes_bounds, pick_box};
#[derive(Debug, thiserror::Error)]
pub enum PlayerError {
#[error(transparent)]
Ren(#[from] renamite_io_ren::RenError),
#[error("composition not found in project")]
MissingComp,
#[error("binary .renb support requires the `binary` feature")]
BinaryDisabled,
#[error("input is not valid UTF-8 .ren text")]
InvalidUtf8,
}
pub enum PlayMode {
Machine {
id: MachineId,
instance: MachineInstance,
background: Playback,
},
Timeline { playback: Playback },
Scrub { frame: f64 },
}
pub struct Engine {
comp: CompId,
rate: FrameRate,
pub mode: PlayMode,
paused: bool,
ov: Overrides, host_ov: Overrides, scene: Scene, hover: Option<PointerHit>, pressed: Option<PointerHit>, events: Vec<String>, }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PointerHit {
outer: NodeId,
leaf: NodeId,
}
impl Engine {
pub fn new(project: &RenFile) -> Result<Self, PlayerError> {
let comp = project.document.main;
let c = project
.document
.compositions
.get(comp)
.ok_or(PlayerError::MissingComp)?;
let playing = Playback {
state: PlayState::Playing,
head: c.range.0.0 as f64,
loop_mode: LoopMode::Loop,
range: c.range,
dir: 1.0,
};
let mode = match project.start_machine {
Some(id) if project.machines.contains_key(id) => PlayMode::Machine {
id,
instance: MachineInstance::new(&project.machines[id]),
background: playing,
},
_ => PlayMode::Timeline { playback: playing },
};
let mut e = Self {
comp,
rate: c.rate,
mode,
paused: false,
ov: Overrides::default(),
host_ov: Overrides::default(),
scene: Scene::default(),
hover: None,
pressed: None,
events: Vec::new(),
};
e.reevaluate(project);
Ok(e)
}
pub fn tick(&mut self, project: &RenFile, dt_secs: f64) -> &[String] {
self.events.clear();
if !dt_secs.is_finite() || dt_secs < 0.0 {
return &self.events;
}
if self.paused {
if dt_secs == 0.0 {
self.ov.clear();
if let PlayMode::Machine { id, instance, .. } = &mut self.mode
&& let Some(m) = project.machines.get(*id)
{
let out = instance.tick(m, &project.clips, 0.0, &mut self.ov);
self.events.extend(out.events);
}
self.apply_host_overrides();
let head = self.head();
self.scene = evaluate_with(&project.document, self.comp, head, &self.ov);
}
return &self.events;
}
if let Some(c) = project.document.compositions.get(self.comp) {
self.rate = c.rate;
}
let dt_frames = dt_secs * self.rate.fps();
if !dt_frames.is_finite() {
return &self.events;
}
self.ov.clear();
let frame = match &mut self.mode {
PlayMode::Machine {
id,
instance,
background,
} => {
background.advance(dt_secs, self.rate);
if let Some(m) = project.machines.get(*id) {
let out = instance.tick(m, &project.clips, dt_frames, &mut self.ov);
self.events.extend(out.events);
}
background.head
}
PlayMode::Timeline { playback } => {
playback.advance(dt_secs, self.rate);
playback.head
}
PlayMode::Scrub { frame } => *frame,
};
self.apply_host_overrides();
self.scene = evaluate_with(&project.document, self.comp, frame, &self.ov);
&self.events
}
fn apply_host_overrides(&mut self) {
for (id, prop, value) in self.host_ov.iter() {
self.ov.set_str(id, prop, value.clone());
}
}
pub fn reevaluate(&mut self, project: &RenFile) {
self.events.clear();
if let Some(c) = project.document.compositions.get(self.comp) {
self.rate = c.rate;
}
self.ov.clear();
if let PlayMode::Machine { id, instance, .. } = &mut self.mode
&& let Some(m) = project.machines.get(*id)
{
let _ = instance.reevaluate(m, &project.clips, &mut self.ov);
}
self.apply_host_overrides();
self.scene = evaluate_with(&project.document, self.comp, self.head(), &self.ov);
}
pub fn set_host_override(&mut self, id: NodeId, prop: PropPath, v: Value) {
self.host_ov.set(id, prop, v);
}
pub fn clear_host_overrides(&mut self) {
self.host_ov.clear();
}
pub fn bake(&mut self, project: &RenFile, frames: usize, dt_secs: f64) -> Vec<Scene> {
(0..frames)
.map(|_| {
self.tick(project, dt_secs);
self.scene.clone()
})
.collect()
}
pub fn play_machine(&mut self, project: &RenFile, id: MachineId) -> bool {
if !project.machines.contains_key(id) {
return false;
}
let background = self.playing_playback(project);
self.mode = PlayMode::Machine {
id,
instance: MachineInstance::new(&project.machines[id]),
background,
};
self.hover = None;
self.pressed = None;
true
}
pub fn play_timeline(&mut self, project: &RenFile, loop_mode: LoopMode) {
let Some(range) = project
.document
.compositions
.get(self.comp)
.map(|composition| composition.range)
else {
return;
};
let head = match &self.mode {
PlayMode::Timeline { playback } if playback.range == range => playback
.head
.clamp(range.0.0 as f64, (range.1.0 as f64).max(range.0.0 as f64)),
PlayMode::Machine { background, .. } if background.range == range => background
.head
.clamp(range.0.0 as f64, (range.1.0 as f64).max(range.0.0 as f64)),
_ => range.0.0 as f64,
};
self.mode = PlayMode::Timeline {
playback: Playback {
state: PlayState::Playing,
head,
loop_mode,
range,
dir: 1.0,
},
};
self.hover = None;
self.pressed = None;
}
pub fn set_timeline_playback(&mut self, pb: Playback) {
match &mut self.mode {
PlayMode::Timeline { playback } => *playback = pb,
PlayMode::Scrub { .. } => {
self.mode = PlayMode::Timeline { playback: pb };
self.hover = None;
self.pressed = None;
}
PlayMode::Machine { .. } => {}
}
}
pub fn scrub(&mut self, project: &RenFile, frame: f64) {
if !frame.is_finite() {
return;
}
self.mode = PlayMode::Scrub { frame };
self.reevaluate(project);
}
pub fn set_composition(&mut self, project: &RenFile, comp: CompId) -> bool {
let Some(c) = project.document.compositions.get(comp) else {
return false;
};
self.comp = comp;
self.rate = c.rate;
self.play_timeline(project, LoopMode::Loop);
self.reevaluate(project);
true
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
pub fn is_paused(&self) -> bool {
self.paused
}
fn playing_playback(&self, project: &RenFile) -> Playback {
let range = project
.document
.compositions
.get(self.comp)
.map(|composition| composition.range)
.unwrap_or((renamite_animation::Frame(0), renamite_animation::Frame(180)));
let head = match &self.mode {
PlayMode::Timeline { playback } if playback.range == range => playback.head,
PlayMode::Machine { background, .. } if background.range == range => background.head,
PlayMode::Scrub { frame } => *frame,
_ => range.0.0 as f64,
};
let head = head.clamp(range.0.0 as f64, (range.1.0 as f64).max(range.0.0 as f64));
Playback {
state: PlayState::Playing,
head,
loop_mode: LoopMode::Loop,
range,
dir: 1.0,
}
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn events(&self) -> &[String] {
&self.events
}
pub fn rate(&self) -> FrameRate {
self.rate
}
pub fn composition(&self) -> CompId {
self.comp
}
pub fn head(&self) -> f64 {
match &self.mode {
PlayMode::Machine { background, .. } => background.head,
PlayMode::Timeline { playback } => playback.head,
PlayMode::Scrub { frame } => *frame,
}
}
pub fn active_machine_states(&self) -> Option<Vec<usize>> {
let PlayMode::Machine { instance, .. } = &self.mode else {
return None;
};
Some(instance.layer_states().collect())
}
pub fn machine_inputs(&self) -> Option<&[InputValue]> {
match &self.mode {
PlayMode::Machine { instance, .. } => Some(&instance.inputs),
_ => None,
}
}
pub fn apply_machine_inputs(&mut self, inputs: &[InputValue]) {
let PlayMode::Machine { instance, .. } = &mut self.mode else {
return;
};
for (dst, src) in instance.inputs.iter_mut().zip(inputs.iter()) {
match (dst, src) {
(InputValue::Bool(a), InputValue::Bool(b)) => *a = *b,
(InputValue::Number(a), InputValue::Number(b)) => *a = *b,
(InputValue::Trigger { fired: a }, InputValue::Trigger { fired: b }) => *a = *b,
_ => {}
}
}
}
pub fn playing_machine_id(&self) -> Option<MachineId> {
match &self.mode {
PlayMode::Machine { id, .. } => Some(*id),
_ => None,
}
}
pub fn set_bool(&mut self, project: &RenFile, name: &str, v: bool) -> bool {
self.with_input(project, name, |inst, i| inst.set_bool(i, v))
}
pub fn set_number(&mut self, project: &RenFile, name: &str, v: f64) -> bool {
self.with_input(project, name, |inst, i| inst.set_number(i, v))
}
pub fn fire(&mut self, project: &RenFile, name: &str) -> bool {
self.with_input(project, name, |inst, i| inst.fire(i))
}
fn with_input(
&mut self,
project: &RenFile,
name: &str,
f: impl FnOnce(&mut MachineInstance, usize),
) -> bool {
if let PlayMode::Machine { id, instance, .. } = &mut self.mode
&& let Some(m) = project.machines.get(*id)
&& let Some(i) = MachineInstance::input_index(m, name)
{
f(instance, i);
return true;
}
false
}
pub fn pointer_move(&mut self, project: &RenFile, pt: DVec2) {
let hit = self.pointer_hit(project, pt);
if hit != self.hover {
if let Some(prev) = self.hover {
self.route_hit(project, prev, PointerEventKind::Exit);
}
if let Some(now) = hit {
self.route_hit(project, now, PointerEventKind::Enter);
}
self.hover = hit;
}
}
pub fn pointer_down(&mut self, project: &RenFile, pt: DVec2) {
self.pressed = self.pointer_hit(project, pt);
if let Some(hit) = self.pressed {
self.route_hit(project, hit, PointerEventKind::Down);
}
}
pub fn pointer_up(&mut self, project: &RenFile, pt: DVec2) {
let release_hit = self.pointer_hit(project, pt);
if let Some(pressed) = self.pressed {
self.route_hit(project, pressed, PointerEventKind::Up);
if release_hit == Some(pressed) {
self.route_hit(project, pressed, PointerEventKind::Click);
}
}
self.pressed = None;
}
pub fn pointer_leave(&mut self, project: &RenFile) {
if let Some(prev) = self.hover.take() {
self.route_hit(project, prev, PointerEventKind::Exit);
}
if let Some(pressed) = self.pressed.take() {
self.route_hit(project, pressed, PointerEventKind::Up);
}
}
fn pointer_hit(&self, project: &RenFile, pt: DVec2) -> Option<PointerHit> {
pick_selectable_with_leaf(&project.document, &self.scene, self.comp, pt)
.map(|(outer, leaf)| PointerHit { outer, leaf })
}
fn route_hit(&mut self, project: &RenFile, hit: PointerHit, kind: PointerEventKind) {
if let PlayMode::Machine { id, instance, .. } = &mut self.mode
&& let Some(m) = project.machines.get(*id)
{
let mut current = Some(hit.leaf);
let mut seen = std::collections::HashSet::new();
let mut chain = Vec::new();
while let Some(node) = current {
if !seen.insert(node) {
return;
}
let Some(entry) = project.document.nodes.get(node) else {
return;
};
if entry.locked {
return;
}
chain.push(node);
if node == hit.outer {
break;
}
current = entry.parent;
}
if !seen.contains(&hit.outer) {
return;
}
for node in chain {
instance.pointer_event(m, node, kind);
}
}
}
}
pub struct Player {
pub project: RenFile,
pub engine: Engine,
}
impl Player {
pub fn new(project: RenFile) -> Result<Self, PlayerError> {
let engine = Engine::new(&project)?;
Ok(Self { project, engine })
}
pub fn from_ren_str(text: &str) -> Result<Self, PlayerError> {
Self::new(renamite_io_ren::open(text)?)
}
#[cfg(feature = "binary")]
pub fn from_ren_bytes(bytes: &[u8]) -> Result<Self, PlayerError> {
Self::new(renamite_io_ren::open_binary(bytes)?)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, PlayerError> {
if renamite_io_ren::is_binary(bytes) {
#[cfg(feature = "binary")]
{
return Self::from_ren_bytes(bytes);
}
#[cfg(not(feature = "binary"))]
{
return Err(PlayerError::BinaryDisabled);
}
}
if bytes.len() > renamite_io_ren::MAX_TEXT_BYTES {
return Err(renamite_io_ren::RenError::InputLimit("text input is too large").into());
}
let text = std::str::from_utf8(bytes).map_err(|_| PlayerError::InvalidUtf8)?;
Self::from_ren_str(text)
}
pub fn tick(&mut self, dt_secs: f64) -> &[String] {
self.engine.tick(&self.project, dt_secs)
}
pub fn bake(&mut self, frames: usize, dt_secs: f64) -> Vec<Scene> {
self.engine.bake(&self.project, frames, dt_secs)
}
pub fn scene(&self) -> &Scene {
self.engine.scene()
}
pub fn events(&self) -> &[String] {
self.engine.events()
}
pub fn head(&self) -> f64 {
self.engine.head()
}
pub fn rate(&self) -> FrameRate {
self.engine.rate()
}
pub fn composition(&self) -> CompId {
self.engine.composition()
}
pub fn active_machine_states(&self) -> Option<Vec<usize>> {
self.engine.active_machine_states()
}
pub fn artboard_size(&self) -> (u32, u32) {
self.project
.document
.compositions
.get(self.engine.composition())
.map(|composition| composition.size)
.unwrap_or((1, 1))
}
pub fn reevaluate(&mut self) {
self.engine.reevaluate(&self.project);
}
pub fn pause(&mut self) {
self.engine.pause();
}
pub fn resume(&mut self) {
self.engine.resume();
}
pub fn is_paused(&self) -> bool {
self.engine.is_paused()
}
pub fn scrub(&mut self, frame: f64) {
self.engine.scrub(&self.project, frame);
}
pub fn set_composition(&mut self, comp: CompId) -> bool {
self.engine.set_composition(&self.project, comp)
}
pub fn set_host_override(&mut self, id: NodeId, prop: PropPath, v: Value) {
self.engine.set_host_override(id, prop, v);
self.engine.reevaluate(&self.project);
}
pub fn clear_host_overrides(&mut self) {
self.engine.clear_host_overrides();
self.engine.reevaluate(&self.project);
}
pub fn set_bool(&mut self, name: &str, v: bool) -> bool {
self.engine.set_bool(&self.project, name, v)
}
pub fn set_number(&mut self, name: &str, v: f64) -> bool {
self.engine.set_number(&self.project, name, v)
}
pub fn fire(&mut self, name: &str) -> bool {
self.engine.fire(&self.project, name)
}
pub fn pointer_move(&mut self, pt: DVec2) {
self.engine.pointer_move(&self.project, pt);
let _ = self.engine.tick(&self.project, 0.0);
}
pub fn pointer_down(&mut self, pt: DVec2) {
self.engine.pointer_down(&self.project, pt);
let _ = self.engine.tick(&self.project, 0.0);
}
pub fn pointer_up(&mut self, pt: DVec2) {
self.engine.pointer_up(&self.project, pt);
let _ = self.engine.tick(&self.project, 0.0);
}
pub fn pointer_leave(&mut self) {
self.engine.pointer_leave(&self.project);
let _ = self.engine.tick(&self.project, 0.0);
}
pub fn play_machine(&mut self, id: MachineId) -> bool {
self.engine.play_machine(&self.project, id)
}
pub fn play_timeline(&mut self, loop_mode: LoopMode) {
self.engine.play_timeline(&self.project, loop_mode)
}
pub fn set_timeline_playback(&mut self, pb: Playback) {
self.engine.set_timeline_playback(pb)
}
}
#[cfg(test)]
mod tests {
use super::*;
use kurbo::Shape;
use renamite_animation::{Animated, EasingHandle, Frame, Interpolation};
use renamite_machine::{
Clip, ClipMap, Condition, InputDef, InputKind, Listener, ListenerAction, Machine,
MachineLayer, MachineMap, State, StateKind, Track, Transition,
};
use renamite_model::{
Color, Document, FillRule, KeyframeData, Node, NodeKind, Parent, PropPath, ScenePaint,
ShapeKind, StyleKind, StylePaint, Value,
};
fn rect_kind(size: f64) -> NodeKind {
NodeKind::Shape(ShapeKind::Rect {
pos: Animated::new(DVec2::ZERO),
size: Animated::new(DVec2::splat(size)),
rounded: Animated::new(0.0),
})
}
fn fill_kind() -> NodeKind {
NodeKind::Style(StyleKind::Fill {
paint: StylePaint::solid(Color::BLACK),
rule: FillRule::NonZero,
})
}
fn key_v2(f: i64, x: f64, y: f64) -> KeyframeData {
KeyframeData {
frame: Frame(f),
value: Value::DVec2(DVec2::new(x, y)),
interpolation: Interpolation::Linear,
ease_out: EasingHandle::LINEAR_OUT,
ease_in: EasingHandle::LINEAR_IN,
}
}
fn center_x(s: &Scene) -> f64 {
s.items[0].path.bounding_box().center().x
}
fn static_box() -> (RenFile, NodeId, NodeId) {
let mut doc = Document::empty();
let comp = doc.main;
let shape = doc.create_node(Node::new("box", rect_kind(100.0)));
doc.attach(shape, Parent::Comp(comp), 0).unwrap();
let fill = doc.create_node(Node::new("fill", fill_kind()));
doc.attach(fill, Parent::Comp(comp), 1).unwrap();
(RenFile::new(doc, "static"), shape, fill)
}
fn moving_box() -> (RenFile, NodeId, NodeId) {
let (mut f, shape, fill) = static_box();
let mut clips = ClipMap::default();
let mv = clips.insert(Clip {
name: "move".into(),
range: (Frame(0), Frame(10)),
tracks: vec![Track {
node: shape,
prop: PropPath::new("transform.position"),
keys: vec![key_v2(0, 0.0, 0.0), key_v2(10, 50.0, 0.0)],
}],
events: vec![],
});
let machine = Machine {
name: "hover".into(),
inputs: vec![InputDef {
name: "go".into(),
kind: InputKind::Bool { default: false },
}],
layers: vec![MachineLayer {
name: "base".into(),
entry: 0,
any_transitions: vec![],
blend: Default::default(),
states: vec![
State {
name: "Idle".into(),
kind: StateKind::Empty,
transitions: vec![Transition {
to: 1,
duration: 0.0,
exit_time: None,
conditions: vec![Condition::BoolIs {
input: 0,
value: true,
}],
}],
graph_pos: None,
sync_group: None,
},
State {
name: "Move".into(),
kind: StateKind::Clip {
clip: mv,
speed: 1.0,
loop_mode: LoopMode::Once,
},
transitions: vec![],
graph_pos: None,
sync_group: None,
},
],
}],
listeners: vec![Listener {
node: shape,
event: PointerEventKind::Enter,
action: ListenerAction::SetBool {
input: 0,
value: true,
},
}],
root_motion: None,
};
let mut machines = MachineMap::default();
let id = machines.insert(machine);
f.clips = clips;
f.machines = machines;
f.start_machine = Some(id);
(f, shape, fill)
}
#[test]
fn timeline_plays_and_renders() {
let (proj, _, _) = static_box();
let mut p = Player::new(proj).unwrap();
assert_eq!(p.scene().items.len(), 1);
let h0 = p.head();
p.tick(0.5);
assert!(p.head() > h0);
}
#[test]
fn machine_composites_over_background_timeline() {
let (mut proj, _, fill) = moving_box();
let prop = PropPath::new("opacity");
proj.document
.add_keyframe(fill, &prop, Frame(0), &Value::F64(0.0))
.unwrap();
proj.document
.add_keyframe(fill, &prop, Frame(60), &Value::F64(1.0))
.unwrap();
let mut p = Player::new(proj).unwrap();
assert!(p.set_bool("go", true));
p.tick(0.25); p.tick(0.25);
let item = &p.scene().items[0];
assert!(
(center_x(p.scene()) - 50.0).abs() < 1.0,
"machine override applied"
);
assert!(
(item.opacity - 0.5).abs() < 0.02,
"background timeline still animating (opacity={})",
item.opacity
);
}
#[test]
fn host_override_wins_over_machine_and_survives_tick() {
let (proj, shape, _) = moving_box();
let mut p = Player::new(proj).unwrap();
assert!(p.set_bool("go", true));
p.tick(0.5); p.tick(0.5); assert!((center_x(p.scene()) - 50.0).abs() < 1.0);
p.set_host_override(
shape,
PropPath::new("transform.position"),
Value::DVec2(DVec2::new(120.0, 0.0)),
);
assert!(
(center_x(p.scene()) - 120.0).abs() < 1e-6,
"set_host_override re-evaluates immediately"
);
p.tick(0.25);
assert!(
(center_x(p.scene()) - 120.0).abs() < 1e-6,
"host override survives tick"
);
p.clear_host_overrides();
p.tick(0.25);
assert!(
(center_x(p.scene()) - 50.0).abs() < 1.0,
"machine override returns after clear"
);
}
#[test]
fn scrub_locks_frame() {
let (mut proj, _, fill) = static_box();
let prop = PropPath::new("opacity");
proj.document
.add_keyframe(fill, &prop, Frame(0), &Value::F64(0.0))
.unwrap();
proj.document
.add_keyframe(fill, &prop, Frame(60), &Value::F64(1.0))
.unwrap();
let mut e = Engine::new(&proj).unwrap();
e.scrub(&proj, 30.0);
e.tick(&proj, 1.0); assert_eq!(e.head(), 30.0);
assert!((e.scene().items[0].opacity - 0.5).abs() < 1e-9);
}
#[test]
fn hover_enter_drives_listener_and_leave_exits() {
let (proj, _, _) = moving_box();
let mut p = Player::new(proj).unwrap();
p.tick(0.1);
assert!(center_x(p.scene()).abs() < 1e-6);
p.pointer_move(DVec2::ZERO); for _ in 0..5 {
p.tick(0.1);
}
assert!(center_x(p.scene()) > 10.0);
p.pointer_leave(); }
#[test]
fn live_edit_between_ticks_preserves_machine_state() {
let (mut proj, _, fill) = moving_box();
let mut e = Engine::new(&proj).unwrap();
e.set_bool(&proj, "go", true);
e.tick(&proj, 0.25);
e.tick(&proj, 0.25);
assert!((center_x(e.scene()) - 50.0).abs() < 1.0);
let red = Color::rgba(1.0, 0.0, 0.0, 1.0);
proj.document
.set_static(fill, &PropPath::new("fill.color"), &Value::Color(red))
.unwrap();
e.tick(&proj, 0.01);
let item = &e.scene().items[0];
assert_eq!(
item.paint,
ScenePaint::Solid(red),
"edit visible immediately"
);
assert!(
(center_x(e.scene()) - 50.0).abs() < 1.0,
"machine override survived the edit"
);
}
#[test]
fn zero_dt_tick_applies_host_override_while_paused() {
let (proj, shape, _) = static_box();
let mut p = Player::new(proj).unwrap();
p.pause();
assert!(center_x(p.scene()).abs() < 1e-6);
p.engine.set_host_override(
shape,
PropPath::new("transform.position"),
Value::DVec2(DVec2::new(120.0, 0.0)),
);
p.tick(0.0);
assert!(
(center_x(p.scene()) - 120.0).abs() < 1e-6,
"paused zero-dt tick must re-evaluate, x={}",
center_x(p.scene())
);
p.tick(0.5);
assert!((center_x(p.scene()) - 120.0).abs() < 1e-6);
}
#[test]
fn pointer_transition_applies_while_paused() {
let (proj, _, _) = moving_box();
let mut p = Player::new(proj).unwrap();
p.pause();
assert_eq!(p.active_machine_states(), Some(vec![0]));
p.pointer_move(DVec2::ZERO);
assert_eq!(
p.active_machine_states(),
Some(vec![1]),
"transition must apply while paused"
);
}
}