1use 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
23pub 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#[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 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#[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 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 depth: usize,
210}
211
212const 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 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 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 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 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 if matches!(self.peek(), Some(Token::Open)) {
344 self.at -= 1;
345 children.push(self.node()?);
346 continue;
347 }
348 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 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 self.skip_group();
466 Ok(Value::Words(Vec::new()))
467 }
468 _ => Ok(Value::Nodes(vec![self.node()?])),
469 }
470 }
471}
472
473fn 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
533fn 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
667fn 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
764fn 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 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#[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 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 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}