Skip to main content

rmf_site_format/
robot_properties.rs

1/*
2 * Copyright (C) 2025 Open Source Robotics Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16*/
17
18use crate::*;
19#[cfg(feature = "bevy")]
20use {
21    bevy::{
22        ecs::component::Mutable,
23        prelude::{Component, *},
24    },
25    sdformat::{ElementData, ElementMap},
26    serde::de::DeserializeOwned,
27};
28
29use serde::{Deserialize, Serialize};
30use serde_json::{Map, Value};
31use thiserror::Error;
32
33#[cfg(feature = "bevy")]
34pub trait RobotProperty:
35    'static
36    + Send
37    + Sync
38    + Default
39    + Clone
40    + Component<Mutability = Mutable>
41    + PartialEq
42    + Serialize
43    + DeserializeOwned
44{
45    fn new(kind: String, config: Value) -> Self;
46
47    fn is_default(&self) -> bool;
48
49    fn kind(&self) -> Option<String>;
50
51    fn label() -> String;
52}
53
54#[cfg(feature = "bevy")]
55pub trait RobotPropertyKind:
56    'static + Send + Sync + Default + Clone + Component + PartialEq + Serialize + DeserializeOwned
57{
58    fn label() -> String;
59}
60
61#[cfg(feature = "bevy")]
62pub trait RecallPropertyKind: Recall + Default + Component<Mutability = Mutable> {
63    type Kind: RobotPropertyKind;
64    fn assume(&self) -> Self::Kind;
65}
66
67#[derive(Debug, Error)]
68pub enum RobotPropertyError {
69    #[error("Robot property [{0}] is not found on this robot")]
70    PropertyNotFound(String),
71    #[error("Robot property kind [{0}] is not found in this robot")]
72    PropertyKindNotFound(String),
73    #[error("Failed serializing robot property kind: {0}")]
74    SerializePropertyError(String),
75    #[error("Failed serializing robot property: {0}")]
76    SerializePropertyKindError(String),
77    #[error("Failed deserializing robot property: {0}")]
78    DeserializePropertyError(String),
79    #[error("Failed deserializing robot property kind: {0}")]
80    DeserializePropertyKindError(String),
81}
82
83#[cfg(feature = "bevy")]
84pub fn serialize_robot_property<Property: RobotProperty>(
85    property: Property,
86) -> Result<Value, RobotPropertyError> {
87    serde_json::to_value(property)
88        .map_err(|_| RobotPropertyError::SerializePropertyError(Property::label()))
89}
90
91#[cfg(feature = "bevy")]
92pub fn serialize_robot_property_from_kind<Property: RobotProperty, Kind: RobotPropertyKind>(
93    property_kind: Kind,
94) -> Result<Value, RobotPropertyError> {
95    let label = Kind::label();
96    serde_json::to_value(property_kind)
97        .map_err(|_| RobotPropertyError::SerializePropertyKindError(label.clone()))
98        .map(|val| Property::new(label, val))
99        .and_then(|new_property| serialize_robot_property::<Property>(new_property))
100}
101
102#[cfg(feature = "bevy")]
103pub fn serialize_robot_property_kind<Kind: RobotPropertyKind>(
104    property_kind: Kind,
105) -> Result<Value, RobotPropertyError> {
106    let label = Kind::label();
107    serde_json::to_value(property_kind)
108        .map_err(|_| RobotPropertyError::SerializePropertyKindError(label))
109}
110
111#[cfg(feature = "bevy")]
112pub fn deserialize_robot_property<Property: RobotProperty>(
113    value: Value,
114) -> Result<Property, RobotPropertyError> {
115    serde_json::from_value::<Property>(value)
116        .map_err(|_| RobotPropertyError::DeserializePropertyError(Property::label()))
117}
118
119#[cfg(feature = "bevy")]
120pub fn deserialize_robot_property_kind<Kind: RobotPropertyKind>(
121    value: Value,
122) -> Result<Kind, RobotPropertyError> {
123    serde_json::from_value::<Kind>(value)
124        .map_err(|_| RobotPropertyError::DeserializePropertyKindError(Kind::label()))
125}
126
127#[cfg(feature = "bevy")]
128/// Returns the specified RobotProperty if it is present in this robot
129pub fn retrieve_robot_property<Property: RobotProperty>(
130    robot: Robot,
131) -> Result<(Property, Value), RobotPropertyError> {
132    let property_label = Property::label();
133
134    match robot.properties.get(&property_label) {
135        Some(property_value) => {
136            if property_value.as_object().is_none_or(|m| m.is_empty()) {
137                // Robot property does not have/require any nested value, return default
138                // TODO(@xiyuoh) check again why do we want to force this to be Value::Object
139                Ok((Property::default(), Value::Object(Map::new())))
140            } else {
141                deserialize_robot_property::<Property>(property_value.clone())
142                    .map(|property| (property.clone(), property_value.clone()))
143            }
144        }
145        None => Err(RobotPropertyError::PropertyNotFound(property_label)),
146    }
147}
148
149/// Returns the specified RobotPropertyKind if it is present in this serialized RobotProperty
150#[cfg(feature = "bevy")]
151pub fn retrieve_robot_property_kind<
152    Kind: RobotPropertyKind,
153    Property: RobotProperty,
154    RecallKind: RecallPropertyKind<Kind = Kind>,
155>(
156    value: Value,
157    recall_kind: Option<&RecallKind>,
158) -> Result<Kind, RobotPropertyError> {
159    let kind_label = Kind::label();
160    if let Some((property, config)) = value
161        .as_object()
162        .and_then(|val_obj| Some(val_obj).zip(val_obj.get("config")))
163    {
164        let opt_kind_config = if property
165            .get("kind")
166            .and_then(|kind| kind.as_str())
167            .is_some_and(|label| label == &kind_label)
168        {
169            // The RobotPropertyKind was specified, use config directly
170            Some(config.clone())
171        } else if let Some(kind_config) = config.as_object().and_then(|m| m.get(&kind_label)) {
172            // No single RobotPropertyKind was specified, but it is found as one of multiple
173            // supported by this RobotProperty.
174            Some(kind_config.clone())
175        } else {
176            // No RobotPropertyKind found in the config
177            None
178        };
179
180        if let Some(kind_config) = opt_kind_config {
181            let kind = match deserialize_robot_property_kind::<Kind>(kind_config) {
182                Ok(property_kind) => {
183                    if let Some(recall) = recall_kind.filter(|_| property_kind == Kind::default()) {
184                        recall.assume()
185                    } else {
186                        property_kind
187                    }
188                }
189                Err(_) => Kind::default(),
190            };
191            return Ok(kind);
192        }
193    }
194    Err(RobotPropertyError::PropertyKindNotFound(Kind::label()))
195}
196
197/// Common RobotPropertyKind used to indicate that this RobotProperty is
198/// intentionally left empty. This helps to prevent overwriting or unwanted
199/// insertion of RobotPropertyKinds.
200#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
201#[cfg(feature = "bevy")]
202#[cfg_attr(feature = "bevy", derive(Component, Reflect))]
203#[cfg_attr(feature = "bevy", reflect(Component))]
204pub struct EmptyRobotProperty<T: RobotProperty> {
205    _ignore: std::marker::PhantomData<T>,
206}
207
208#[cfg(feature = "bevy")]
209impl<T: RobotProperty> RobotPropertyKind for EmptyRobotProperty<T> {
210    fn label() -> String {
211        format!("Empty {}", T::label())
212    }
213}
214
215#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
216#[cfg_attr(feature = "bevy", derive(Component))]
217pub struct Mobility {
218    pub kind: String,
219    pub config: Value,
220}
221
222impl Default for Mobility {
223    fn default() -> Self {
224        Self {
225            kind: String::new(),
226            config: Value::Object(Map::new()),
227        }
228    }
229}
230
231#[cfg(feature = "bevy")]
232impl RobotProperty for Mobility {
233    fn new(kind: String, config: Value) -> Self {
234        Self { kind, config }
235    }
236
237    fn is_default(&self) -> bool {
238        if *self == Self::default() {
239            return true;
240        }
241        false
242    }
243
244    fn kind(&self) -> Option<String> {
245        Some(self.kind.clone())
246    }
247
248    fn label() -> String {
249        "Mobility".to_string()
250    }
251}
252
253#[derive(Clone, Debug, Default, PartialEq)]
254#[cfg_attr(feature = "bevy", derive(Component))]
255pub struct RecallMobility {
256    pub kind: Option<String>,
257    pub config: Option<Value>,
258}
259
260impl Recall for RecallMobility {
261    type Source = Mobility;
262
263    fn remember(&mut self, source: &Mobility) {
264        self.kind = Some(source.kind.clone());
265        self.config = Some(source.config.clone());
266    }
267}
268
269// Supported kinds of Mobility
270#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
271#[cfg_attr(feature = "bevy", derive(Component, Reflect))]
272#[cfg_attr(feature = "bevy", reflect(Component))]
273pub struct DifferentialDrive {
274    pub bidirectional: bool,
275    pub rotation_center_offset: [f32; 2],
276    pub translational_speed: f32,
277    pub rotational_speed: f32,
278}
279
280impl Default for DifferentialDrive {
281    fn default() -> Self {
282        Self {
283            bidirectional: false,
284            rotation_center_offset: [0.0, 0.0],
285            translational_speed: 0.5,
286            rotational_speed: 1.0,
287        }
288    }
289}
290
291#[cfg(feature = "bevy")]
292impl RobotPropertyKind for DifferentialDrive {
293    fn label() -> String {
294        "Differential Drive".to_string()
295    }
296}
297
298#[cfg(feature = "bevy")]
299impl From<&ElementMap> for DifferentialDrive {
300    fn from(elements: &ElementMap) -> Self {
301        let mut diff_drive = DifferentialDrive::default();
302        if let Some(reversible) = elements.get("reversible") {
303            if let ElementData::String(reversible_str) = &reversible.data {
304                diff_drive.bidirectional = if reversible_str == "true" {
305                    true
306                } else if reversible_str == "false" {
307                    false
308                } else {
309                    warn!(
310                        "Found invalid slotcar reversibility data {:?},
311                                        setting DifferentialDrive reversibility to false.",
312                        reversible_str
313                    );
314                    false
315                };
316            }
317        }
318        if let Some(translational_speed) = elements
319            .get("nominal_drive_speed")
320            .and_then(|speed| f64::try_from(speed.data.clone()).ok())
321        {
322            diff_drive.translational_speed = translational_speed as f32;
323        }
324        if let Some(rotational_speed) = elements
325            .get("nominal_turn_speed")
326            .and_then(|speed| f64::try_from(speed.data.clone()).ok())
327        {
328            diff_drive.rotational_speed = rotational_speed as f32;
329        }
330
331        diff_drive
332    }
333}
334
335#[derive(Clone, Debug, Default, PartialEq)]
336#[cfg_attr(feature = "bevy", derive(Component))]
337pub struct RecallDifferentialDrive {
338    pub bidirectional: Option<bool>,
339    pub rotation_center_offset: Option<[f32; 2]>,
340    pub translational_speed: Option<f32>,
341    pub rotational_speed: Option<f32>,
342}
343
344#[cfg(feature = "bevy")]
345impl RecallPropertyKind for RecallDifferentialDrive {
346    type Kind = DifferentialDrive;
347
348    fn assume(&self) -> DifferentialDrive {
349        DifferentialDrive {
350            bidirectional: self.bidirectional.clone().unwrap_or(false),
351            rotation_center_offset: self.rotation_center_offset.clone().unwrap_or([0.0, 0.0]),
352            translational_speed: self.translational_speed.clone().unwrap_or(0.5),
353            rotational_speed: self.rotational_speed.clone().unwrap_or(1.0),
354        }
355    }
356}
357
358impl Recall for RecallDifferentialDrive {
359    type Source = DifferentialDrive;
360
361    fn remember(&mut self, source: &DifferentialDrive) {
362        self.bidirectional = Some(source.bidirectional);
363        self.rotation_center_offset = Some(source.rotation_center_offset);
364        self.translational_speed = Some(source.translational_speed);
365        self.rotational_speed = Some(source.rotational_speed);
366    }
367}
368
369#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
370#[cfg_attr(feature = "bevy", derive(Component))]
371pub struct Collision {
372    pub kind: String,
373    pub config: Value,
374}
375
376impl Default for Collision {
377    fn default() -> Self {
378        Self {
379            kind: String::new(),
380            config: Value::Object(Map::new()),
381        }
382    }
383}
384
385#[cfg(feature = "bevy")]
386impl RobotProperty for Collision {
387    fn new(kind: String, config: Value) -> Self {
388        Self { kind, config }
389    }
390
391    fn is_default(&self) -> bool {
392        if *self == Self::default() {
393            return true;
394        }
395        false
396    }
397
398    fn kind(&self) -> Option<String> {
399        Some(self.kind.clone())
400    }
401
402    fn label() -> String {
403        "Collision".to_string()
404    }
405}
406
407#[derive(Clone, Debug, Default, PartialEq)]
408#[cfg_attr(feature = "bevy", derive(Component))]
409pub struct RecallCollision {
410    pub kind: Option<String>,
411    pub config: Option<Value>,
412}
413
414impl Recall for RecallCollision {
415    type Source = Collision;
416
417    fn remember(&mut self, source: &Collision) {
418        self.kind = Some(source.kind.clone());
419        self.config = Some(source.config.clone());
420    }
421}
422
423// Supported kinds of Collision
424#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
425#[cfg_attr(feature = "bevy", derive(Component, Reflect))]
426#[cfg_attr(feature = "bevy", reflect(Component))]
427pub struct CircleCollision {
428    pub radius: f32,
429    pub offset: [f32; 2],
430}
431
432impl Default for CircleCollision {
433    fn default() -> Self {
434        Self {
435            radius: 0.0,
436            offset: [0.0, 0.0],
437        }
438    }
439}
440
441#[cfg(feature = "bevy")]
442impl RobotPropertyKind for CircleCollision {
443    fn label() -> String {
444        "Circle Collision".to_string()
445    }
446}
447
448#[derive(Clone, Debug, Default, PartialEq)]
449#[cfg_attr(feature = "bevy", derive(Component))]
450pub struct RecallCircleCollision {
451    pub radius: Option<f32>,
452    pub offset: Option<[f32; 2]>,
453}
454
455#[cfg(feature = "bevy")]
456impl RecallPropertyKind for RecallCircleCollision {
457    type Kind = CircleCollision;
458
459    fn assume(&self) -> CircleCollision {
460        CircleCollision {
461            radius: self.radius.clone().unwrap_or_default(),
462            offset: self.offset.clone().unwrap_or_default(),
463        }
464    }
465}
466
467impl Recall for RecallCircleCollision {
468    type Source = CircleCollision;
469
470    fn remember(&mut self, source: &CircleCollision) {
471        self.radius = Some(source.radius);
472        self.offset = Some(source.offset);
473    }
474}
475
476#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
477#[cfg_attr(feature = "bevy", derive(Component))]
478pub struct PowerSource {
479    pub kind: String,
480    pub config: Value,
481}
482
483impl Default for PowerSource {
484    fn default() -> Self {
485        Self {
486            kind: String::new(),
487            config: Value::Object(Map::new()),
488        }
489    }
490}
491
492#[cfg(feature = "bevy")]
493impl RobotProperty for PowerSource {
494    fn new(kind: String, config: Value) -> Self {
495        Self { kind, config }
496    }
497
498    fn is_default(&self) -> bool {
499        if *self == Self::default() {
500            return true;
501        }
502        false
503    }
504
505    fn kind(&self) -> Option<String> {
506        Some(self.kind.clone())
507    }
508
509    fn label() -> String {
510        "Power Source".to_string()
511    }
512}
513
514#[derive(Clone, Debug, Default, PartialEq)]
515#[cfg_attr(feature = "bevy", derive(Component))]
516pub struct RecallPowerSource {
517    pub kind: Option<String>,
518    pub config: Option<Value>,
519}
520
521impl Recall for RecallPowerSource {
522    type Source = PowerSource;
523
524    fn remember(&mut self, source: &PowerSource) {
525        self.kind = Some(source.kind.clone());
526        self.config = Some(source.config.clone());
527    }
528}
529
530// Supported kinds of Power Source
531#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
532#[cfg_attr(feature = "bevy", derive(Component, Reflect))]
533#[cfg_attr(feature = "bevy", reflect(Component))]
534pub struct Battery {
535    pub voltage: f32,
536    pub capacity: f32,
537    pub charging_current: f32,
538}
539
540impl Default for Battery {
541    fn default() -> Self {
542        Self {
543            voltage: 12.0,
544            capacity: 24.0,
545            charging_current: 5.0,
546        }
547    }
548}
549
550#[cfg(feature = "bevy")]
551impl RobotPropertyKind for Battery {
552    fn label() -> String {
553        "Battery".to_string()
554    }
555}
556
557#[cfg(feature = "bevy")]
558impl From<&ElementMap> for Battery {
559    fn from(elements: &ElementMap) -> Self {
560        let mut battery = Battery::default();
561        if let Some(voltage) = elements
562            .get("nominal_voltage")
563            .and_then(|voltage| f64::try_from(voltage.data.clone()).ok())
564        {
565            battery.voltage = voltage as f32;
566        }
567        if let Some(capacity) = elements
568            .get("nominal_capacity")
569            .and_then(|capacity| f64::try_from(capacity.data.clone()).ok())
570        {
571            battery.capacity = capacity as f32;
572        }
573        if let Some(current) = elements
574            .get("charging_current")
575            .and_then(|current| f64::try_from(current.data.clone()).ok())
576        {
577            battery.charging_current = current as f32;
578        }
579
580        battery
581    }
582}
583
584#[derive(Clone, Debug, Default, PartialEq)]
585#[cfg_attr(feature = "bevy", derive(Component))]
586pub struct RecallBattery {
587    pub voltage: Option<f32>,
588    pub capacity: Option<f32>,
589    pub charging_current: Option<f32>,
590}
591
592#[cfg(feature = "bevy")]
593impl RecallPropertyKind for RecallBattery {
594    type Kind = Battery;
595
596    fn assume(&self) -> Battery {
597        Battery {
598            voltage: self.voltage.clone().unwrap_or(12.0),
599            capacity: self.capacity.clone().unwrap_or(24.0),
600            charging_current: self.charging_current.clone().unwrap_or(5.0),
601        }
602    }
603}
604
605impl Recall for RecallBattery {
606    type Source = Battery;
607
608    fn remember(&mut self, source: &Battery) {
609        self.voltage = Some(source.voltage);
610        self.capacity = Some(source.capacity);
611        self.charging_current = Some(source.charging_current);
612    }
613}
614
615// TODO(@xiyuoh) Update RobotProperty trait to accommodate properties that can accommodate multiple Kinds
616#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
617#[cfg_attr(feature = "bevy", derive(Component))]
618pub struct PowerDissipation {
619    pub config: Value,
620}
621
622impl Default for PowerDissipation {
623    fn default() -> Self {
624        Self {
625            config: Value::Object(Map::new()),
626        }
627    }
628}
629
630#[cfg(feature = "bevy")]
631impl RobotProperty for PowerDissipation {
632    fn new(kind: String, config: Value) -> Self {
633        let mut property_map = Map::new();
634        property_map.insert(kind.clone(), config);
635
636        Self {
637            config: Value::Object(property_map),
638        }
639    }
640
641    fn is_default(&self) -> bool {
642        if *self == Self::default() {
643            return true;
644        }
645        false
646    }
647
648    fn kind(&self) -> Option<String> {
649        // This RobotProperty supports multiple kinds
650        None
651    }
652
653    fn label() -> String {
654        "Power Dissipation".to_string()
655    }
656}
657
658#[derive(Clone, Debug, Default, PartialEq)]
659#[cfg_attr(feature = "bevy", derive(Component))]
660pub struct RecallPowerDissipation {
661    pub config: Option<Value>,
662}
663
664impl Recall for RecallPowerDissipation {
665    type Source = PowerDissipation;
666
667    fn remember(&mut self, source: &PowerDissipation) {
668        self.config = Some(source.config.clone());
669    }
670}
671
672// Supported kinds of PowerDissipation
673#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
674#[cfg_attr(feature = "bevy", derive(Component, Reflect))]
675#[cfg_attr(feature = "bevy", reflect(Component))]
676pub struct MechanicalSystem {
677    pub mass: f32,
678    pub moment_of_inertia: f32,
679    pub friction_coefficient: f32,
680}
681
682impl Default for MechanicalSystem {
683    fn default() -> Self {
684        Self {
685            mass: 20.0,
686            moment_of_inertia: 10.0,
687            friction_coefficient: 0.22,
688        }
689    }
690}
691
692#[cfg(feature = "bevy")]
693impl RobotPropertyKind for MechanicalSystem {
694    fn label() -> String {
695        "Mechanical System".to_string()
696    }
697}
698
699#[cfg(feature = "bevy")]
700impl From<&ElementMap> for MechanicalSystem {
701    fn from(elements: &ElementMap) -> Self {
702        let mut mechanical_system = MechanicalSystem::default();
703        if let Some(mass) = elements
704            .get("mass")
705            .and_then(|mass| f64::try_from(mass.data.clone()).ok())
706        {
707            mechanical_system.mass = mass as f32;
708        }
709        if let Some(moment_of_inertia) = elements
710            .get("inertia")
711            .and_then(|moment_of_inertia| f64::try_from(moment_of_inertia.data.clone()).ok())
712        {
713            mechanical_system.moment_of_inertia = moment_of_inertia as f32;
714        }
715        if let Some(friction_coefficient) = elements
716            .get("friction_coefficient")
717            .and_then(|friction_coefficient| f64::try_from(friction_coefficient.data.clone()).ok())
718        {
719            mechanical_system.friction_coefficient = friction_coefficient as f32;
720        }
721
722        mechanical_system
723    }
724}
725
726#[derive(Clone, Debug, Default, PartialEq)]
727#[cfg_attr(feature = "bevy", derive(Component))]
728pub struct RecallMechanicalSystem {
729    pub mass: Option<f32>,
730    pub moment_of_inertia: Option<f32>,
731    pub friction_coefficient: Option<f32>,
732}
733
734#[cfg(feature = "bevy")]
735impl RecallPropertyKind for RecallMechanicalSystem {
736    type Kind = MechanicalSystem;
737
738    fn assume(&self) -> MechanicalSystem {
739        MechanicalSystem {
740            mass: self.mass.clone().unwrap_or(20.0),
741            moment_of_inertia: self.moment_of_inertia.clone().unwrap_or(10.0),
742            friction_coefficient: self.friction_coefficient.clone().unwrap_or(0.22),
743        }
744    }
745}
746
747impl Recall for RecallMechanicalSystem {
748    type Source = MechanicalSystem;
749
750    fn remember(&mut self, source: &MechanicalSystem) {
751        self.mass = Some(source.mass);
752        self.moment_of_inertia = Some(source.moment_of_inertia);
753        self.friction_coefficient = Some(source.friction_coefficient);
754    }
755}
756
757#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
758#[cfg_attr(feature = "bevy", derive(Component, Reflect))]
759#[cfg_attr(feature = "bevy", reflect(Component))]
760pub struct AmbientSystem {
761    pub idle_power: f32,
762}
763
764impl Default for AmbientSystem {
765    fn default() -> Self {
766        Self { idle_power: 20.0 }
767    }
768}
769
770#[cfg(feature = "bevy")]
771impl RobotPropertyKind for AmbientSystem {
772    fn label() -> String {
773        "Ambient System".to_string()
774    }
775}
776
777#[cfg(feature = "bevy")]
778impl From<&ElementMap> for AmbientSystem {
779    fn from(elements: &ElementMap) -> Self {
780        let mut ambient_system = AmbientSystem::default();
781        if let Some(power) = elements
782            .get("nominal_power")
783            .and_then(|power| f64::try_from(power.data.clone()).ok())
784        {
785            ambient_system.idle_power = power as f32;
786        }
787
788        ambient_system
789    }
790}
791
792#[derive(Clone, Debug, Default, PartialEq)]
793#[cfg_attr(feature = "bevy", derive(Component))]
794pub struct RecallAmbientSystem {
795    pub idle_power: Option<f32>,
796}
797
798#[cfg(feature = "bevy")]
799impl RecallPropertyKind for RecallAmbientSystem {
800    type Kind = AmbientSystem;
801
802    fn assume(&self) -> AmbientSystem {
803        AmbientSystem {
804            idle_power: self.idle_power.clone().unwrap_or(20.0),
805        }
806    }
807}
808
809impl Recall for RecallAmbientSystem {
810    type Source = AmbientSystem;
811
812    fn remember(&mut self, source: &AmbientSystem) {
813        self.idle_power = Some(source.idle_power);
814    }
815}