1#![allow(dead_code)]
10
11use std::collections::{HashMap, HashSet};
12use std::io::Write;
13
14pub fn base64_encode_buffer(data: &[u8]) -> String {
20 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
21 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
22 for chunk in data.chunks(3) {
23 let b0 = chunk[0] as usize;
24 let b1 = if chunk.len() > 1 {
25 chunk[1] as usize
26 } else {
27 0
28 };
29 let b2 = if chunk.len() > 2 {
30 chunk[2] as usize
31 } else {
32 0
33 };
34 out.push(TABLE[b0 >> 2] as char);
35 out.push(TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
36 if chunk.len() > 1 {
37 out.push(TABLE[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
38 } else {
39 out.push('=');
40 }
41 if chunk.len() > 2 {
42 out.push(TABLE[b2 & 0x3f] as char);
43 } else {
44 out.push('=');
45 }
46 }
47 out
48}
49
50pub fn write_gltf_accessor<W: Write>(
52 w: &mut W,
53 buffer_view: usize,
54 component_type: u32,
55 count: usize,
56 type_str: &str,
57 trailing_comma: bool,
58) -> std::io::Result<()> {
59 let comma = if trailing_comma { "," } else { "" };
60 writeln!(
61 w,
62 r#" {{"bufferView":{buffer_view},"componentType":{component_type},"count":{count},"type":"{type_str}"}}{comma}"#
63 )
64}
65
66pub fn nastran_field8(v: f64) -> String {
68 let s = format!("{:>8.4e}", v);
70 if s.len() > 8 {
71 format!("{:>8.3e}", v)
72 } else {
73 s
74 }
75}
76
77pub fn gmsh_element_type_str(n_nodes: usize) -> &'static str {
80 match n_nodes {
81 2 => "Line",
82 3 => "Triangle",
83 4 => "Quad",
84 10 => "Tet10",
85 _ => "Unknown",
86 }
87}
88
89pub struct GltfMeshWriter {
95 pub vertices: Vec<[f64; 3]>,
97 pub normals: Vec<[f64; 3]>,
99 pub uvs: Vec<[f64; 2]>,
101 pub indices: Vec<[u32; 3]>,
103 pub material_name: String,
105}
106
107impl GltfMeshWriter {
108 pub fn new(
110 vertices: Vec<[f64; 3]>,
111 normals: Vec<[f64; 3]>,
112 uvs: Vec<[f64; 2]>,
113 indices: Vec<[u32; 3]>,
114 ) -> Self {
115 GltfMeshWriter {
116 vertices,
117 normals,
118 uvs,
119 indices,
120 material_name: "default".to_string(),
121 }
122 }
123
124 pub fn positions_f32_bytes(&self) -> Vec<u8> {
126 let mut buf = Vec::with_capacity(self.vertices.len() * 12);
127 for &[x, y, z] in &self.vertices {
128 buf.extend_from_slice(&(x as f32).to_le_bytes());
129 buf.extend_from_slice(&(y as f32).to_le_bytes());
130 buf.extend_from_slice(&(z as f32).to_le_bytes());
131 }
132 buf
133 }
134
135 pub fn indices_u32_bytes(&self) -> Vec<u8> {
137 let mut buf = Vec::with_capacity(self.indices.len() * 12);
138 for &[a, b, c] in &self.indices {
139 buf.extend_from_slice(&a.to_le_bytes());
140 buf.extend_from_slice(&b.to_le_bytes());
141 buf.extend_from_slice(&c.to_le_bytes());
142 }
143 buf
144 }
145
146 pub fn write_gltf_json<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
148 let pos_bytes = self.positions_f32_bytes();
149 let idx_bytes = self.indices_u32_bytes();
150 let mut binary = pos_bytes.clone();
151 binary.extend_from_slice(&idx_bytes);
152 let b64 = base64_encode_buffer(&binary);
153 let n_verts = self.vertices.len();
154 let n_tris = self.indices.len();
155 let pos_len = pos_bytes.len();
156 let idx_len = idx_bytes.len();
157 writeln!(w, "{{")?;
158 writeln!(w, r#" "asset": {{"version": "2.0"}},"#)?;
159 writeln!(w, r#" "scene": 0,"#)?;
160 writeln!(w, r#" "scenes": [{{"nodes": [0]}}],"#)?;
161 writeln!(w, r#" "nodes": [{{"mesh": 0}}],"#)?;
162 writeln!(
163 w,
164 r#" "meshes": [{{"name": "mesh","primitives": [{{"attributes": {{"POSITION": 0}},"indices": 1}}]}}],"#
165 )?;
166 writeln!(w, r#" "accessors": ["#)?;
167 write_gltf_accessor(w, 0, 5126, n_verts, "VEC3", true)?;
168 write_gltf_accessor(w, 1, 5125, n_tris * 3, "SCALAR", false)?;
169 writeln!(w, r#" ],"#)?;
170 writeln!(w, r#" "bufferViews": ["#)?;
171 writeln!(
172 w,
173 r#" {{"buffer": 0, "byteOffset": 0, "byteLength": {pos_len}}},"#
174 )?;
175 writeln!(
176 w,
177 r#" {{"buffer": 0, "byteOffset": {pos_len}, "byteLength": {idx_len}}}"#
178 )?;
179 writeln!(w, r#" ],"#)?;
180 writeln!(
181 w,
182 r#" "buffers": [{{"byteLength": {total}, "uri": "data:application/octet-stream;base64,{b64}"}}]"#,
183 total = binary.len()
184 )?;
185 writeln!(w, "}}")?;
186 Ok(())
187 }
188}
189
190pub struct GltfMeshReader {
198 pub vertices: Vec<[f64; 3]>,
200 pub indices: Vec<[u32; 3]>,
202 pub node_names: Vec<String>,
204}
205
206impl GltfMeshReader {
207 pub fn parse_json(json: &str) -> Self {
209 let mut node_names = Vec::new();
210 for line in json.lines() {
212 let line = line.trim();
213 if line.contains("\"name\"")
214 && let Some(start) = line.find("\"name\":")
215 {
216 let rest = &line[start + 7..].trim_start_matches([' ', '"'].as_ref());
217 let end = rest.find('"').unwrap_or(rest.len());
218 node_names.push(rest[..end].to_string());
219 }
220 }
221 GltfMeshReader {
222 vertices: Vec::new(),
223 indices: Vec::new(),
224 node_names,
225 }
226 }
227}
228
229pub struct UsdMeshWriter {
235 pub vertices: Vec<[f64; 3]>,
237 pub face_vertex_counts: Vec<u32>,
239 pub face_vertex_indices: Vec<u32>,
241 pub prim_path: String,
243}
244
245impl UsdMeshWriter {
246 pub fn from_triangles(
248 vertices: Vec<[f64; 3]>,
249 triangles: &[[u32; 3]],
250 prim_path: &str,
251 ) -> Self {
252 let face_vertex_counts = vec![3; triangles.len()];
253 let face_vertex_indices: Vec<u32> =
254 triangles.iter().flat_map(|t| t.iter().copied()).collect();
255 UsdMeshWriter {
256 vertices,
257 face_vertex_counts,
258 face_vertex_indices,
259 prim_path: prim_path.to_string(),
260 }
261 }
262
263 pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
265 writeln!(w, "#usda 1.0")?;
266 writeln!(
267 w,
268 r#"def Mesh "{name}" {{"#,
269 name = self.prim_path.trim_start_matches('/')
270 )?;
271 write!(w, " point3f[] points = [")?;
273 for (i, &[x, y, z]) in self.vertices.iter().enumerate() {
274 if i > 0 {
275 write!(w, ", ")?;
276 }
277 write!(w, "({x}, {y}, {z})")?;
278 }
279 writeln!(w, "]")?;
280 write!(w, " int[] faceVertexCounts = [")?;
282 for (i, &c) in self.face_vertex_counts.iter().enumerate() {
283 if i > 0 {
284 write!(w, ", ")?;
285 }
286 write!(w, "{c}")?;
287 }
288 writeln!(w, "]")?;
289 write!(w, " int[] faceVertexIndices = [")?;
291 for (i, &idx) in self.face_vertex_indices.iter().enumerate() {
292 if i > 0 {
293 write!(w, ", ")?;
294 }
295 write!(w, "{idx}")?;
296 }
297 writeln!(w, "]")?;
298 writeln!(w, "}}")?;
299 Ok(())
300 }
301}
302
303pub struct MshWriter {
309 pub nodes: Vec<[f64; 3]>,
311 pub elements: Vec<Vec<usize>>,
313 pub element_tags: Vec<i64>,
315 pub version: u32,
317}
318
319impl MshWriter {
320 pub fn new(version: u32) -> Self {
322 MshWriter {
323 nodes: Vec::new(),
324 elements: Vec::new(),
325 element_tags: Vec::new(),
326 version,
327 }
328 }
329
330 pub fn add_node(&mut self, x: f64, y: f64, z: f64) {
332 self.nodes.push([x, y, z]);
333 }
334
335 pub fn add_triangle(&mut self, a: usize, b: usize, c: usize, tag: i64) {
337 self.elements.push(vec![a, b, c]);
338 self.element_tags.push(tag);
339 }
340
341 pub fn write_v2<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
343 writeln!(w, "$MeshFormat")?;
344 writeln!(w, "2.2 0 8")?;
345 writeln!(w, "$EndMeshFormat")?;
346 writeln!(w, "$Nodes")?;
347 writeln!(w, "{}", self.nodes.len())?;
348 for (i, &[x, y, z]) in self.nodes.iter().enumerate() {
349 writeln!(w, "{} {} {} {}", i + 1, x, y, z)?;
350 }
351 writeln!(w, "$EndNodes")?;
352 writeln!(w, "$Elements")?;
353 writeln!(w, "{}", self.elements.len())?;
354 for (i, elem) in self.elements.iter().enumerate() {
355 let tag = self.element_tags.get(i).copied().unwrap_or(0);
356 let elem_type = match elem.len() {
357 2 => 1, 3 => 2, 4 => 3, _ => 2,
361 };
362 write!(w, "{} {} 2 {} {}", i + 1, elem_type, tag, tag)?;
363 for &n in elem {
364 write!(w, " {n}")?;
365 }
366 writeln!(w)?;
367 }
368 writeln!(w, "$EndElements")?;
369 Ok(())
370 }
371}
372
373pub struct MshReader {
379 pub nodes: Vec<[f64; 3]>,
381 pub elements: Vec<Vec<usize>>,
383 pub element_tags: Vec<i64>,
385}
386
387impl MshReader {
388 pub fn parse(s: &str) -> Self {
390 let mut nodes = Vec::new();
391 let mut elements = Vec::new();
392 let mut element_tags = Vec::new();
393 let mut in_nodes = false;
394 let mut in_elements = false;
395 let mut node_count = 0usize;
396 let mut elem_count = 0usize;
397 let mut node_idx = 0usize;
398 let mut elem_idx = 0usize;
399
400 for line in s.lines() {
401 let line = line.trim();
402 if line == "$Nodes" {
403 in_nodes = true;
404 node_idx = 0;
405 continue;
406 }
407 if line == "$EndNodes" {
408 in_nodes = false;
409 continue;
410 }
411 if line == "$Elements" {
412 in_elements = true;
413 elem_idx = 0;
414 continue;
415 }
416 if line == "$EndElements" {
417 in_elements = false;
418 continue;
419 }
420 if in_nodes {
421 if node_idx == 0 {
422 node_count = line.parse().unwrap_or(0);
423 nodes.reserve(node_count);
424 node_idx += 1;
425 continue;
426 }
427 let vals: Vec<f64> = line
428 .split_whitespace()
429 .skip(1)
430 .filter_map(|v| v.parse().ok())
431 .collect();
432 if vals.len() >= 3 {
433 nodes.push([vals[0], vals[1], vals[2]]);
434 }
435 }
436 if in_elements {
437 if elem_idx == 0 {
438 elem_count = line.parse().unwrap_or(0);
439 elements.reserve(elem_count);
440 element_tags.reserve(elem_count);
441 elem_idx += 1;
442 continue;
443 }
444 let vals: Vec<i64> = line
445 .split_whitespace()
446 .filter_map(|v| v.parse().ok())
447 .collect();
448 if vals.len() >= 5 {
449 let n_tags = vals[2] as usize;
450 let tag = vals[3];
451 let node_start = 3 + n_tags;
452 let node_indices: Vec<usize> = vals[node_start..]
453 .iter()
454 .map(|&v| (v - 1) as usize)
455 .collect();
456 elements.push(node_indices);
457 element_tags.push(tag);
458 }
459 }
460 }
461 let _ = node_count;
462 let _ = elem_count;
463 MshReader {
464 nodes,
465 elements,
466 element_tags,
467 }
468 }
469}
470
471pub struct AbaqusInpWriter {
477 pub part_name: String,
479 pub nodes: Vec<[f64; 3]>,
481 pub elements: Vec<Vec<usize>>,
483 pub element_type: String,
485 pub node_sets: HashMap<String, Vec<usize>>,
487 pub boundary_conditions: Vec<(String, usize, usize, f64)>,
489}
490
491impl AbaqusInpWriter {
492 pub fn new(part_name: &str, element_type: &str) -> Self {
494 AbaqusInpWriter {
495 part_name: part_name.to_string(),
496 nodes: Vec::new(),
497 elements: Vec::new(),
498 element_type: element_type.to_string(),
499 node_sets: HashMap::new(),
500 boundary_conditions: Vec::new(),
501 }
502 }
503
504 pub fn add_node(&mut self, x: f64, y: f64, z: f64) {
506 self.nodes.push([x, y, z]);
507 }
508
509 pub fn add_element(&mut self, nodes: Vec<usize>) {
511 self.elements.push(nodes);
512 }
513
514 pub fn add_node_set(&mut self, name: &str, indices: Vec<usize>) {
516 self.node_sets.insert(name.to_string(), indices);
517 }
518
519 pub fn add_boundary(&mut self, nset: &str, dof_min: usize, dof_max: usize, value: f64) {
521 self.boundary_conditions
522 .push((nset.to_string(), dof_min, dof_max, value));
523 }
524
525 pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
527 writeln!(w, "*PART, NAME={}", self.part_name)?;
528 writeln!(w, "*NODE")?;
529 for (i, &[x, y, z]) in self.nodes.iter().enumerate() {
530 writeln!(w, "{}, {}, {}, {}", i + 1, x, y, z)?;
531 }
532 writeln!(w, "*ELEMENT, TYPE={}", self.element_type)?;
533 for (i, elem) in self.elements.iter().enumerate() {
534 write!(w, "{}", i + 1)?;
535 for &n in elem {
536 write!(w, ", {n}")?;
537 }
538 writeln!(w)?;
539 }
540 for (name, indices) in &self.node_sets {
541 writeln!(w, "*NSET, NSET={name}")?;
542 for chunk in indices.chunks(8) {
543 let s: Vec<String> = chunk.iter().map(|n| n.to_string()).collect();
544 writeln!(w, "{}", s.join(", "))?;
545 }
546 }
547 if !self.boundary_conditions.is_empty() {
548 writeln!(w, "*BOUNDARY")?;
549 for (nset, d1, d2, val) in &self.boundary_conditions {
550 writeln!(w, "{nset}, {d1}, {d2}, {val}")?;
551 }
552 }
553 writeln!(w, "*END PART")?;
554 Ok(())
555 }
556}
557
558pub struct AbaqusInpReader {
564 pub nodes: Vec<[f64; 3]>,
566 pub elements: Vec<Vec<usize>>,
568 pub element_type: String,
570 pub node_sets: HashMap<String, Vec<usize>>,
572}
573
574impl AbaqusInpReader {
575 pub fn parse(s: &str) -> Self {
577 let mut nodes = Vec::new();
578 let mut elements = Vec::new();
579 let mut element_type = String::new();
580 let mut node_sets: HashMap<String, Vec<usize>> = HashMap::new();
581 let mut mode = "";
582 let mut cur_nset = String::new();
583
584 for line in s.lines() {
585 let line = line.trim();
586 if line.starts_with('*') {
587 let kw = line.to_uppercase();
588 if kw.starts_with("*NODE") && !kw.starts_with("*NSET") {
589 mode = "node";
590 } else if kw.starts_with("*ELEMENT") {
591 mode = "element";
592 if let Some(tp) = line.split("TYPE=").nth(1) {
593 element_type = tp.split(',').next().unwrap_or("").trim().to_string();
594 }
595 } else if kw.starts_with("*NSET") {
596 mode = "nset";
597 if let Some(name) = line.split("NSET=").nth(1) {
598 cur_nset = name.split(',').next().unwrap_or("").trim().to_string();
599 node_sets.entry(cur_nset.clone()).or_default();
600 }
601 } else if kw.starts_with("*BOUNDARY") {
602 mode = "boundary";
603 } else {
604 mode = "";
605 }
606 continue;
607 }
608 match mode {
609 "node" => {
610 let vals: Vec<f64> = line
611 .split(',')
612 .filter_map(|v| v.trim().parse().ok())
613 .collect();
614 if vals.len() >= 4 {
615 nodes.push([vals[1], vals[2], vals[3]]);
616 }
617 }
618 "element" => {
619 let vals: Vec<usize> = line
620 .split(',')
621 .filter_map(|v| v.trim().parse().ok())
622 .collect();
623 if vals.len() >= 2 {
624 elements.push(vals[1..].to_vec());
625 }
626 }
627 "nset" => {
628 let indices: Vec<usize> = line
629 .split(',')
630 .filter_map(|v| v.trim().parse().ok())
631 .collect();
632 if let Some(set) = node_sets.get_mut(&cur_nset) {
633 set.extend(indices);
634 }
635 }
636 _ => {}
637 }
638 }
639 AbaqusInpReader {
640 nodes,
641 elements,
642 element_type,
643 node_sets,
644 }
645 }
646}
647
648#[derive(Debug, Clone)]
654pub struct NastranGrid {
655 pub id: u64,
657 pub cp: u64,
659 pub xyz: [f64; 3],
661}
662
663#[derive(Debug, Clone)]
665pub struct NastranElement {
666 pub eid: u64,
668 pub pid: u64,
670 pub nodes: Vec<u64>,
672 pub elem_type: String,
674}
675
676pub struct NastranBdf {
678 pub grids: Vec<NastranGrid>,
680 pub elements: Vec<NastranElement>,
682}
683
684impl NastranBdf {
685 pub fn new() -> Self {
687 NastranBdf {
688 grids: Vec::new(),
689 elements: Vec::new(),
690 }
691 }
692
693 pub fn add_grid(&mut self, id: u64, xyz: [f64; 3]) {
695 self.grids.push(NastranGrid { id, cp: 0, xyz });
696 }
697
698 pub fn add_element(&mut self, eid: u64, pid: u64, nodes: Vec<u64>, elem_type: &str) {
700 self.elements.push(NastranElement {
701 eid,
702 pid,
703 nodes,
704 elem_type: elem_type.to_string(),
705 });
706 }
707
708 pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
710 writeln!(w, "BEGIN BULK")?;
711 for g in &self.grids {
712 writeln!(
713 w,
714 "GRID {:8}{:8}{:8}{:8}{:8}",
715 g.id,
716 g.cp,
717 nastran_field8(g.xyz[0]),
718 nastran_field8(g.xyz[1]),
719 nastran_field8(g.xyz[2]),
720 )?;
721 }
722 for e in &self.elements {
723 write!(w, "{:<8}{:8}{:8}", e.elem_type, e.eid, e.pid)?;
724 for &n in &e.nodes {
725 write!(w, "{:8}", n)?;
726 }
727 writeln!(w)?;
728 }
729 writeln!(w, "ENDDATA")?;
730 Ok(())
731 }
732
733 pub fn parse(s: &str) -> Self {
735 let mut bdf = NastranBdf::new();
736 for line in s.lines() {
737 let line = if line.len() > 8 { line } else { continue };
738 let kw = &line[..8].trim().to_uppercase();
739 if kw.starts_with("GRID") {
740 if let Some(g) = Self::parse_grid(line) {
741 bdf.grids.push(g);
742 }
743 } else if (kw.starts_with("CTETRA") || kw.starts_with("CHEXA"))
744 && let Some(e) = Self::parse_element(line)
745 {
746 bdf.elements.push(e);
747 }
748 }
749 bdf
750 }
751
752 fn parse_grid(line: &str) -> Option<NastranGrid> {
753 let fields: Vec<&str> = (0..9)
754 .map(|i| {
755 let s = i * 8;
756 let e = (s + 8).min(line.len());
757 if s < line.len() {
758 line[s..e].trim()
759 } else {
760 ""
761 }
762 })
763 .collect();
764 let id: u64 = fields[1].parse().ok()?;
765 let x: f64 = fields[3].parse().ok()?;
766 let y: f64 = fields[4].parse().ok()?;
767 let z: f64 = fields[5].parse().ok()?;
768 Some(NastranGrid {
769 id,
770 cp: 0,
771 xyz: [x, y, z],
772 })
773 }
774
775 fn parse_element(line: &str) -> Option<NastranElement> {
776 let kw = line[..8].trim().to_uppercase();
777 let fields: Vec<&str> = (0..12)
778 .map(|i| {
779 let s = i * 8;
780 let e = (s + 8).min(line.len());
781 if s < line.len() {
782 line[s..e].trim()
783 } else {
784 ""
785 }
786 })
787 .collect();
788 let eid: u64 = fields[1].parse().ok()?;
789 let pid: u64 = fields[2].parse().ok()?;
790 let nodes: Vec<u64> = fields[3..].iter().filter_map(|f| f.parse().ok()).collect();
791 Some(NastranElement {
792 eid,
793 pid,
794 nodes,
795 elem_type: kw,
796 })
797 }
798}
799
800impl Default for NastranBdf {
801 fn default() -> Self {
802 Self::new()
803 }
804}
805
806pub struct MeshConverter;
812
813impl MeshConverter {
814 pub fn compact_nodes(
817 nodes: &[[f64; 3]],
818 elements: &[Vec<usize>],
819 ) -> (Vec<[f64; 3]>, Vec<Vec<usize>>) {
820 let mut used: HashSet<usize> = HashSet::new();
821 for elem in elements {
822 for &n in elem {
823 used.insert(n);
824 }
825 }
826 let mut old_to_new: HashMap<usize, usize> = HashMap::new();
827 let mut new_nodes = Vec::new();
828 let mut sorted: Vec<usize> = used.into_iter().collect();
829 sorted.sort_unstable();
830 for old_idx in sorted {
831 old_to_new.insert(old_idx, new_nodes.len());
832 if old_idx < nodes.len() {
833 new_nodes.push(nodes[old_idx]);
834 }
835 }
836 let new_elements: Vec<Vec<usize>> = elements
837 .iter()
838 .map(|e| {
839 e.iter()
840 .filter_map(|&n| old_to_new.get(&n).copied())
841 .collect()
842 })
843 .collect();
844 (new_nodes, new_elements)
845 }
846
847 pub fn deduplicate_vertices(
849 positions: &[[f64; 3]],
850 indices: &[[u32; 3]],
851 tolerance: f64,
852 ) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
853 let mut unique: Vec<[f64; 3]> = Vec::new();
854 let mut remap: Vec<u32> = Vec::with_capacity(positions.len());
855 for &p in positions {
856 let mut found = None;
857 for (j, &u) in unique.iter().enumerate() {
858 let dx = p[0] - u[0];
859 let dy = p[1] - u[1];
860 let dz = p[2] - u[2];
861 if (dx * dx + dy * dy + dz * dz).sqrt() < tolerance {
862 found = Some(j as u32);
863 break;
864 }
865 }
866 if let Some(j) = found {
867 remap.push(j);
868 } else {
869 remap.push(unique.len() as u32);
870 unique.push(p);
871 }
872 }
873 let new_indices: Vec<[u32; 3]> = indices
874 .iter()
875 .map(|&[a, b, c]| [remap[a as usize], remap[b as usize], remap[c as usize]])
876 .collect();
877 (unique, new_indices)
878 }
879
880 pub fn flip_winding(indices: &[[u32; 3]]) -> Vec<[u32; 3]> {
882 indices.iter().map(|&[a, b, c]| [a, c, b]).collect()
883 }
884}
885
886#[derive(Debug, Clone)]
892pub struct MeshQuality {
893 pub min_aspect_ratio: f64,
895 pub max_aspect_ratio: f64,
897 pub min_area: f64,
899 pub degenerate_count: usize,
901 pub inverted_count: usize,
903}
904
905pub struct MeshValidator;
907
908impl MeshValidator {
909 pub fn validate(
911 vertices: &[[f64; 3]],
912 triangles: &[[u32; 3]],
913 area_epsilon: f64,
914 ) -> MeshQuality {
915 let mut min_ar = f64::MAX;
916 let mut max_ar = f64::MIN;
917 let mut min_area = f64::MAX;
918 let mut deg_count = 0;
919 let mut inv_count = 0;
920
921 for &[a, b, c] in triangles {
922 let va = vertices[a as usize];
923 let vb = vertices[b as usize];
924 let vc = vertices[c as usize];
925 let ab = sub3(vb, va);
926 let ac = sub3(vc, va);
927 let bc = sub3(vc, vb);
928 let cross = cross3(ab, ac);
929 let area = len3(cross) * 0.5;
930 let ar = triangle_aspect_ratio(len3(ab), len3(ac), len3(bc));
931 min_ar = min_ar.min(ar);
932 max_ar = max_ar.max(ar);
933 min_area = min_area.min(area);
934 if area < area_epsilon {
935 deg_count += 1;
936 }
937 if cross[1] < 0.0 {
939 inv_count += 1;
940 }
941 }
942
943 if triangles.is_empty() {
944 min_ar = 0.0;
945 max_ar = 0.0;
946 min_area = 0.0;
947 }
948
949 MeshQuality {
950 min_aspect_ratio: min_ar,
951 max_aspect_ratio: max_ar,
952 min_area,
953 degenerate_count: deg_count,
954 inverted_count: inv_count,
955 }
956 }
957
958 pub fn check_bounds(vertices: &[[f64; 3]], triangles: &[[u32; 3]]) -> bool {
960 let n = vertices.len() as u32;
961 triangles.iter().all(|&[a, b, c]| a < n && b < n && c < n)
962 }
963}
964
965fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
966 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
967}
968
969fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
970 [
971 a[1] * b[2] - a[2] * b[1],
972 a[2] * b[0] - a[0] * b[2],
973 a[0] * b[1] - a[1] * b[0],
974 ]
975}
976
977fn len3(a: [f64; 3]) -> f64 {
978 (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
979}
980
981fn triangle_aspect_ratio(a: f64, b: f64, c: f64) -> f64 {
983 let s = (a + b + c) / 2.0;
984 let area = (s * (s - a) * (s - b) * (s - c)).max(0.0).sqrt();
985 if area < 1e-14 {
986 return f64::MAX;
987 }
988 let longest = a.max(b).max(c);
989 longest * (a + b + c) / (4.0 * 3_f64.sqrt() * area)
990}
991
992#[cfg(test)]
997mod tests {
998 use super::*;
999
1000 #[test]
1003 fn base64_empty() {
1004 assert_eq!(base64_encode_buffer(&[]), "");
1005 }
1006
1007 #[test]
1008 fn base64_man() {
1009 let result = base64_encode_buffer(b"Man");
1011 assert_eq!(result, "TWFu");
1012 }
1013
1014 #[test]
1015 fn base64_one_byte_padded() {
1016 let result = base64_encode_buffer(b"M");
1017 assert_eq!(result.len(), 4);
1018 assert!(result.ends_with("=="));
1019 }
1020
1021 #[test]
1022 fn base64_two_bytes_padded() {
1023 let result = base64_encode_buffer(b"Ma");
1024 assert_eq!(result.len(), 4);
1025 assert!(result.ends_with('='));
1026 }
1027
1028 #[test]
1031 fn nastran_field8_length() {
1032 let s = nastran_field8(1.23456789);
1033 assert!(s.len() <= 8, "length={}", s.len());
1034 }
1035
1036 #[test]
1037 fn nastran_field8_zero() {
1038 let s = nastran_field8(0.0);
1039 assert!(s.len() <= 8);
1040 }
1041
1042 #[test]
1045 fn gmsh_triangle() {
1046 assert_eq!(gmsh_element_type_str(3), "Triangle");
1047 }
1048
1049 #[test]
1050 fn gmsh_line() {
1051 assert_eq!(gmsh_element_type_str(2), "Line");
1052 }
1053
1054 #[test]
1057 fn gltf_writer_produces_json() {
1058 let verts = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1059 let indices = vec![[0u32, 1, 2]];
1060 let w = GltfMeshWriter::new(verts, vec![], vec![], indices);
1061 let mut buf = Vec::new();
1062 w.write_gltf_json(&mut buf).unwrap();
1063 let s = String::from_utf8(buf).unwrap();
1064 assert!(s.contains("asset"), "s={s}");
1065 assert!(s.contains("accessors"));
1066 }
1067
1068 #[test]
1069 fn gltf_positions_bytes_length() {
1070 let verts = vec![[0.0f64; 3]; 4];
1071 let w = GltfMeshWriter::new(verts, vec![], vec![], vec![]);
1072 assert_eq!(w.positions_f32_bytes().len(), 48); }
1074
1075 #[test]
1076 fn gltf_indices_bytes_length() {
1077 let indices = vec![[0u32, 1, 2], [1u32, 2, 3]];
1078 let w = GltfMeshWriter::new(vec![], vec![], vec![], indices);
1079 assert_eq!(w.indices_u32_bytes().len(), 24); }
1081
1082 #[test]
1085 fn gltf_reader_parse_names() {
1086 let json = r#"{"meshes": [{"name": "MyMesh"}]}"#;
1087 let r = GltfMeshReader::parse_json(json);
1088 assert!(
1089 r.node_names.iter().any(|n| n.contains("MyMesh")),
1090 "names={:?}",
1091 r.node_names
1092 );
1093 }
1094
1095 #[test]
1098 fn usd_writer_contains_def_mesh() {
1099 let verts = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1100 let w = UsdMeshWriter::from_triangles(verts, &[[0, 1, 2]], "/World/Mesh");
1101 let mut buf = Vec::new();
1102 w.write(&mut buf).unwrap();
1103 let s = String::from_utf8(buf).unwrap();
1104 assert!(s.contains("def Mesh"), "s={s}");
1105 assert!(s.contains("points"));
1106 }
1107
1108 #[test]
1109 fn usd_writer_face_vertex_indices() {
1110 let verts = vec![[0.0f64; 3]; 3];
1111 let w = UsdMeshWriter::from_triangles(verts, &[[0, 1, 2]], "/Mesh");
1112 assert_eq!(w.face_vertex_indices.len(), 3);
1113 assert_eq!(w.face_vertex_counts.len(), 1);
1114 }
1115
1116 #[test]
1119 fn msh_writer_v2_roundtrip() {
1120 let mut mw = MshWriter::new(2);
1121 mw.add_node(0.0, 0.0, 0.0);
1122 mw.add_node(1.0, 0.0, 0.0);
1123 mw.add_node(0.0, 1.0, 0.0);
1124 mw.add_triangle(1, 2, 3, 1);
1125 let mut buf = Vec::new();
1126 mw.write_v2(&mut buf).unwrap();
1127 let s = String::from_utf8(buf).unwrap();
1128 assert!(s.contains("$MeshFormat"));
1129 assert!(s.contains("$Nodes"));
1130 assert!(s.contains("$Elements"));
1131 let r = MshReader::parse(&s);
1132 assert_eq!(r.nodes.len(), 3, "parsed nodes: {:?}", r.nodes);
1133 assert_eq!(r.elements.len(), 1);
1134 }
1135
1136 #[test]
1137 fn msh_reader_node_coords() {
1138 let msh = "$MeshFormat\n2.2 0 8\n$EndMeshFormat\n$Nodes\n2\n1 1.0 2.0 3.0\n2 4.0 5.0 6.0\n$EndNodes\n$Elements\n0\n$EndElements\n";
1139 let r = MshReader::parse(msh);
1140 assert_eq!(r.nodes.len(), 2);
1141 assert!((r.nodes[0][0] - 1.0).abs() < 1e-9);
1142 }
1143
1144 #[test]
1147 fn abaqus_writer_produces_part() {
1148 let mut w = AbaqusInpWriter::new("PART-1", "C3D4");
1149 w.add_node(0.0, 0.0, 0.0);
1150 w.add_node(1.0, 0.0, 0.0);
1151 w.add_node(0.0, 1.0, 0.0);
1152 w.add_node(0.0, 0.0, 1.0);
1153 w.add_element(vec![1, 2, 3, 4]);
1154 let mut buf = Vec::new();
1155 w.write(&mut buf).unwrap();
1156 let s = String::from_utf8(buf).unwrap();
1157 assert!(s.contains("*PART"), "s={s}");
1158 assert!(s.contains("*NODE"));
1159 assert!(s.contains("*ELEMENT"));
1160 }
1161
1162 #[test]
1163 fn abaqus_reader_parse() {
1164 let inp = "*PART, NAME=P1\n*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n3, 0.0, 1.0, 0.0\n*ELEMENT, TYPE=C3D4\n1, 1, 2, 3\n*END PART\n";
1165 let r = AbaqusInpReader::parse(inp);
1166 assert_eq!(r.nodes.len(), 3, "nodes={:?}", r.nodes);
1167 assert_eq!(r.elements.len(), 1);
1168 }
1169
1170 #[test]
1171 fn abaqus_boundary_written() {
1172 let mut w = AbaqusInpWriter::new("P", "C3D4");
1173 w.add_node_set("FIXED", vec![1, 2]);
1174 w.add_boundary("FIXED", 1, 3, 0.0);
1175 let mut buf = Vec::new();
1176 w.write(&mut buf).unwrap();
1177 let s = String::from_utf8(buf).unwrap();
1178 assert!(s.contains("*BOUNDARY"), "s={s}");
1179 }
1180
1181 #[test]
1184 fn nastran_write_and_parse() {
1185 let mut bdf = NastranBdf::new();
1186 bdf.add_grid(1, [0.0, 0.0, 0.0]);
1187 bdf.add_grid(2, [1.0, 0.0, 0.0]);
1188 bdf.add_element(1, 1, vec![1, 2, 3, 4], "CTETRA");
1189 let mut buf = Vec::new();
1190 bdf.write(&mut buf).unwrap();
1191 let s = String::from_utf8(buf).unwrap();
1192 assert!(s.contains("BEGIN BULK"), "s={s}");
1193 assert!(s.contains("GRID"));
1194 assert!(s.contains("ENDDATA"));
1195 }
1196
1197 #[test]
1198 fn nastran_parse_grids() {
1199 let bdf_str = "BEGIN BULK\nGRID 1 0 1.00 2.00 3.00\nENDDATA\n";
1200 let bdf = NastranBdf::parse(bdf_str);
1201 let _ = bdf.grids.len();
1203 }
1204
1205 #[test]
1208 fn compact_nodes_removes_unused() {
1209 let nodes = vec![[0.0f64; 3]; 5];
1210 let elements = vec![vec![0, 2, 4]];
1211 let (new_nodes, new_elements) = MeshConverter::compact_nodes(&nodes, &elements);
1212 assert_eq!(new_nodes.len(), 3);
1213 assert_eq!(new_elements[0].len(), 3);
1214 }
1215
1216 #[test]
1217 fn deduplicate_identical_vertices() {
1218 let positions = vec![[0.0f64, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1219 let indices = vec![[0u32, 1, 2]];
1220 let (uniq, _new_idx) = MeshConverter::deduplicate_vertices(&positions, &indices, 1e-6);
1221 assert_eq!(uniq.len(), 2);
1222 }
1223
1224 #[test]
1225 fn flip_winding_reverses_order() {
1226 let indices = vec![[0u32, 1, 2]];
1227 let flipped = MeshConverter::flip_winding(&indices);
1228 assert_eq!(flipped[0], [0, 2, 1]);
1229 }
1230
1231 #[test]
1234 fn validator_equilateral_good_aspect_ratio() {
1235 let sq3 = 3_f64.sqrt();
1236 let verts = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, sq3 / 2.0, 0.0]];
1237 let tris = vec![[0u32, 1, 2]];
1238 let q = MeshValidator::validate(&verts, &tris, 1e-10);
1239 assert!(q.min_aspect_ratio < 1.1, "ar={}", q.min_aspect_ratio);
1240 }
1241
1242 #[test]
1243 fn validator_degenerate_triangle() {
1244 let verts = vec![[0.0f64; 3]; 3]; let tris = vec![[0u32, 1, 2]];
1246 let q = MeshValidator::validate(&verts, &tris, 1e-10);
1247 assert_eq!(q.degenerate_count, 1);
1248 }
1249
1250 #[test]
1251 fn validator_check_bounds_ok() {
1252 let verts = vec![[0.0f64; 3]; 3];
1253 let tris = vec![[0u32, 1, 2]];
1254 assert!(MeshValidator::check_bounds(&verts, &tris));
1255 }
1256
1257 #[test]
1258 fn validator_check_bounds_fail() {
1259 let verts = vec![[0.0f64; 3]; 2];
1260 let tris = vec![[0u32, 1, 5]];
1261 assert!(!MeshValidator::check_bounds(&verts, &tris));
1262 }
1263
1264 #[test]
1267 fn write_gltf_accessor_correct() {
1268 let mut buf = Vec::new();
1269 write_gltf_accessor(&mut buf, 0, 5126, 100, "VEC3", false).unwrap();
1270 let s = String::from_utf8(buf).unwrap();
1271 assert!(s.contains("VEC3"));
1272 assert!(s.contains("100"));
1273 }
1274
1275 #[test]
1278 fn aspect_ratio_equilateral_is_one() {
1279 let ar = triangle_aspect_ratio(1.0, 1.0, 1.0);
1280 assert!((ar - 1.0).abs() < 1e-6, "ar={ar}");
1281 }
1282
1283 #[test]
1284 fn aspect_ratio_degenerate_is_max() {
1285 let ar = triangle_aspect_ratio(1.0, 1.0, 2.0);
1286 assert!(ar > 1.0 || ar == f64::MAX);
1287 }
1288}