1use 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
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: 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#[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 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#[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 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 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 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 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 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 if matches!(self.peek(), Some(Token::Open)) {
326 self.at -= 1;
327 children.push(self.node()?);
328 continue;
329 }
330 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 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 self.skip_group();
448 Ok(Value::Words(Vec::new()))
449 }
450 _ => Ok(Value::Nodes(vec![self.node()?])),
451 }
452 }
453}
454
455fn 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
515fn 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
649fn 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
746fn 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 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#[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 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 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}