openscenario-rs 0.3.1

Rust library for parsing and manipulating OpenSCENARIO files
Documentation
//! Vehicle entity builder with fluent API

use crate::types::{
    basic::{Double, OSString},
    entities::axles::Axles,
    entities::vehicle::{Performance, Properties},
    entities::{ScenarioObject, Vehicle},
    enums::VehicleCategory,
    geometry::{BoundingBox, Center, Dimensions},
};

/// Builder for vehicle entities with position integration
pub struct VehicleBuilder<'parent> {
    parent: &'parent mut crate::builder::scenario::ScenarioBuilder<
        crate::builder::scenario::HasEntities,
    >,
    name: String,
    vehicle_data: PartialVehicleData,
}

/// Detached vehicle builder that doesn't hold references
pub struct DetachedVehicleBuilder {
    name: String,
    vehicle_data: PartialVehicleData,
}

#[derive(Debug, Default)]
struct PartialVehicleData {
    name: Option<String>,
    vehicle_category: Option<VehicleCategory>,
    properties: Option<Properties>,
    bounding_box: Option<BoundingBox>,
    performance: Option<Performance>,
    axles: Option<Axles>,
}

impl<'parent> VehicleBuilder<'parent> {
    pub fn new(
        parent: &'parent mut crate::builder::scenario::ScenarioBuilder<
            crate::builder::scenario::HasEntities,
        >,
        name: &str,
    ) -> Self {
        Self {
            parent,
            name: name.to_string(),
            vehicle_data: PartialVehicleData::default(),
        }
    }

    /// Set vehicle as passenger car
    pub fn car(mut self) -> Self {
        self.vehicle_data.vehicle_category = Some(VehicleCategory::Car);
        self.vehicle_data.name = Some("PassengerCar".to_string());

        // Default car dimensions
        self.vehicle_data.bounding_box = Some(BoundingBox {
            center: Center {
                x: Double::literal(1.4),
                y: Double::literal(0.0),
                z: Double::literal(0.9),
            },
            dimensions: Dimensions {
                width: Double::literal(1.8),
                length: Double::literal(4.5),
                height: Double::literal(1.4),
            },
        });

        // Default car performance
        self.vehicle_data.performance = Some(Performance::default());

        // Default car axles
        self.vehicle_data.axles = Some(Axles::car());

        self
    }

    /// Set vehicle as truck
    pub fn truck(mut self) -> Self {
        self.vehicle_data.vehicle_category = Some(VehicleCategory::Truck);
        self.vehicle_data.name = Some("Truck".to_string());

        // Default truck dimensions
        self.vehicle_data.bounding_box = Some(BoundingBox {
            center: Center {
                x: Double::literal(4.0),
                y: Double::literal(0.0),
                z: Double::literal(1.5),
            },
            dimensions: Dimensions {
                width: Double::literal(2.5),
                length: Double::literal(8.0),
                height: Double::literal(3.0),
            },
        });

        // Default truck performance
        self.vehicle_data.performance = Some(Performance {
            max_speed: Double::literal(120.0),
            max_acceleration: Double::literal(3.0),
            max_deceleration: Double::literal(8.0),
        });

        // Default truck axles
        self.vehicle_data.axles = Some(Axles::truck());

        self
    }

    /// Set custom dimensions
    pub fn with_dimensions(mut self, length: f64, width: f64, height: f64) -> Self {
        let existing_bbox = self.vehicle_data.bounding_box.unwrap_or_default();

        self.vehicle_data.bounding_box = Some(BoundingBox {
            center: existing_bbox.center,
            dimensions: Dimensions {
                width: Double::literal(width),
                length: Double::literal(length),
                height: Double::literal(height),
            },
        });

        self
    }

    /// Set custom performance characteristics
    pub fn with_performance(
        mut self,
        max_speed: f64,
        max_acceleration: f64,
        max_deceleration: f64,
    ) -> Self {
        self.vehicle_data.performance = Some(Performance {
            max_speed: Double::literal(max_speed),
            max_acceleration: Double::literal(max_acceleration),
            max_deceleration: Double::literal(max_deceleration),
        });
        self
    }

    /// Finish vehicle and add to scenario
    pub fn finish(
        self,
    ) -> &'parent mut crate::builder::scenario::ScenarioBuilder<crate::builder::scenario::HasEntities>
    {
        let vehicle = Vehicle {
            name: OSString::literal(
                self.vehicle_data
                    .name
                    .unwrap_or_else(|| "DefaultVehicle".to_string()),
            ),
            vehicle_category: self
                .vehicle_data
                .vehicle_category
                .unwrap_or(VehicleCategory::Car),
            bounding_box: self.vehicle_data.bounding_box.unwrap_or_default(),
            performance: self.vehicle_data.performance.unwrap_or_default(),
            axles: self.vehicle_data.axles.unwrap_or_else(|| Axles::car()),
            properties: self.vehicle_data.properties,
        };

        let scenario_object = ScenarioObject::new_vehicle(self.name.clone(), vehicle);

        // Add to parent's entities
        if let Some(ref mut entities) = self.parent.data.entities {
            entities.add_object(scenario_object);
        }

        self.parent
    }

    /// Convert to detached builder for closure-based configuration
    pub fn detached(self) -> DetachedVehicleBuilder {
        DetachedVehicleBuilder {
            name: self.name,
            vehicle_data: self.vehicle_data,
        }
    }
}

