use std::io::Write;
use energyml_geometry::{Placement, SceneObject, Topology};
use crate::error::{Error, Result};
use crate::registry::{Exporter, Options};
pub struct ObjExporter;
impl Exporter for ObjExporter {
fn name(&self) -> &'static str {
"obj"
}
fn extension(&self) -> &'static str {
"obj"
}
fn mime(&self) -> &'static str {
"model/obj"
}
fn describe(&self) -> &'static str {
"Wavefront OBJ: triangles and polylines, placed and in metres"
}
fn placement(&self) -> Placement {
Placement::Metres
}
fn handles(&self, object: &SceneObject) -> bool {
!object.geometry.is_empty()
}
fn write(&self, object: &SceneObject, out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
if object.geometry.is_empty() {
return Err(Error::unsuitable(
"obj",
format!("`{}`, which has no points", object.title),
));
}
self.write_all(std::slice::from_ref(object), out, options)
}
fn write_all(&self, objects: &[SceneObject], out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
let mut base = 1u32;
for object in objects {
if !self.handles(object) {
continue;
}
let object = crate::registry::prepare(object, options)?;
let object = object.as_ref();
let placement = options.placement.unwrap_or_else(|| self.placement());
let points = options.points(object, placement)?;
header(object, placement, options, out)?;
writeln!(out, "o {}", sanitise(&object.title, &object.uuid))?;
for point in &points {
writeln!(out, "v {} {} {}", point[0], point[1], point[2])?;
}
for patch in &object.geometry.patches {
match &patch.topology {
Topology::Triangles(triangles) => {
for triangle in triangles {
writeln!(
out,
"f {} {} {}",
triangle[0] + base,
triangle[1] + base,
triangle[2] + base
)?;
}
}
Topology::Segments(pairs) => {
for pair in pairs {
writeln!(out, "l {} {}", pair[0] + base, pair[1] + base)?;
}
}
Topology::Polylines(runs) => {
let mut start = base;
for (rank, run) in runs.iter().enumerate() {
if *run >= 2 {
let mut indices: Vec<String> =
(0..*run).map(|i| (start + i).to_string()).collect();
if patch.closed.get(rank) == Some(&true) {
indices.push(start.to_string());
}
writeln!(out, "l {}", indices.join(" "))?;
}
start += run;
}
}
Topology::Hexahedra { cells, .. } => {
for cell in cells {
for face in HEXAHEDRON_FACES {
writeln!(
out,
"f {} {} {} {}",
cell[face[0]] + base,
cell[face[1]] + base,
cell[face[2]] + base,
cell[face[3]] + base
)?;
}
}
}
Topology::Polyhedra(cells) => {
for cell in 0..cells.cell_count() {
for face in cells.faces_outward(cell) {
let nodes: Vec<String> =
face.iter().map(|node| (node + base).to_string()).collect();
writeln!(out, "f {}", nodes.join(" "))?;
}
}
}
Topology::Quads(quads) => {
for quad in quads {
writeln!(
out,
"f {} {} {} {}",
quad[0] + base,
quad[1] + base,
quad[2] + base,
quad[3] + base
)?;
}
}
Topology::Polygons(rings) => {
for ring in rings {
let nodes: Vec<String> =
ring.iter().map(|node| (node + base).to_string()).collect();
writeln!(out, "f {}", nodes.join(" "))?;
}
}
Topology::Grid { .. } | Topology::Points => {
writeln!(out, "# {} point(s), no faces", patch.points.len())?;
}
}
base += patch.points.len() as u32;
}
}
Ok(())
}
}
fn header(
object: &SceneObject,
placement: Placement,
options: &Options<'_>,
out: &mut dyn Write,
) -> Result<()> {
writeln!(out, "# {}", object.title)?;
writeln!(out, "# uuid {}", object.uuid)?;
writeln!(out, "# uri {}", object.uri)?;
writeln!(out, "# type {}", object.qualified_type)?;
if let Some(interpretation) = &object.interpretation {
writeln!(
out,
"# interpretation {} {}",
interpretation.uuid, interpretation.title
)?;
}
if let Some(feature) = &object.feature {
writeln!(out, "# feature {} {}", feature.uuid, feature.title)?;
}
for (key, value) in object.metadata.pairs() {
writeln!(out, "# {key} {value}")?;
}
match (&object.crs, placement) {
(Some(crs), Placement::Local) => writeln!(out, "# crs {} (local coordinates)", crs.uuid)?,
(Some(crs), _) => {
let system = match crs.projected.epsg() {
Some(code) => format!("EPSG:{code}"),
None => format!("{:?}", crs.projected),
};
let vertical = if crs.is_time() {
"z from travel time"
} else {
"metres, z up"
};
writeln!(out, "# crs {} {system}, {vertical}", crs.uuid)?;
}
(None, _) => writeln!(out, "# crs none stated; coordinates are as written")?,
}
if let Some(note) = options.time_equivalence_note(object) {
for line in wrap_comment(¬e) {
writeln!(out, "# {line}")?;
}
}
Ok(())
}
const HEXAHEDRON_FACES: [[usize; 4]; 6] = [
[0, 3, 2, 1], [4, 5, 6, 7],
[0, 1, 5, 4],
[1, 2, 6, 5],
[2, 3, 7, 6],
[3, 0, 4, 7],
];
fn sanitise(title: &str, uuid: &str) -> String {
let cleaned: String = title
.chars()
.map(|c| if c.is_whitespace() { '_' } else { c })
.filter(|c| !matches!(c, '#' | '\n' | '\r'))
.collect();
if cleaned.trim().is_empty() {
uuid.to_string()
} else {
cleaned
}
}
fn wrap_comment(text: &str) -> Vec<String> {
const WIDTH: usize = 76;
let mut lines = Vec::new();
let mut line = String::new();
for word in text.split_whitespace() {
if !line.is_empty() && line.len() + 1 + word.len() > WIDTH {
lines.push(std::mem::take(&mut line));
}
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
}
if !line.is_empty() {
lines.push(line);
}
lines
}