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