Skip to main content

ogeom_io/
vrml.rs

1//! Reading VRML scenes: the meshes a VRML97 (2.0) or VRML 1.0 file draws,
2//! each placed by the transforms above it and coloured by its material.
3//!
4//! VRML97 is a tree of nodes. Group-like nodes (`Transform`, `Group`,
5//! `Anchor`, `Billboard`, `Collision`, `LOD`'s first level, `Switch`'s
6//! chosen child) are walked, `DEF` names a node and `USE` places it again,
7//! and each `Shape` gives one mesh: an `IndexedFaceSet`'s polygons fanned
8//! into triangles, or a `Box`, `Sphere`, `Cylinder` or `Cone` tessellated.
9//! Prototypes, routes, scripts, sensors and interpolators draw nothing and
10//! are passed over. VRML 1.0 is a state machine instead: `Separator`
11//! saves and restores the state, `Coordinate3`, `Material` and the
12//! transform nodes set it, and each `IndexedFaceSet` draws with it.
13
14use std::collections::HashMap;
15use std::rc::Rc;
16
17use ogeom_core::{OgeomResult, ogeom_bail};
18use ogeom_math::{Point, Vector};
19use ogeom_topo::Triangulation;
20
21use crate::mesh_formats::{ImportedMesh, Placement, normals_from_triangles};
22
23/// Read the meshes a VRML file draws.
24///
25/// # Errors
26///
27/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
28/// header names no VRML version read here, or the text does not parse.
29pub fn read_vrml(text: &str) -> OgeomResult<Vec<ImportedMesh>> {
30    let header = text.lines().next().unwrap_or("").trim();
31    let version_one = if header.starts_with("#VRML V2.0") {
32        false
33    } else if header.starts_with("#VRML V1.0") {
34        true
35    } else {
36        ogeom_bail!(Construction, "this is not a VRML 1.0 or 2.0 file");
37    };
38    let tokens = tokenize(text)?;
39    let mut parser = Parser {
40        tokens,
41        at: 0,
42        defined: HashMap::new(),
43    };
44    let mut roots = Vec::new();
45    while parser.at < parser.tokens.len() {
46        if let Some(node) = parser.statement()? {
47            roots.push(node);
48        }
49    }
50    let mut out = Vec::new();
51    if version_one {
52        let mut state = State::default();
53        for node in &roots {
54            walk_one(node, &mut state, &mut out)?;
55        }
56    } else {
57        for node in &roots {
58            walk_two(node, Placement::IDENTITY, &mut out)?;
59        }
60    }
61    Ok(out)
62}
63
64// --- tokens and nodes ---------------------------------------------------------
65
66#[derive(Debug, Clone, PartialEq)]
67enum Token {
68    Word(String),
69    Number(f64),
70    Text(String),
71    Open,
72    Close,
73    OpenList,
74    CloseList,
75}
76
77fn tokenize(text: &str) -> OgeomResult<Vec<Token>> {
78    let mut out = Vec::new();
79    let mut chars = text.chars().peekable();
80    while let Some(&c) = chars.peek() {
81        match c {
82            '#' => {
83                for c in chars.by_ref() {
84                    if c == '\n' {
85                        break;
86                    }
87                }
88            }
89            '{' | '}' | '[' | ']' => {
90                chars.next();
91                out.push(match c {
92                    '{' => Token::Open,
93                    '}' => Token::Close,
94                    '[' => Token::OpenList,
95                    _ => Token::CloseList,
96                });
97            }
98            '"' => {
99                chars.next();
100                let mut s = String::new();
101                loop {
102                    match chars.next() {
103                        Some('\\') => {
104                            if let Some(e) = chars.next() {
105                                s.push(e);
106                            }
107                        }
108                        Some('"') => break,
109                        Some(c) => s.push(c),
110                        None => ogeom_bail!(Construction, "a VRML string does not close"),
111                    }
112                }
113                out.push(Token::Text(s));
114            }
115            // Bit masks' parentheses and bars separate words and say
116            // nothing a mesh needs.
117            c if c.is_whitespace() || matches!(c, ',' | '(' | ')' | '|') => {
118                chars.next();
119            }
120            _ => {
121                let mut word = String::new();
122                while let Some(&c) = chars.peek() {
123                    if c.is_whitespace()
124                        || matches!(c, ',' | '{' | '}' | '[' | ']' | '"' | '#' | '(' | ')' | '|')
125                    {
126                        break;
127                    }
128                    word.push(c);
129                    chars.next();
130                }
131                let numeric = word
132                    .chars()
133                    .next()
134                    .is_some_and(|c| c.is_ascii_digit() || matches!(c, '-' | '+' | '.'));
135                match (numeric, word.parse::<f64>()) {
136                    (true, Ok(v)) => out.push(Token::Number(v)),
137                    (true, Err(_)) if word.starts_with("0x") || word.starts_with("0X") => {
138                        let v = i64::from_str_radix(&word[2..], 16).unwrap_or(0);
139                        #[allow(clippy::cast_precision_loss)]
140                        out.push(Token::Number(v as f64));
141                    }
142                    _ => out.push(Token::Word(word)),
143                }
144            }
145        }
146    }
147    Ok(out)
148}
149
150/// A field's value: nodes, or a run of plain values.
151#[derive(Debug, Clone)]
152enum Value {
153    Nodes(Vec<Rc<Node>>),
154    Numbers(Vec<f64>),
155    Words(Vec<String>),
156}
157
158#[derive(Debug)]
159struct Node {
160    kind: String,
161    fields: Vec<(String, Value)>,
162    /// A VRML 1.0 group's children, which stand among its fields unnamed.
163    children: Vec<Rc<Node>>,
164}
165
166impl Node {
167    fn field(&self, name: &str) -> Option<&Value> {
168        self.fields.iter().find(|(n, _)| n == name).map(|(_, v)| v)
169    }
170
171    fn numbers(&self, name: &str) -> Option<&[f64]> {
172        match self.field(name) {
173            Some(Value::Numbers(v)) => Some(v),
174            _ => None,
175        }
176    }
177
178    fn number(&self, name: &str, default: f64) -> f64 {
179        self.numbers(name)
180            .and_then(|v| v.first().copied())
181            .unwrap_or(default)
182    }
183
184    fn nodes(&self, name: &str) -> &[Rc<Node>] {
185        match self.field(name) {
186            Some(Value::Nodes(v)) => v,
187            _ => &[],
188        }
189    }
190
191    fn node(&self, name: &str) -> Option<&Rc<Node>> {
192        self.nodes(name).first()
193    }
194
195    fn flag(&self, name: &str, default: bool) -> bool {
196        match self.field(name) {
197            Some(Value::Words(w)) => w.first().map_or(default, |w| w == "TRUE"),
198            _ => default,
199        }
200    }
201}
202
203struct Parser {
204    tokens: Vec<Token>,
205    at: usize,
206    defined: HashMap<String, Rc<Node>>,
207}
208
209impl Parser {
210    fn peek(&self) -> Option<&Token> {
211        self.tokens.get(self.at)
212    }
213
214    fn next(&mut self) -> Option<Token> {
215        let t = self.tokens.get(self.at).cloned();
216        self.at += 1;
217        t
218    }
219
220    fn word(&mut self) -> OgeomResult<String> {
221        match self.next() {
222            Some(Token::Word(w)) => Ok(w),
223            other => ogeom_bail!(Construction, "a VRML name was expected, found {other:?}"),
224        }
225    }
226
227    /// Skip a balanced `{ }` or `[ ]` group starting at the cursor.
228    fn skip_group(&mut self) {
229        let mut depth = 0i32;
230        while let Some(t) = self.next() {
231            match t {
232                Token::Open | Token::OpenList => depth += 1,
233                Token::Close | Token::CloseList => {
234                    depth -= 1;
235                    if depth <= 0 {
236                        return;
237                    }
238                }
239                _ => {}
240            }
241            if depth == 0 {
242                return;
243            }
244        }
245    }
246
247    /// A top-level or child statement: a node, or a prototype or route that
248    /// draws nothing.
249    fn statement(&mut self) -> OgeomResult<Option<Rc<Node>>> {
250        match self.peek() {
251            Some(Token::Word(w)) if w == "PROTO" => {
252                self.next();
253                self.word()?;
254                self.skip_group();
255                self.skip_group();
256                Ok(None)
257            }
258            Some(Token::Word(w)) if w == "EXTERNPROTO" => {
259                self.next();
260                self.word()?;
261                self.skip_group();
262                // The URL list or string.
263                if matches!(self.peek(), Some(Token::OpenList)) {
264                    self.skip_group();
265                } else {
266                    self.next();
267                }
268                Ok(None)
269            }
270            Some(Token::Word(w)) if w == "ROUTE" => {
271                // ROUTE a.out TO b.in
272                self.at += 4;
273                Ok(None)
274            }
275            Some(Token::Close | Token::CloseList) | None => {
276                self.next();
277                Ok(None)
278            }
279            _ => self.node().map(Some),
280        }
281    }
282
283    fn node(&mut self) -> OgeomResult<Rc<Node>> {
284        let first = self.word()?;
285        if first == "USE" {
286            let name = self.word()?;
287            return self.defined.get(&name).cloned().ok_or_else(|| {
288                ogeom_core::ogeom_err!(Construction, "USE of {name} before its DEF")
289            });
290        }
291        if first == "NULL" {
292            return Ok(Rc::new(Node {
293                kind: "NULL".into(),
294                fields: Vec::new(),
295                children: Vec::new(),
296            }));
297        }
298        let (name, kind) = if first == "DEF" {
299            let name = self.word()?;
300            (Some(name), self.word()?)
301        } else {
302            (None, first)
303        };
304        if !matches!(self.next(), Some(Token::Open)) {
305            ogeom_bail!(Construction, "a {kind} node opens with a brace");
306        }
307        let mut fields = Vec::new();
308        let mut children = Vec::new();
309        loop {
310            match self.peek() {
311                Some(Token::Close) => {
312                    self.next();
313                    break;
314                }
315                None => ogeom_bail!(Construction, "a {kind} node does not close"),
316                Some(Token::Word(w)) if w == "ROUTE" || w == "PROTO" || w == "EXTERNPROTO" => {
317                    self.statement()?;
318                }
319                Some(Token::Word(w)) if w == "DEF" || w == "USE" => {
320                    children.push(self.node()?);
321                }
322                Some(Token::Word(_)) => {
323                    let field = self.word()?;
324                    // A VRML 1.0 child node: a name followed by a brace.
325                    if matches!(self.peek(), Some(Token::Open)) {
326                        self.at -= 1;
327                        children.push(self.node()?);
328                        continue;
329                    }
330                    // Interface declarations in a script: `field SFType name value`.
331                    if matches!(field.as_str(), "eventIn" | "eventOut") {
332                        self.word()?;
333                        self.word()?;
334                        continue;
335                    }
336                    if matches!(field.as_str(), "field" | "exposedField") {
337                        self.word()?;
338                        self.word()?;
339                    }
340                    let value = self.value()?;
341                    fields.push((field, value));
342                }
343                _ => {
344                    self.next();
345                }
346            }
347        }
348        let node = Rc::new(Node {
349            kind,
350            fields,
351            children,
352        });
353        if let Some(name) = name {
354            self.defined.insert(name, node.clone());
355        }
356        Ok(node)
357    }
358
359    fn value(&mut self) -> OgeomResult<Value> {
360        match self.peek() {
361            Some(Token::OpenList) => {
362                self.next();
363                let mut nodes = Vec::new();
364                let mut numbers = Vec::new();
365                let mut words = Vec::new();
366                loop {
367                    match self.peek() {
368                        Some(Token::CloseList) => {
369                            self.next();
370                            break;
371                        }
372                        None => ogeom_bail!(Construction, "a VRML list does not close"),
373                        Some(Token::Number(v)) => {
374                            numbers.push(*v);
375                            self.next();
376                        }
377                        Some(Token::Text(t)) => {
378                            words.push(t.clone());
379                            self.next();
380                        }
381                        Some(Token::Word(w)) if w == "TRUE" || w == "FALSE" => {
382                            words.push(w.clone());
383                            self.next();
384                        }
385                        Some(Token::Word(_)) => {
386                            if let Some(n) = self.statement()? {
387                                nodes.push(n);
388                            }
389                        }
390                        _ => {
391                            self.next();
392                        }
393                    }
394                }
395                Ok(if !nodes.is_empty() {
396                    Value::Nodes(nodes)
397                } else if !words.is_empty() {
398                    Value::Words(words)
399                } else {
400                    Value::Numbers(numbers)
401                })
402            }
403            Some(Token::Number(_)) => {
404                let mut numbers = Vec::new();
405                while let Some(Token::Number(v)) = self.peek() {
406                    numbers.push(*v);
407                    self.next();
408                }
409                Ok(Value::Numbers(numbers))
410            }
411            Some(Token::Text(_)) => {
412                let Some(Token::Text(t)) = self.next() else {
413                    unreachable!()
414                };
415                Ok(Value::Words(vec![t]))
416            }
417            Some(Token::Word(w)) if w == "TRUE" || w == "FALSE" => {
418                let w = w.clone();
419                self.next();
420                Ok(Value::Words(vec![w]))
421            }
422            Some(Token::Word(w)) if w == "IS" => {
423                self.next();
424                self.word()?;
425                Ok(Value::Words(Vec::new()))
426            }
427            Some(Token::Word(w))
428                if w.chars().next().is_some_and(|c| c.is_ascii_lowercase())
429                    && !matches!(self.tokens.get(self.at + 1), Some(Token::Open)) =>
430            {
431                // An enumerated value (VRML 1.0 `SIDES`-style words come
432                // upper case; a lower-case word here is the next field).
433                Ok(Value::Words(Vec::new()))
434            }
435            Some(Token::Word(w))
436                if w.chars().all(|c| c.is_ascii_uppercase() || c == '_')
437                    && w != "NULL"
438                    && w != "DEF"
439                    && w != "USE" =>
440            {
441                let w = w.clone();
442                self.next();
443                Ok(Value::Words(vec![w]))
444            }
445            Some(Token::Open) => {
446                // A VRML 1.0 bit mask `( A | B )` never braces; skip what does.
447                self.skip_group();
448                Ok(Value::Words(Vec::new()))
449            }
450            _ => Ok(Value::Nodes(vec![self.node()?])),
451        }
452    }
453}
454
455// --- VRML97 ---------------------------------------------------------------------
456
457fn walk_two(node: &Rc<Node>, placement: Placement, out: &mut Vec<ImportedMesh>) -> OgeomResult<()> {
458    match node.kind.as_str() {
459        "Transform" => {
460            let here = placement.then(transform_of(node));
461            for child in node.nodes("children") {
462                walk_two(child, here, out)?;
463            }
464        }
465        "Group" | "Anchor" | "Billboard" | "Collision" | "StaticGroup" => {
466            for child in node.nodes("children") {
467                walk_two(child, placement, out)?;
468            }
469        }
470        "LOD" => {
471            let levels = if node.nodes("level").is_empty() {
472                node.nodes("children")
473            } else {
474                node.nodes("level")
475            };
476            if let Some(first) = levels.first() {
477                walk_two(first, placement, out)?;
478            }
479        }
480        "Switch" => {
481            let choices = if node.nodes("choice").is_empty() {
482                node.nodes("children")
483            } else {
484                node.nodes("choice")
485            };
486            let which = node.number("whichChoice", -1.0);
487            if which >= 0.0 {
488                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
489                if let Some(chosen) = choices.get(which as usize) {
490                    walk_two(chosen, placement, out)?;
491                }
492            }
493        }
494        "Shape" => {
495            let colour = node
496                .node("appearance")
497                .and_then(|a| a.node("material"))
498                .map(|m| material_colour(m, "diffuseColor"));
499            let Some(geometry) = node.node("geometry") else {
500                return Ok(());
501            };
502            if let Some(mesh) = geometry_mesh(geometry)? {
503                out.push(ImportedMesh {
504                    mesh: placed(mesh, placement),
505                    colour: colour.flatten(),
506                    name: None,
507                });
508            }
509        }
510        _ => {}
511    }
512    Ok(())
513}
514
515/// A VRML97 `Transform`'s map: `T C R SR S -SR -C`.
516fn transform_of(node: &Node) -> Placement {
517    let triple = |name: &str, default: [f64; 3]| -> Vector {
518        let v = node.numbers(name).unwrap_or(&[]);
519        if v.len() >= 3 {
520            Vector::new(v[0], v[1], v[2])
521        } else {
522            Vector::new(default[0], default[1], default[2])
523        }
524    };
525    let rotation = |name: &str| -> Placement {
526        let v = node.numbers(name).unwrap_or(&[]);
527        if v.len() >= 4 {
528            axis_angle(Vector::new(v[0], v[1], v[2]), v[3])
529        } else {
530            Placement::IDENTITY
531        }
532    };
533    let translate = |v: Vector| Placement {
534        translation: v,
535        ..Placement::IDENTITY
536    };
537    let scale = triple("scale", [1.0, 1.0, 1.0]);
538    let scaling = Placement {
539        columns: [
540            Vector::new(scale.x, 0.0, 0.0),
541            Vector::new(0.0, scale.y, 0.0),
542            Vector::new(0.0, 0.0, scale.z),
543        ],
544        translation: Vector::new(0.0, 0.0, 0.0),
545    };
546    let centre = triple("center", [0.0, 0.0, 0.0]);
547    let so = rotation("scaleOrientation");
548    let so_inverse = inverse_rotation(so);
549    translate(triple("translation", [0.0, 0.0, 0.0]))
550        .then(translate(centre))
551        .then(rotation("rotation"))
552        .then(so)
553        .then(scaling)
554        .then(so_inverse)
555        .then(translate(-centre))
556}
557
558fn axis_angle(axis: Vector, angle: f64) -> Placement {
559    let m = axis.magnitude();
560    if m <= 0.0 {
561        return Placement::IDENTITY;
562    }
563    let (x, y, z) = (axis.x / m, axis.y / m, axis.z / m);
564    let (s, c) = angle.sin_cos();
565    let t = 1.0 - c;
566    Placement {
567        columns: [
568            Vector::new(t * x * x + c, t * x * y + s * z, t * x * z - s * y),
569            Vector::new(t * x * y - s * z, t * y * y + c, t * y * z + s * x),
570            Vector::new(t * x * z + s * y, t * y * z - s * x, t * z * z + c),
571        ],
572        translation: Vector::new(0.0, 0.0, 0.0),
573    }
574}
575
576fn inverse_rotation(r: Placement) -> Placement {
577    let [a, b, c] = r.columns;
578    Placement {
579        columns: [
580            Vector::new(a.x, b.x, c.x),
581            Vector::new(a.y, b.y, c.y),
582            Vector::new(a.z, b.z, c.z),
583        ],
584        translation: Vector::new(0.0, 0.0, 0.0),
585    }
586}
587
588fn material_colour(material: &Node, field: &str) -> Option<[f64; 4]> {
589    let c = material.numbers(field)?;
590    if c.len() < 3 {
591        return None;
592    }
593    let transparency = material.number("transparency", 0.0);
594    Some([c[0], c[1], c[2], 1.0 - transparency])
595}
596
597fn geometry_mesh(geometry: &Node) -> OgeomResult<Option<Triangulation>> {
598    let (positions, triangles) = match geometry.kind.as_str() {
599        "IndexedFaceSet" => {
600            let points = geometry
601                .node("coord")
602                .and_then(|c| c.numbers("point"))
603                .unwrap_or(&[]);
604            let index = geometry.numbers("coordIndex").unwrap_or(&[]);
605            faces(points, index, geometry.flag("ccw", true))?
606        }
607        "Box" => {
608            let size = geometry.numbers("size").unwrap_or(&[2.0, 2.0, 2.0]);
609            let s = if size.len() >= 3 {
610                [size[0], size[1], size[2]]
611            } else {
612                [2.0; 3]
613            };
614            cuboid(s)
615        }
616        "Sphere" => ball(geometry.number("radius", 1.0)),
617        "Cylinder" => lathe(
618            geometry.number("radius", 1.0),
619            geometry.number("radius", 1.0),
620            geometry.number("height", 2.0),
621            geometry.flag("bottom", true),
622            geometry.flag("top", true),
623            geometry.flag("side", true),
624        ),
625        "Cone" => lathe(
626            geometry.number("bottomRadius", 1.0),
627            0.0,
628            geometry.number("height", 2.0),
629            geometry.flag("bottom", true),
630            false,
631            geometry.flag("side", true),
632        ),
633        _ => return Ok(None),
634    };
635    if triangles.is_empty() {
636        return Ok(None);
637    }
638    let normals = normals_from_triangles(&positions, &triangles);
639    let parameters = vec![(0.0, 0.0); positions.len()];
640    Ok(Some(Triangulation {
641        positions,
642        normals,
643        parameters,
644        triangles,
645        deflection_met: true,
646    }))
647}
648
649/// Polygons, `-1` apart, fanned into triangles.
650fn faces(points: &[f64], index: &[f64], ccw: bool) -> OgeomResult<(Vec<Point>, Vec<[u32; 3]>)> {
651    let positions: Vec<Point> = points
652        .as_chunks::<3>()
653        .0
654        .iter()
655        .map(|[x, y, z]| Point::new(*x, *y, *z))
656        .collect();
657    let mut triangles = Vec::new();
658    let mut polygon: Vec<u32> = Vec::new();
659    let close = |polygon: &mut Vec<u32>, triangles: &mut Vec<[u32; 3]>| {
660        for k in 1..polygon.len().saturating_sub(1) {
661            let t = [polygon[0], polygon[k], polygon[k + 1]];
662            triangles.push(if ccw { t } else { [t[0], t[2], t[1]] });
663        }
664        polygon.clear();
665    };
666    for &i in index {
667        if i < 0.0 {
668            close(&mut polygon, &mut triangles);
669            continue;
670        }
671        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
672        let i = i as usize;
673        if i >= positions.len() {
674            ogeom_bail!(
675                Construction,
676                "a face names point {i} of {}",
677                positions.len()
678            );
679        }
680        #[allow(clippy::cast_possible_truncation)]
681        polygon.push(i as u32);
682    }
683    close(&mut polygon, &mut triangles);
684    Ok((positions, triangles))
685}
686
687fn cuboid(size: [f64; 3]) -> (Vec<Point>, Vec<[u32; 3]>) {
688    let [x, y, z] = size.map(|s| s * 0.5);
689    let positions: Vec<Point> = (0..8)
690        .map(|i| {
691            Point::new(
692                if i & 1 == 0 { -x } else { x },
693                if i & 2 == 0 { -y } else { y },
694                if i & 4 == 0 { -z } else { z },
695            )
696        })
697        .collect();
698    let quads = [
699        [0, 2, 3, 1],
700        [4, 5, 7, 6],
701        [0, 1, 5, 4],
702        [2, 6, 7, 3],
703        [0, 4, 6, 2],
704        [1, 3, 7, 5],
705    ];
706    let triangles = quads
707        .iter()
708        .flat_map(|q| [[q[0], q[1], q[2]], [q[0], q[2], q[3]]])
709        .collect();
710    (positions, triangles)
711}
712
713const AROUND: u32 = 32;
714
715fn ball(radius: f64) -> (Vec<Point>, Vec<[u32; 3]>) {
716    let rows = AROUND / 2;
717    let mut positions = Vec::new();
718    for i in 0..=rows {
719        let phi = core::f64::consts::PI * f64::from(i) / f64::from(rows);
720        for j in 0..AROUND {
721            let theta = core::f64::consts::TAU * f64::from(j) / f64::from(AROUND);
722            positions.push(Point::new(
723                radius * phi.sin() * theta.sin(),
724                radius * phi.cos(),
725                radius * phi.sin() * theta.cos(),
726            ));
727        }
728    }
729    let mut triangles = Vec::new();
730    for i in 0..rows {
731        for j in 0..AROUND {
732            let k = (j + 1) % AROUND;
733            let (a, b) = (i * AROUND + j, i * AROUND + k);
734            let (c, d) = ((i + 1) * AROUND + j, (i + 1) * AROUND + k);
735            if i > 0 {
736                triangles.push([a, c, b]);
737            }
738            if i + 1 < rows {
739                triangles.push([b, c, d]);
740            }
741        }
742    }
743    (positions, triangles)
744}
745
746/// A drum or cone about `y`, centred, with the caps asked for.
747fn lathe(
748    bottom: f64,
749    top: f64,
750    height: f64,
751    bottom_cap: bool,
752    top_cap: bool,
753    side: bool,
754) -> (Vec<Point>, Vec<[u32; 3]>) {
755    let h = height * 0.5;
756    let mut positions = Vec::new();
757    let ring = |positions: &mut Vec<Point>, r: f64, y: f64| -> u32 {
758        #[allow(clippy::cast_possible_truncation)]
759        let start = positions.len() as u32;
760        for j in 0..AROUND {
761            let theta = core::f64::consts::TAU * f64::from(j) / f64::from(AROUND);
762            positions.push(Point::new(r * theta.sin(), y, r * theta.cos()));
763        }
764        start
765    };
766    let low = ring(&mut positions, bottom, -h);
767    let high = ring(&mut positions, top, h);
768    #[allow(clippy::cast_possible_truncation)]
769    let centres = positions.len() as u32;
770    positions.push(Point::new(0.0, -h, 0.0));
771    positions.push(Point::new(0.0, h, 0.0));
772    let mut triangles = Vec::new();
773    for j in 0..AROUND {
774        let k = (j + 1) % AROUND;
775        if side {
776            triangles.push([low + j, low + k, high + j]);
777            if top > 0.0 {
778                triangles.push([low + k, high + k, high + j]);
779            }
780        }
781        if bottom_cap && bottom > 0.0 {
782            triangles.push([centres, low + k, low + j]);
783        }
784        if top_cap && top > 0.0 {
785            triangles.push([centres + 1, high + j, high + k]);
786        }
787    }
788    (positions, triangles)
789}
790
791fn placed(mut mesh: Triangulation, placement: Placement) -> Triangulation {
792    for p in &mut mesh.positions {
793        *p = placement.point(*p);
794    }
795    let [a, b, c] = placement.columns;
796    if a.cross(b).dot(c) < 0.0 {
797        // A mirroring map turns the winding inside out; turn it back.
798        for t in &mut mesh.triangles {
799            t.swap(1, 2);
800        }
801    }
802    mesh.normals = normals_from_triangles(&mesh.positions, &mesh.triangles);
803    mesh
804}
805
806// --- VRML 1.0 --------------------------------------------------------------------
807
808#[derive(Clone)]
809struct State {
810    placement: Placement,
811    points: Rc<Vec<f64>>,
812    colour: Option<[f64; 4]>,
813    ccw: bool,
814}
815
816impl Default for State {
817    fn default() -> Self {
818        Self {
819            placement: Placement::IDENTITY,
820            points: Rc::new(Vec::new()),
821            colour: None,
822            ccw: true,
823        }
824    }
825}
826
827fn walk_one(node: &Rc<Node>, state: &mut State, out: &mut Vec<ImportedMesh>) -> OgeomResult<()> {
828    let triple = |name: &str, default: f64| -> Vector {
829        let v = node.numbers(name).unwrap_or(&[]);
830        if v.len() >= 3 {
831            Vector::new(v[0], v[1], v[2])
832        } else {
833            Vector::new(default, default, default)
834        }
835    };
836    match node.kind.as_str() {
837        "Separator" | "TransformSeparator" => {
838            let mut inner = state.clone();
839            for child in &node.children {
840                walk_one(child, &mut inner, out)?;
841            }
842            if node.kind == "TransformSeparator" {
843                let placement = state.placement;
844                *state = inner;
845                state.placement = placement;
846            }
847        }
848        "Group" => {
849            for child in &node.children {
850                walk_one(child, state, out)?;
851            }
852        }
853        "Switch" => {
854            let which = node.number("whichChild", -1.0);
855            if which >= 0.0 {
856                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
857                if let Some(chosen) = node.children.get(which as usize) {
858                    walk_one(chosen, state, out)?;
859                }
860            } else if which <= -3.0 {
861                for child in &node.children {
862                    walk_one(child, state, out)?;
863                }
864            }
865        }
866        "Coordinate3" => {
867            state.points = Rc::new(node.numbers("point").unwrap_or(&[]).to_vec());
868        }
869        "Material" => {
870            state.colour = material_colour(node, "diffuseColor");
871        }
872        "ShapeHints" => {
873            if let Some(Value::Words(w)) = node.field("vertexOrdering") {
874                state.ccw = w.first().is_none_or(|w| w != "CLOCKWISE");
875            }
876        }
877        "Translation" => {
878            state.placement = state.placement.then(Placement {
879                translation: triple("translation", 0.0),
880                ..Placement::IDENTITY
881            });
882        }
883        "Rotation" => {
884            let v = node.numbers("rotation").unwrap_or(&[]);
885            if v.len() >= 4 {
886                state.placement = state
887                    .placement
888                    .then(axis_angle(Vector::new(v[0], v[1], v[2]), v[3]));
889            }
890        }
891        "Scale" => {
892            let s = triple("scaleFactor", 1.0);
893            state.placement = state.placement.then(Placement {
894                columns: [
895                    Vector::new(s.x, 0.0, 0.0),
896                    Vector::new(0.0, s.y, 0.0),
897                    Vector::new(0.0, 0.0, s.z),
898                ],
899                translation: Vector::new(0.0, 0.0, 0.0),
900            });
901        }
902        "Transform" => {
903            let mut renamed: Vec<(String, Value)> = node.fields.clone();
904            for (name, _) in &mut renamed {
905                if name == "scaleFactor" {
906                    *name = "scale".into();
907                }
908            }
909            let as_two = Node {
910                kind: "Transform".into(),
911                fields: renamed,
912                children: Vec::new(),
913            };
914            state.placement = state.placement.then(transform_of(&as_two));
915        }
916        "MatrixTransform" => {
917            let m = node.numbers("matrix").unwrap_or(&[]);
918            if m.len() >= 16 {
919                // Row vectors: each row is where an axis goes.
920                state.placement = state.placement.then(Placement {
921                    columns: [
922                        Vector::new(m[0], m[1], m[2]),
923                        Vector::new(m[4], m[5], m[6]),
924                        Vector::new(m[8], m[9], m[10]),
925                    ],
926                    translation: Vector::new(m[12], m[13], m[14]),
927                });
928            }
929        }
930        "IndexedFaceSet" => {
931            let index = node.numbers("coordIndex").unwrap_or(&[]);
932            let (positions, triangles) = faces(&state.points, index, state.ccw)?;
933            // Only the points the faces use.
934            let mut used: HashMap<u32, u32> = HashMap::new();
935            let mut kept = Vec::new();
936            let triangles: Vec<[u32; 3]> = triangles
937                .iter()
938                .map(|t| {
939                    t.map(|i| {
940                        *used.entry(i).or_insert_with(|| {
941                            kept.push(positions[i as usize]);
942                            #[allow(clippy::cast_possible_truncation)]
943                            let at = (kept.len() - 1) as u32;
944                            at
945                        })
946                    })
947                })
948                .collect();
949            if !triangles.is_empty() {
950                let normals = normals_from_triangles(&kept, &triangles);
951                let parameters = vec![(0.0, 0.0); kept.len()];
952                let mesh = Triangulation {
953                    positions: kept,
954                    normals,
955                    parameters,
956                    triangles,
957                    deflection_met: true,
958                };
959                out.push(ImportedMesh {
960                    mesh: placed(mesh, state.placement),
961                    colour: state.colour,
962                    name: None,
963                });
964            }
965        }
966        "Cube" | "Sphere" | "Cylinder" | "Cone" => {
967            let (positions, triangles) = match node.kind.as_str() {
968                "Cube" => cuboid([
969                    node.number("width", 2.0),
970                    node.number("height", 2.0),
971                    node.number("depth", 2.0),
972                ]),
973                "Sphere" => ball(node.number("radius", 1.0)),
974                "Cylinder" => lathe(
975                    node.number("radius", 1.0),
976                    node.number("radius", 1.0),
977                    node.number("height", 2.0),
978                    true,
979                    true,
980                    true,
981                ),
982                _ => lathe(
983                    node.number("bottomRadius", 1.0),
984                    0.0,
985                    node.number("height", 2.0),
986                    true,
987                    false,
988                    true,
989                ),
990            };
991            let normals = normals_from_triangles(&positions, &triangles);
992            let parameters = vec![(0.0, 0.0); positions.len()];
993            out.push(ImportedMesh {
994                mesh: placed(
995                    Triangulation {
996                        positions,
997                        normals,
998                        parameters,
999                        triangles,
1000                        deflection_met: true,
1001                    },
1002                    state.placement,
1003                ),
1004                colour: state.colour,
1005                name: None,
1006            });
1007        }
1008        _ => {}
1009    }
1010    Ok(())
1011}