1use crate::{PointColor, VisualPrimitive, VisualStyle, VizError, VizResult};
2
3#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct LayerId(String);
7
8impl LayerId {
9 pub fn try_new(value: impl Into<String>) -> VizResult<Self> {
11 let value = value.into();
12 if value.trim().is_empty() {
13 return Err(VizError::InvalidLayer("layer identifier must not be empty".into()));
14 }
15 Ok(Self(value))
16 }
17
18 #[must_use]
20 pub fn as_str(&self) -> &str {
21 &self.0
22 }
23}
24
25#[derive(Clone, Debug, PartialEq)]
27pub struct VisualLayer<'a> {
28 pub id: LayerId,
30 pub label: String,
32 pub visible: bool,
34 pub primitive: VisualPrimitive<'a>,
36 pub style: VisualStyle,
38}
39
40impl<'a> VisualLayer<'a> {
41 pub fn try_new(
43 id: LayerId,
44 label: impl Into<String>,
45 primitive: VisualPrimitive<'a>,
46 style: VisualStyle,
47 ) -> VizResult<Self> {
48 validate_compatibility(&primitive, &style)?;
49 Ok(Self { id, label: label.into(), visible: true, primitive, style })
50 }
51}
52
53fn validate_compatibility(primitive: &VisualPrimitive<'_>, style: &VisualStyle) -> VizResult<()> {
54 match (primitive, style) {
55 (VisualPrimitive::Points(points), VisualStyle::Points(point_style)) => {
56 match &point_style.color {
57 PointColor::Rgb if points.rgb.is_none() => Err(VizError::InvalidStyle(
58 "RGB point style requires borrowed RGB columns".into(),
59 )),
60 PointColor::Scalar { .. } if points.scalar.is_none() => {
61 Err(VizError::InvalidStyle(
62 "scalar point style requires a borrowed scalar column".into(),
63 ))
64 }
65 _ => Ok(()),
66 }
67 }
68 (VisualPrimitive::Points(_), VisualStyle::Uniform(_)) => Ok(()),
69 (_, VisualStyle::Points(_)) => {
70 Err(VizError::InvalidStyle("point style can only be applied to point geometry".into()))
71 }
72 (_, VisualStyle::Uniform(_)) => Ok(()),
73 }
74}
75
76#[derive(Clone, Debug, Default, PartialEq)]
78pub struct VisualScene<'a> {
79 layers: Vec<VisualLayer<'a>>,
80}
81
82impl<'a> VisualScene<'a> {
83 #[must_use]
85 pub const fn new() -> Self {
86 Self { layers: Vec::new() }
87 }
88
89 pub fn add_layer(&mut self, layer: VisualLayer<'a>) -> VizResult<()> {
91 if self.layers.iter().any(|current| current.id == layer.id) {
92 return Err(VizError::InvalidLayer(format!(
93 "duplicate layer identifier `{}`",
94 layer.id.as_str()
95 )));
96 }
97 self.layers.push(layer);
98 Ok(())
99 }
100
101 #[must_use]
103 pub fn layers(&self) -> &[VisualLayer<'a>] {
104 &self.layers
105 }
106
107 #[must_use]
109 pub fn layer(&self, id: &LayerId) -> Option<&VisualLayer<'a>> {
110 self.layers.iter().find(|layer| &layer.id == id)
111 }
112
113 pub fn remove_layer(&mut self, id: &LayerId) -> Option<VisualLayer<'a>> {
115 let index = self.layers.iter().position(|layer| &layer.id == id)?;
116 Some(self.layers.remove(index))
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use crate::{
123 LayerId, LineListView, LinearRgba, VisualLayer, VisualPrimitive, VisualScene, VisualStyle,
124 };
125
126 fn layer<'a>(id: &str, positions: &'a [f32]) -> VisualLayer<'a> {
127 VisualLayer::try_new(
128 LayerId::try_new(id).unwrap(),
129 id,
130 VisualPrimitive::Lines(LineListView::try_new(positions).unwrap()),
131 VisualStyle::Uniform(LinearRgba::WHITE),
132 )
133 .unwrap()
134 }
135
136 #[test]
137 fn scene_rejects_duplicate_ids_and_preserves_order() {
138 let positions = [0.0; 6];
139 let mut scene = VisualScene::new();
140 scene.add_layer(layer("first", &positions)).unwrap();
141 scene.add_layer(layer("second", &positions)).unwrap();
142 assert!(scene.add_layer(layer("first", &positions)).is_err());
143 assert_eq!(scene.layers()[0].id.as_str(), "first");
144
145 let first = LayerId::try_new("first").unwrap();
146 scene.remove_layer(&first).unwrap();
147 assert_eq!(scene.layers()[0].id.as_str(), "second");
148 }
149
150 #[test]
151 fn layer_rejects_style_without_required_attributes() {
152 use crate::{ColorMap, PointCloudView, PointColor, PointStyle, PositionColumns3};
153
154 let positions = PositionColumns3::try_new(&[0.0], &[0.0], &[0.0]).unwrap();
155 let primitive = VisualPrimitive::Points(PointCloudView::positions_only(positions));
156 let rgb_style = VisualStyle::Points(PointStyle::try_new(1.0, PointColor::Rgb).unwrap());
157 assert!(VisualLayer::try_new(
158 LayerId::try_new("points").unwrap(),
159 "points",
160 primitive,
161 rgb_style,
162 )
163 .is_err());
164
165 let scalar_style = VisualStyle::Points(
166 PointStyle::try_new(
167 1.0,
168 PointColor::Scalar { min: 0.0, max: 1.0, map: ColorMap::Viridis },
169 )
170 .unwrap(),
171 );
172 assert!(VisualLayer::try_new(
173 LayerId::try_new("scalar").unwrap(),
174 "scalar",
175 primitive,
176 scalar_style,
177 )
178 .is_err());
179 }
180}