1use std::io::Write;
43
44use energyml_geometry::{Attribute, AttributeScope, Placement, SceneObject, Topology};
45
46use crate::error::{Error, Result};
47use crate::registry::{Exporter, Options};
48
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
51pub enum Flavour {
52 Legacy,
54 PolyData,
56 UnstructuredGrid,
58}
59
60pub struct VtkExporter {
62 flavour: Flavour,
63 version: &'static str,
64}
65
66impl VtkExporter {
67 pub fn legacy() -> Self {
69 Self {
70 flavour: Flavour::Legacy,
71 version: "3.0",
75 }
76 }
77
78 pub fn poly_data() -> Self {
80 Self {
81 flavour: Flavour::PolyData,
82 version: "1.0",
83 }
84 }
85
86 pub fn unstructured_grid() -> Self {
88 Self {
89 flavour: Flavour::UnstructuredGrid,
90 version: "1.0",
91 }
92 }
93
94 pub fn with_version(mut self, version: &'static str) -> Self {
101 self.version = version;
102 self
103 }
104
105 pub fn flavour(&self) -> Flavour {
107 self.flavour
108 }
109}
110
111impl Exporter for VtkExporter {
112 fn name(&self) -> &'static str {
113 match self.flavour {
114 Flavour::Legacy => "vtk",
115 Flavour::PolyData => "vtp",
116 Flavour::UnstructuredGrid => "vtu",
117 }
118 }
119
120 fn extension(&self) -> &'static str {
121 self.name()
122 }
123
124 fn mime(&self) -> &'static str {
125 match self.flavour {
126 Flavour::Legacy => "application/octet-stream",
127 _ => "application/xml",
128 }
129 }
130
131 fn describe(&self) -> &'static str {
132 match self.flavour {
133 Flavour::Legacy => "VTK legacy ASCII, DATASET POLYDATA",
134 Flavour::PolyData => "VTK XML PolyData (.vtp), surfaces and lines",
135 Flavour::UnstructuredGrid => "VTK XML UnstructuredGrid (.vtu), the general container",
136 }
137 }
138
139 fn placement(&self) -> Placement {
140 Placement::Metres
141 }
142
143 fn handles(&self, object: &SceneObject) -> bool {
144 !object.geometry.is_empty()
145 }
146
147 fn write(&self, object: &SceneObject, out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
148 if object.geometry.is_empty() {
149 return Err(Error::unsuitable(
150 "vtk",
151 format!("`{}`, which has no points", object.title),
152 ));
153 }
154 let object = crate::registry::prepare(object, options)?;
161 let object = object.as_ref();
162
163 let placement = options.placement.unwrap_or_else(|| self.placement());
164 let points = options.points(object, placement)?;
165 let cells = cells_of(object);
166
167 match self.flavour {
168 Flavour::Legacy => self.legacy_document(object, &points, &cells, options, out),
169 _ => self.xml_document(object, &points, &cells, options, out),
170 }
171 }
172
173 fn write_all(&self, objects: &[SceneObject], out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
178 let writable: Vec<&SceneObject> = objects.iter().filter(|o| self.handles(o)).collect();
179 match writable.len() {
180 0 => Ok(()),
181 1 => self.write(writable[0], out, options),
182 n => Err(Error::unsuitable(
183 "vtk",
184 format!(
185 "{n} objects in one file - a VTK file holds one dataset; \
186 write to a directory to get one file each"
187 ),
188 )),
189 }
190 }
191}
192
193struct Cells {
195 triangles: Vec<[u32; 3]>,
197 polygons: Vec<Vec<u32>>,
200 quads: Vec<[u32; 4]>,
207 lines: Vec<Vec<u32>>,
209 vertices: Vec<u32>,
211 hexahedra: Vec<[u32; 8]>,
213 polyhedra: Vec<Vec<Vec<u32>>>,
219}
220
221impl Cells {
222 fn count(&self) -> usize {
227 self.vertices.len()
228 + self.lines.len()
229 + self.triangles.len()
230 + self.quads.len()
231 + self.polygons.len()
232 + self.hexahedra.len()
233 + self.polyhedra.len()
234 }
235}
236
237const HEXAHEDRON_FACES: [[usize; 4]; 6] = [
243 [0, 3, 2, 1],
244 [4, 5, 6, 7],
245 [0, 1, 5, 4],
246 [1, 2, 6, 5],
247 [2, 3, 7, 6],
248 [3, 0, 4, 7],
249];
250
251fn cells_of(object: &SceneObject) -> Cells {
252 let (_, triangles) = object.geometry.merged();
253 let mut lines = Vec::new();
254 let mut quads = Vec::new();
255 let mut polygons: Vec<Vec<u32>> = Vec::new();
256 let mut vertices = Vec::new();
257 let mut hexahedra = Vec::new();
258 let mut polyhedra: Vec<Vec<Vec<u32>>> = Vec::new();
259 let mut base = 0u32;
260 for patch in &object.geometry.patches {
261 match &patch.topology {
262 Topology::Segments(pairs) => {
265 lines.extend(pairs.iter().map(|pair| vec![pair[0] + base, pair[1] + base]));
266 }
267 Topology::Polylines(runs) => {
268 let mut start = base;
269 for (rank, run) in runs.iter().enumerate() {
270 if *run >= 2 {
271 let mut line: Vec<_> = (0..*run).map(|i| start + i).collect();
272 if patch.closed.get(rank) == Some(&true) {
275 line.push(start);
276 }
277 lines.push(line);
278 }
279 start += run;
280 }
281 }
282 Topology::Quads(quadrilaterals) => {
286 quads.extend(quadrilaterals.iter().map(|quad| {
287 let mut shifted = *quad;
288 for corner in &mut shifted {
289 *corner += base;
290 }
291 shifted
292 }));
293 }
294 Topology::Polygons(rings) => {
295 polygons.extend(
296 rings
297 .iter()
298 .map(|ring| ring.iter().map(|node| node + base).collect()),
299 );
300 }
301 Topology::Points | Topology::Grid { .. } => {
305 vertices.extend(base..base + patch.points.len() as u32);
306 }
307 Topology::Hexahedra { cells, .. } => {
308 hexahedra.extend(cells.iter().map(|cell| {
309 let mut shifted = *cell;
310 for corner in &mut shifted {
311 *corner += base;
312 }
313 shifted
314 }));
315 }
316 Topology::Polyhedra(cells) => {
319 polyhedra.extend((0..cells.cell_count()).map(|cell| {
320 cells
321 .faces_outward(cell)
322 .map(|face| face.iter().map(|node| node + base).collect())
323 .collect()
324 }));
325 }
326 Topology::Triangles(_) => {}
327 }
328 base += patch.points.len() as u32;
329 }
330 Cells {
331 triangles,
332 quads,
333 polygons,
334 lines,
335 vertices,
336 hexahedra,
337 polyhedra,
338 }
339}
340
341impl VtkExporter {
342 fn legacy_document(
343 &self,
344 object: &SceneObject,
345 points: &[[f64; 3]],
346 cells: &Cells,
347 options: &Options<'_>,
348 out: &mut dyn Write,
349 ) -> Result<()> {
350 writeln!(out, "# vtk DataFile Version {}", self.version)?;
351 let title = format!("{} [{}]", one_line(&object.title), object.uuid);
354 writeln!(out, "{}", &title[..title.len().min(255)])?;
355 writeln!(out, "ASCII")?;
356 let volumetric = !cells.hexahedra.is_empty() || !cells.polyhedra.is_empty();
360 writeln!(
361 out,
362 "DATASET {}",
363 if volumetric {
364 "UNSTRUCTURED_GRID"
365 } else {
366 "POLYDATA"
367 }
368 )?;
369
370 writeln!(out, "POINTS {} double", points.len())?;
371 for point in points {
372 writeln!(out, "{} {} {}", point[0], point[1], point[2])?;
373 }
374 if volumetric {
375 let count = cells.hexahedra.len()
377 + cells.polyhedra.len()
378 + cells.triangles.len()
379 + cells.quads.len()
380 + cells.polygons.len()
381 + cells.lines.len();
382 let size: usize = cells.hexahedra.len() * 9
383 + cells
384 .polyhedra
385 .iter()
386 .map(|c| legacy_polyhedron_len(c) + 1)
387 .sum::<usize>()
388 + cells.triangles.len() * 4
389 + cells.quads.len() * 5
390 + cells.polygons.iter().map(|p| p.len() + 1).sum::<usize>()
391 + cells.lines.iter().map(|l| l.len() + 1).sum::<usize>();
392 writeln!(out, "CELLS {count} {size}")?;
393 for cell in &cells.hexahedra {
394 let corners: Vec<String> = cell.iter().map(|c| c.to_string()).collect();
395 writeln!(out, "8 {}", corners.join(" "))?;
396 }
397 for cell in &cells.polyhedra {
401 let mut stream = vec![cell.len().to_string()];
402 for face in cell {
403 stream.push(face.len().to_string());
404 stream.extend(face.iter().map(|node| node.to_string()));
405 }
406 writeln!(out, "{} {}", legacy_polyhedron_len(cell), stream.join(" "))?;
407 }
408 for triangle in &cells.triangles {
409 writeln!(out, "3 {} {} {}", triangle[0], triangle[1], triangle[2])?;
410 }
411 for quad in &cells.quads {
412 writeln!(out, "4 {} {} {} {}", quad[0], quad[1], quad[2], quad[3])?;
413 }
414 for ring in &cells.polygons {
415 let nodes: Vec<String> = ring.iter().map(|n| n.to_string()).collect();
416 writeln!(out, "{} {}", ring.len(), nodes.join(" "))?;
417 }
418 for line in &cells.lines {
419 let indices: Vec<String> = line.iter().map(|i| i.to_string()).collect();
420 writeln!(out, "{} {}", line.len(), indices.join(" "))?;
421 }
422 writeln!(out, "CELL_TYPES {count}")?;
423 for _ in &cells.hexahedra {
424 writeln!(out, "12")?; }
426 for _ in &cells.polyhedra {
427 writeln!(out, "42")?; }
429 for _ in &cells.triangles {
430 writeln!(out, "5")?; }
432 for _ in &cells.quads {
433 writeln!(out, "9")?; }
435 for _ in &cells.polygons {
436 writeln!(out, "7")?; }
438 for _ in &cells.lines {
439 writeln!(out, "4")?; }
441 }
442 if !volumetric
443 && (!cells.triangles.is_empty() || !cells.quads.is_empty() || !cells.polygons.is_empty())
444 {
445 let size: usize = cells.triangles.len() * 4
446 + cells.quads.len() * 5
447 + cells.polygons.iter().map(|p| p.len() + 1).sum::<usize>();
448 let count = cells.triangles.len() + cells.quads.len() + cells.polygons.len();
449 writeln!(out, "POLYGONS {count} {size}")?;
450 for triangle in &cells.triangles {
451 writeln!(out, "3 {} {} {}", triangle[0], triangle[1], triangle[2])?;
452 }
453 for quad in &cells.quads {
454 writeln!(out, "4 {} {} {} {}", quad[0], quad[1], quad[2], quad[3])?;
455 }
456 for ring in &cells.polygons {
457 let nodes: Vec<String> = ring.iter().map(|n| n.to_string()).collect();
458 writeln!(out, "{} {}", ring.len(), nodes.join(" "))?;
459 }
460 }
461 if !volumetric && !cells.lines.is_empty() {
462 let size: usize = cells.lines.iter().map(|l| l.len() + 1).sum();
463 writeln!(out, "LINES {} {size}", cells.lines.len())?;
464 for line in &cells.lines {
465 let indices: Vec<String> = line.iter().map(|i| i.to_string()).collect();
466 writeln!(out, "{} {}", line.len(), indices.join(" "))?;
467 }
468 }
469 if !cells.vertices.is_empty() {
470 writeln!(
471 out,
472 "VERTICES {} {}",
473 cells.vertices.len(),
474 cells.vertices.len() * 2
475 )?;
476 for vertex in &cells.vertices {
477 writeln!(out, "1 {vertex}")?;
478 }
479 }
480
481 let pairs = self.pairs(object, options);
482 if !pairs.is_empty() {
483 writeln!(out, "FIELD FieldData {}", pairs.len())?;
484 for (key, value) in &pairs {
485 writeln!(out, "{} 1 1 string", sanitise(key))?;
488 writeln!(out, "{}", one_line(value))?;
489 }
490 }
491
492 let cell_count = cells.count();
497 for (scope, count) in [
498 (AttributeScope::Points, points.len()),
499 (AttributeScope::Cells, cell_count),
500 ] {
501 let attributes: Vec<&Attribute> = object
502 .attributes
503 .iter()
504 .filter(|attribute| attribute.scope == scope)
505 .collect();
506 if attributes.is_empty() || count == 0 {
507 continue;
508 }
509 writeln!(
510 out,
511 "{} {count}",
512 match scope {
513 AttributeScope::Points => "POINT_DATA",
514 AttributeScope::Cells => "CELL_DATA",
515 }
516 )?;
517 for attribute in attributes {
518 let table = if attribute.lut.is_empty() {
525 "default".to_string()
526 } else {
527 format!("{}_colors", sanitise(&attribute.name))
528 };
529 writeln!(
530 out,
531 "SCALARS {} double {}",
532 sanitise(&attribute.name),
533 attribute.components.max(1)
534 )?;
535 writeln!(out, "LOOKUP_TABLE {table}")?;
536 for chunk in attribute.values.chunks(attribute.components.max(1) as usize) {
537 let numbers: Vec<String> = chunk.iter().map(number).collect();
538 writeln!(out, "{}", numbers.join(" "))?;
539 }
540 if !attribute.lut.is_empty() {
541 writeln!(out, "LOOKUP_TABLE {table} {}", attribute.lut.len())?;
545 for [r, g, b, a] in &attribute.lut {
546 writeln!(out, "{r} {g} {b} {a}")?;
547 }
548 }
549 }
550 }
551 Ok(())
552 }
553
554 fn xml_document(
555 &self,
556 object: &SceneObject,
557 points: &[[f64; 3]],
558 cells: &Cells,
559 options: &Options<'_>,
560 out: &mut dyn Write,
561 ) -> Result<()> {
562 let container = match self.flavour {
563 Flavour::UnstructuredGrid => "UnstructuredGrid",
564 _ => "PolyData",
565 };
566 writeln!(out, "<?xml version=\"1.0\"?>")?;
567 writeln!(
568 out,
569 "<VTKFile type=\"{container}\" version=\"{}\" byte_order=\"LittleEndian\">",
570 self.version
571 )?;
572
573 let pairs = self.pairs(object, options);
576 if !pairs.is_empty() {
577 writeln!(out, " <!--")?;
578 for (key, value) in &pairs {
579 writeln!(out, " {key}: {}", one_line(value).replace("--", "- -"))?;
580 }
581 writeln!(out, " -->")?;
582 }
583
584 writeln!(out, " <{container}>")?;
585 if !pairs.is_empty() {
586 writeln!(out, " <FieldData>")?;
587 for (key, value) in &pairs {
588 let codes: Vec<String> = one_line(value)
589 .chars()
590 .map(|c| (c as u32).to_string())
591 .chain(std::iter::once("0".to_string()))
592 .collect();
593 writeln!(
594 out,
595 " <DataArray type=\"String\" Name=\"{}\" NumberOfTuples=\"1\" format=\"ascii\">",
596 escape(&sanitise(key))
597 )?;
598 writeln!(out, " {}", codes.join(" "))?;
599 writeln!(out, " </DataArray>")?;
600 }
601 writeln!(out, " </FieldData>")?;
602 }
603
604 match self.flavour {
605 Flavour::UnstructuredGrid => {
606 let count = cells.count();
607 writeln!(
608 out,
609 " <Piece NumberOfPoints=\"{}\" NumberOfCells=\"{count}\">",
610 points.len()
611 )?;
612 write_points(points, out)?;
613 write_unstructured_cells(cells, out)?;
614 }
615 _ => {
616 writeln!(
617 out,
618 " <Piece NumberOfPoints=\"{}\" NumberOfVerts=\"{}\" NumberOfLines=\"{}\" \
619 NumberOfStrips=\"0\" NumberOfPolys=\"{}\">",
620 points.len(),
621 cells.vertices.len(),
622 cells.lines.len(),
623 cells.triangles.len()
627 + cells.quads.len()
628 + cells.polygons.len()
629 + cells.hexahedra.len() * 6
630 + cells.polyhedra.iter().map(|cell| cell.len()).sum::<usize>()
631 )?;
632 write_points(points, out)?;
633 write_poly_cells(cells, out)?;
634 }
635 }
636 write_attributes(object, cells, out)?;
637 writeln!(out, " </Piece>")?;
638 writeln!(out, " </{container}>")?;
639 writeln!(out, "</VTKFile>")?;
640 Ok(())
641 }
642
643 fn pairs(&self, object: &SceneObject, options: &Options<'_>) -> Vec<(String, String)> {
649 let mut pairs: Vec<(String, String)> = vec![
650 ("uuid".to_string(), object.uuid.clone()),
651 ("uri".to_string(), object.uri.clone()),
652 ("title".to_string(), object.title.clone()),
653 ("type".to_string(), object.qualified_type.clone()),
654 ];
655 if let Some(crs) = &object.crs {
656 let system = match crs.projected.epsg() {
657 Some(code) => format!("EPSG:{code}"),
658 None => format!("{:?}", crs.projected),
659 };
660 pairs.push(("crs".to_string(), format!("{} {system}", crs.uuid)));
661 }
662 if let Some(note) = options.time_equivalence_note(object) {
666 pairs.push(("vertical_axis".to_string(), note));
667 }
668 for (key, value) in object.metadata.pairs() {
669 if let Some(existing) = pairs.iter_mut().find(|(k, _)| k == key) {
671 existing.1 = value.to_string();
672 } else {
673 pairs.push((key.to_string(), value.to_string()));
674 }
675 }
676 pairs.retain(|(_, value)| !value.is_empty());
677 pairs
678 }
679}
680
681fn write_attributes(object: &SceneObject, cells: &Cells, out: &mut dyn Write) -> Result<()> {
687 for (scope, tag) in [
688 (AttributeScope::Points, "PointData"),
689 (AttributeScope::Cells, "CellData"),
690 ] {
691 let attributes: Vec<&Attribute> = object
692 .attributes
693 .iter()
694 .filter(|attribute| attribute.scope == scope)
695 .collect();
696 if attributes.is_empty() {
697 continue;
698 }
699 let _ = cells;
700 writeln!(
701 out,
702 " <{tag} Scalars=\"{}\">",
703 escape(&sanitise(&attributes[0].name))
704 )?;
705 for attribute in attributes {
706 writeln!(
707 out,
708 " <DataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"{}\" \
709 format=\"ascii\">",
710 escape(&sanitise(&attribute.name)),
711 attribute.components.max(1)
712 )?;
713 let numbers: Vec<String> = attribute.values.iter().map(number).collect();
714 writeln!(out, " {}", numbers.join(" "))?;
715 writeln!(out, " </DataArray>")?;
716 }
717 writeln!(out, " </{tag}>")?;
718 }
719 Ok(())
720}
721
722fn number(value: &f64) -> String {
727 if value.is_nan() {
728 "nan".to_string()
729 } else if value.is_infinite() {
730 if *value > 0.0 {
731 "inf".to_string()
732 } else {
733 "-inf".to_string()
734 }
735 } else {
736 value.to_string()
737 }
738}
739
740fn write_points(points: &[[f64; 3]], out: &mut dyn Write) -> Result<()> {
741 writeln!(out, " <Points>")?;
742 writeln!(
743 out,
744 " <DataArray type=\"Float64\" NumberOfComponents=\"3\" format=\"ascii\">"
745 )?;
746 for point in points {
747 writeln!(out, " {} {} {}", point[0], point[1], point[2])?;
748 }
749 writeln!(out, " </DataArray>")?;
750 writeln!(out, " </Points>")?;
751 Ok(())
752}
753
754fn legacy_polyhedron_len(cell: &[Vec<u32>]) -> usize {
761 1 + cell.iter().map(|face| face.len() + 1).sum::<usize>()
762}
763
764fn write_poly_cells(cells: &Cells, out: &mut dyn Write) -> Result<()> {
765 if !cells.vertices.is_empty() {
766 let connectivity: Vec<String> = cells.vertices.iter().map(|v| v.to_string()).collect();
767 let offsets: Vec<String> = (1..=cells.vertices.len()).map(|i| i.to_string()).collect();
768 section("Verts", &connectivity, &offsets, out)?;
769 }
770 if !cells.lines.is_empty() {
771 let mut connectivity = Vec::new();
772 let mut offsets = Vec::new();
773 let mut at = 0usize;
774 for line in &cells.lines {
775 connectivity.extend(line.iter().map(|i| i.to_string()));
776 at += line.len();
777 offsets.push(at.to_string());
778 }
779 section("Lines", &connectivity, &offsets, out)?;
780 }
781 if !cells.triangles.is_empty()
782 || !cells.quads.is_empty()
783 || !cells.polygons.is_empty()
784 || !cells.hexahedra.is_empty()
785 || !cells.polyhedra.is_empty()
786 {
787 let mut connectivity = Vec::new();
788 let mut offsets = Vec::new();
789 let mut at = 0usize;
790 for triangle in &cells.triangles {
791 connectivity.extend(triangle.iter().map(|v| v.to_string()));
792 at += 3;
793 offsets.push(at.to_string());
794 }
795 for quad in &cells.quads {
796 connectivity.extend(quad.iter().map(|v| v.to_string()));
797 at += 4;
798 offsets.push(at.to_string());
799 }
800 for ring in &cells.polygons {
801 connectivity.extend(ring.iter().map(|v| v.to_string()));
802 at += ring.len();
803 offsets.push(at.to_string());
804 }
805 for cell in &cells.hexahedra {
806 for face in HEXAHEDRON_FACES {
807 connectivity.extend(face.iter().map(|corner| cell[*corner].to_string()));
808 at += 4;
809 offsets.push(at.to_string());
810 }
811 }
812 for cell in &cells.polyhedra {
816 for face in cell {
817 connectivity.extend(face.iter().map(|node| node.to_string()));
818 at += face.len();
819 offsets.push(at.to_string());
820 }
821 }
822 section("Polys", &connectivity, &offsets, out)?;
823 }
824 Ok(())
825}
826
827fn write_unstructured_cells(cells: &Cells, out: &mut dyn Write) -> Result<()> {
828 let mut connectivity: Vec<String> = Vec::new();
829 let mut offsets: Vec<String> = Vec::new();
830 let mut types: Vec<String> = Vec::new();
831 let mut at = 0usize;
832
833 for vertex in &cells.vertices {
836 connectivity.push(vertex.to_string());
837 at += 1;
838 offsets.push(at.to_string());
839 types.push("1".to_string());
840 }
841 for line in &cells.lines {
842 connectivity.extend(line.iter().map(|i| i.to_string()));
843 at += line.len();
844 offsets.push(at.to_string());
845 types.push("4".to_string());
846 }
847 for triangle in &cells.triangles {
848 connectivity.extend(triangle.iter().map(|v| v.to_string()));
849 at += 3;
850 offsets.push(at.to_string());
851 types.push("5".to_string());
852 }
853 for quad in &cells.quads {
854 connectivity.extend(quad.iter().map(|v| v.to_string()));
855 at += 4;
856 offsets.push(at.to_string());
857 types.push("9".to_string());
858 }
859 for ring in &cells.polygons {
860 connectivity.extend(ring.iter().map(|v| v.to_string()));
861 at += ring.len();
862 offsets.push(at.to_string());
863 types.push("7".to_string());
864 }
865 for cell in &cells.hexahedra {
866 connectivity.extend(cell.iter().map(|corner| corner.to_string()));
867 at += 8;
868 offsets.push(at.to_string());
869 types.push("12".to_string());
870 }
871
872 let mut faces: Vec<String> = Vec::new();
880 let mut face_offsets: Vec<String> = Vec::new();
881 for cell in &cells.polyhedra {
882 let mut points: Vec<u32> = cell.iter().flatten().copied().collect();
883 points.sort_unstable();
884 points.dedup();
885 connectivity.extend(points.iter().map(|node| node.to_string()));
886 at += points.len();
887 offsets.push(at.to_string());
888 types.push("42".to_string());
889
890 faces.push(cell.len().to_string());
891 for face in cell {
892 faces.push(face.len().to_string());
893 faces.extend(face.iter().map(|node| node.to_string()));
894 }
895 face_offsets.push(faces.len().to_string());
896 }
897
898 writeln!(out, " <Cells>")?;
899 array("Int64", "connectivity", &connectivity, out)?;
900 array("Int64", "offsets", &offsets, out)?;
901 array("UInt8", "types", &types, out)?;
902 if !faces.is_empty() {
903 let others = cells.count() - cells.polyhedra.len();
904 let mut all: Vec<String> = vec!["-1".to_string(); others];
905 all.extend(face_offsets);
906 array("Int64", "faces", &faces, out)?;
907 array("Int64", "faceoffsets", &all, out)?;
908 }
909 writeln!(out, " </Cells>")?;
910 Ok(())
911}
912
913fn section(name: &str, connectivity: &[String], offsets: &[String], out: &mut dyn Write) -> Result<()> {
914 writeln!(out, " <{name}>")?;
915 array("Int64", "connectivity", connectivity, out)?;
916 array("Int64", "offsets", offsets, out)?;
917 writeln!(out, " </{name}>")?;
918 Ok(())
919}
920
921fn array(kind: &str, name: &str, values: &[String], out: &mut dyn Write) -> Result<()> {
922 writeln!(
923 out,
924 " <DataArray type=\"{kind}\" Name=\"{name}\" format=\"ascii\">"
925 )?;
926 writeln!(out, " {}", values.join(" "))?;
927 writeln!(out, " </DataArray>")?;
928 Ok(())
929}
930
931fn sanitise(name: &str) -> String {
933 let cleaned: String = name
934 .chars()
935 .map(|c| if c.is_whitespace() { '_' } else { c })
936 .collect();
937 if cleaned.is_empty() {
938 "field".to_string()
939 } else {
940 cleaned
941 }
942}
943
944fn one_line(value: &str) -> String {
947 value.replace(['\n', '\r'], " ")
948}
949
950fn escape(value: &str) -> String {
951 value
952 .replace('&', "&")
953 .replace('<', "<")
954 .replace('>', ">")
955 .replace('"', """)
956}