1use sim_kernel::{Error, Expr, Result, Symbol};
10use sim_value::access;
11
12pub use sim_value::build::{float, int, list, map, sym, text, uint, vector};
13
14use crate::model::node;
15
16pub const RESERVED_DATA_KEYS: &[&str] = &["kind"];
18
19pub fn stack(dir: &str, children: Vec<Expr>) -> Expr {
21 node(
22 "stack",
23 vec![("dir", sym(dir)), ("children", list(children))],
24 )
25}
26
27pub fn box_(role: &str, children: Vec<Expr>) -> Expr {
29 node(
30 "box",
31 vec![("role", sym(role)), ("children", list(children))],
32 )
33}
34
35pub fn badge(status: &str, label: &str) -> Expr {
37 node(
38 "badge",
39 vec![("status", sym(status)), ("label", text(label))],
40 )
41}
42
43pub fn badge_cluster(badges: Vec<Expr>) -> Expr {
45 node("badge-cluster", vec![("badges", list(badges))])
46}
47
48pub fn text_node(content: impl Into<String>) -> Expr {
50 node("text", vec![("text", text(content.into()))])
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum AnchorSpace {
56 Head,
58 World,
60 Screen,
62 Body,
64 Device,
66}
67
68impl AnchorSpace {
69 pub fn as_name(self) -> &'static str {
71 match self {
72 Self::Head => "head",
73 Self::World => "world",
74 Self::Screen => "screen",
75 Self::Body => "body",
76 Self::Device => "device",
77 }
78 }
79
80 pub fn to_expr(self) -> Expr {
82 sym(self.as_name())
83 }
84
85 pub fn from_expr(expr: &Expr) -> Result<Self> {
87 let Expr::Symbol(symbol) = expr else {
88 return Err(Error::Eval(
89 "scene/anchor space must be a symbol".to_owned(),
90 ));
91 };
92 Self::from_symbol(symbol)
93 }
94
95 fn from_symbol(symbol: &Symbol) -> Result<Self> {
96 if symbol.namespace.is_some() {
97 return Err(Error::Eval(format!(
98 "scene/anchor space must be unqualified, got {symbol}"
99 )));
100 }
101 match symbol.name.as_ref() {
102 "head" => Ok(Self::Head),
103 "world" => Ok(Self::World),
104 "screen" => Ok(Self::Screen),
105 "body" => Ok(Self::Body),
106 "device" => Ok(Self::Device),
107 other => Err(Error::Eval(format!("unknown scene/anchor space {other}"))),
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct Anchor {
115 pub space: AnchorSpace,
117 pub target: String,
119}
120
121impl Anchor {
122 pub fn new(space: AnchorSpace, target: impl Into<String>) -> Self {
124 Self {
125 space,
126 target: target.into(),
127 }
128 }
129
130 pub fn to_expr(&self) -> Expr {
132 node(
133 "anchor",
134 vec![
135 ("space", self.space.to_expr()),
136 ("target", text(self.target.clone())),
137 ],
138 )
139 }
140
141 pub fn from_expr(expr: &Expr) -> Result<Self> {
143 expect_kind(expr, "anchor")?;
144 Ok(Self {
145 space: AnchorSpace::from_expr(access::required(expr, "space", "scene/anchor")?)?,
146 target: access::required_str(expr, "target", "scene/anchor")?.to_owned(),
147 })
148 }
149}
150
151#[derive(Clone, Debug, PartialEq)]
153pub struct Transform3 {
154 pub translate_m: [f64; 3],
156 pub rotate_xyzw: [f64; 4],
158 pub scale: [f64; 3],
160}
161
162impl Transform3 {
163 pub fn new(translate_m: [f64; 3], rotate_xyzw: [f64; 4], scale: [f64; 3]) -> Self {
165 Self {
166 translate_m,
167 rotate_xyzw,
168 scale,
169 }
170 }
171
172 pub fn identity() -> Self {
174 Self::new([0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0])
175 }
176
177 pub fn to_expr(&self) -> Expr {
179 map(vec![
180 ("translate-m", f64_vector(self.translate_m)),
181 ("rotate-xyzw", f64_vector(self.rotate_xyzw)),
182 ("scale", f64_vector(self.scale)),
183 ])
184 }
185
186 pub fn from_expr(expr: &Expr) -> Result<Self> {
188 let transform = Self {
189 translate_m: read_f64_vector(expr, "translate-m", "scene/Transform3")?,
190 rotate_xyzw: read_f64_vector(expr, "rotate-xyzw", "scene/Transform3")?,
191 scale: read_f64_vector(expr, "scale", "scene/Transform3")?,
192 };
193 if transform.rotate_xyzw.iter().all(|value| *value == 0.0) {
194 return Err(Error::Eval(
195 "scene/Transform3 rotation must not be the zero quaternion".to_owned(),
196 ));
197 }
198 if transform.scale.contains(&0.0) {
199 return Err(Error::Eval(
200 "scene/Transform3 scale entries must be nonzero".to_owned(),
201 ));
202 }
203 Ok(transform)
204 }
205}
206
207pub fn spatial(children: Vec<Expr>) -> Expr {
209 node("spatial", vec![("children", list(children))])
210}
211
212pub fn stereo(left_eye: Expr, right_eye: Expr, predict_ms: u64) -> Expr {
214 node(
215 "stereo",
216 vec![
217 ("left-eye", left_eye),
218 ("right-eye", right_eye),
219 ("predict-ms", uint(predict_ms)),
220 ],
221 )
222}
223
224pub fn anchor(space: AnchorSpace, target: impl Into<String>) -> Expr {
226 Anchor::new(space, target).to_expr()
227}
228
229pub fn panel(id: impl Into<String>, body: Expr, anchor: Anchor, transform: Transform3) -> Expr {
231 node(
232 "panel",
233 vec![
234 ("id", text(id.into())),
235 ("body", body),
236 ("anchor", anchor.to_expr()),
237 ("transform", transform.to_expr()),
238 ],
239 )
240}
241
242pub fn gaze_cursor(anchor: Anchor, transform: Transform3) -> Expr {
244 node(
245 "gaze-cursor",
246 vec![
247 ("anchor", anchor.to_expr()),
248 ("transform", transform.to_expr()),
249 ],
250 )
251}
252
253pub fn hand_ray(hand: &str, anchor: Anchor, transform: Transform3) -> Expr {
255 node(
256 "hand-ray",
257 vec![
258 ("hand", sym(hand)),
259 ("anchor", anchor.to_expr()),
260 ("transform", transform.to_expr()),
261 ],
262 )
263}
264
265pub fn world_plane(
267 id: impl Into<String>,
268 anchor: Anchor,
269 transform: Transform3,
270 size_m: [f64; 2],
271) -> Expr {
272 node(
273 "world-plane",
274 vec![
275 ("id", text(id.into())),
276 ("anchor", anchor.to_expr()),
277 ("transform", transform.to_expr()),
278 ("size-m", f64_vector(size_m)),
279 ],
280 )
281}
282
283pub fn data_map(entries: Vec<(&str, Expr)>) -> Expr {
286 debug_assert!(
287 entries
288 .iter()
289 .all(|(key, _)| !RESERVED_DATA_KEYS.contains(key)),
290 "data_map: a plain data map must not use a reserved scene-node key (e.g. 'kind'); \
291 rename the field"
292 );
293 map(entries)
294}
295
296fn expect_kind(expr: &Expr, expected: &str) -> Result<()> {
297 match crate::model::node_kind(expr) {
298 Some(kind)
299 if kind.namespace.as_deref() == Some(crate::kinds::SCENE_NAMESPACE)
300 && kind.name.as_ref() == expected =>
301 {
302 Ok(())
303 }
304 _ => Err(Error::Eval(format!("expected scene/{expected} node"))),
305 }
306}
307
308fn f64_vector<const N: usize>(values: [f64; N]) -> Expr {
309 vector(values.into_iter().map(float).collect())
310}
311
312fn read_f64_vector<const N: usize>(expr: &Expr, name: &str, context: &str) -> Result<[f64; N]> {
313 let Expr::Vector(items) = access::required(expr, name, context)? else {
314 return Err(Error::Eval(format!(
315 "{context} field {name} is not a vector"
316 )));
317 };
318 if items.len() != N {
319 return Err(Error::Eval(format!(
320 "{context} field {name} must contain {N} numbers"
321 )));
322 }
323 let mut out = [0.0; N];
324 for (index, item) in items.iter().enumerate() {
325 let value = access::as_f64(item)
326 .ok_or_else(|| Error::Eval(format!("{context} field {name}[{index}] is not f64")))?;
327 if !value.is_finite() {
328 return Err(Error::Eval(format!(
329 "{context} field {name}[{index}] is not finite"
330 )));
331 }
332 out[index] = value;
333 }
334 Ok(out)
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn scene_shape_helpers_validate() {
343 let scene = stack(
344 "column",
345 vec![box_(
346 "summary",
347 vec![text_node("hi"), badge_cluster(vec![badge("ok", "done")])],
348 )],
349 );
350 crate::model::validate_scene(&scene).expect("helper scenes validate");
351 }
352
353 #[test]
354 fn data_map_allows_non_reserved_keys() {
355 let value = data_map(vec![("style", sym("line")), ("at", int(3))]);
356 assert!(matches!(value, Expr::Map(_)));
357 }
358
359 #[test]
360 #[should_panic(expected = "reserved scene-node key")]
361 fn data_map_rejects_a_reserved_key_in_debug() {
362 let _ = data_map(vec![("kind", sym("line"))]);
363 }
364}