impl DetachedVehicleBuilder {
    /// Create a new detached vehicle builder
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            vehicle_data: PartialVehicleData::default(),
        }
    }

    /// Set vehicle as passenger car
    pub fn car(mut self) -> Self {
        self.vehicle_data.vehicle_category = Some(VehicleCategory::Car);
        self.vehicle_data.name = Some("PassengerCar".to_string());

        // Default car dimensions
        self.vehicle_data.bounding_box = Some(BoundingBox {
            center: Center {
                x: Double::literal(1.4),
                y: Double::literal(0.0),
                z: Double::literal(0.9),
            },
            dimensions: Dimensions {
                width: Double::literal(1.8),
                length: Double::literal(4.5),
                height: Double::literal(1.4),
            },
        });

        // Default car performance
        self.vehicle_data.performance = Some(Performance::default());

        // Default car axles
        self.vehicle_data.axles = Some(Axles::car());

        self
    }

    /// Set vehicle as truck
    pub fn truck(mut self) -> Self {
        self.vehicle_data.vehicle_category = Some(VehicleCategory::Truck);
        self.vehicle_data.name = Some("Truck".to_string());

        // Default truck dimensions
        self.vehicle_data.bounding_box = Some(BoundingBox {
            center: Center {
                x: Double::literal(4.0),
                y: Double::literal(0.0),
                z: Double::literal(1.5),
            },
            dimensions: Dimensions {
                width: Double::literal(2.5),
                length: Double::literal(8.0),
                height: Double::literal(3.0),
            },
        });

        // Default truck performance
        self.vehicle_data.performance = Some(Performance {
            max_speed: Double::literal(120.0),
            max_acceleration: Double::literal(3.0),
            max_deceleration: Double::literal(8.0),
        });

        // Default truck axles
        self.vehicle_data.axles = Some(Axles::truck());

        self
    }

    /// Set custom dimensions
    pub fn with_dimensions(mut self, length: f64, width: f64, height: f64) -> Self {
        let existing_bbox = self.vehicle_data.bounding_box.unwrap_or_default();

        self.vehicle_data.bounding_box = Some(BoundingBox {
            center: existing_bbox.center,
            dimensions: Dimensions {
                width: Double::literal(width),
                length: Double::literal(length),
                height: Double::literal(height),
            },
        });

        self
    }

    /// Set custom performance characteristics
    pub fn with_performance(
        mut self,
        max_speed: f64,
        max_acceleration: f64,
        max_deceleration: f64,
    ) -> Self {
        self.vehicle_data.performance = Some(Performance {
            max_speed: Double::literal(max_speed),
            max_acceleration: Double::literal(max_acceleration),
            max_deceleration: Double::literal(max_deceleration),
        });
        self
    }

    /// Build the vehicle object
    pub fn build(self) -> ScenarioObject {
        let vehicle = Vehicle {
            name: OSString::literal(
                self.vehicle_data
                    .name
                    .unwrap_or_else(|| "DefaultVehicle".to_string()),
            ),
            vehicle_category: self
                .vehicle_data
                .vehicle_category
                .unwrap_or(VehicleCategory::Car),
            bounding_box: self.vehicle_data.bounding_box.unwrap_or_default(),
            performance: self.vehicle_data.performance.unwrap_or_default(),
            axles: self.vehicle_data.axles.unwrap_or_else(|| Axles::car()),
            properties: self.vehicle_data.properties,
        };

        ScenarioObject::new_vehicle(self.name.clone(), vehicle)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_detached_builder_defaults_when_no_preset_called() {
        let obj = DetachedVehicleBuilder::new("ego").build();
        let v = obj.vehicle.as_ref().unwrap();
        assert_eq!(v.name.as_literal(), Some(&"DefaultVehicle".to_string()));
        assert_eq!(v.vehicle_category, VehicleCategory::Car);
    }

    #[test]
    fn test_car_preset_sets_category_and_dimensions() {
        let obj = DetachedVehicleBuilder::new("ego").car().build();
        let v = obj.vehicle.as_ref().unwrap();
        assert_eq!(v.vehicle_category, VehicleCategory::Car);
        assert_eq!(v.name.as_literal(), Some(&"PassengerCar".to_string()));
        assert_eq!(v.bounding_box.dimensions.length.as_literal(), Some(&4.5));
    }

    #[test]
    fn test_truck_preset_overrides_car_preset() {
        let obj = DetachedVehicleBuilder::new("ego").car().truck().build();
        let v = obj.vehicle.as_ref().unwrap();
        assert_eq!(v.vehicle_category, VehicleCategory::Truck);
        assert_eq!(v.bounding_box.dimensions.length.as_literal(), Some(&8.0));
        assert_eq!(v.performance.max_speed.as_literal(), Some(&120.0));
    }

    #[test]
    fn test_with_dimensions_overrides_preset_but_preserves_center() {
        let obj = DetachedVehicleBuilder::new("ego")
            .car()
            .with_dimensions(5.0, 2.0, 1.6)
            .build();
        let v = obj.vehicle.as_ref().unwrap();
        assert_eq!(v.bounding_box.dimensions.length.as_literal(), Some(&5.0));
        assert_eq!(v.bounding_box.dimensions.width.as_literal(), Some(&2.0));
        assert_eq!(v.bounding_box.dimensions.height.as_literal(), Some(&1.6));
        // Center preserved from car preset
        assert_eq!(v.bounding_box.center.x.as_literal(), Some(&1.4));
    }

    #[test]
    fn test_with_performance_overrides_preset() {
        let obj = DetachedVehicleBuilder::new("ego")
            .truck()
            .with_performance(200.0, 5.0, 10.0)
            .build();
        let v = obj.vehicle.as_ref().unwrap();
        assert_eq!(v.performance.max_speed.as_literal(), Some(&200.0));
        assert_eq!(v.performance.max_acceleration.as_literal(), Some(&5.0));
        assert_eq!(v.performance.max_deceleration.as_literal(), Some(&10.0));
    }
}