use crate::mesh::{sdf_to_mesh, MarchingCubesConfig};
use crate::types::SdfNode;
use glam::Vec3;
use std::io::Write;
use std::path::Path;
#[derive(Clone, Debug)]
pub struct StepConfig {
pub bounds: (f32, f32),
pub resolution: u32,
pub name: String,
}
impl Default for StepConfig {
fn default() -> Self {
Self {
bounds: (-2.0, 2.0),
resolution: 32,
name: "AliceSdfMesh".into(),
}
}
}
fn sdf_to_facets(node: &SdfNode, cfg: &StepConfig) -> (Vec<Vec3>, Vec<[usize; 3]>) {
let (lo, hi) = cfg.bounds;
let mc = MarchingCubesConfig {
resolution: (cfg.resolution as usize).max(4),
iso_level: 0.0,
compute_normals: false,
compute_uvs: false,
uv_scale: 1.0,
compute_tangents: false,
compute_materials: false,
};
let mesh = sdf_to_mesh(node, Vec3::splat(lo), Vec3::splat(hi), &mc);
let verts: Vec<Vec3> = mesh.vertices.iter().map(|v| v.position).collect();
let tris: Vec<[usize; 3]> = mesh
.indices
.chunks_exact(3)
.map(|t| [t[0] as usize, t[1] as usize, t[2] as usize])
.collect();
(verts, tris)
}
pub fn export_step(
path: impl AsRef<Path>,
node: &SdfNode,
cfg: &StepConfig,
) -> std::io::Result<()> {
let (verts, tris) = sdf_to_facets(node, cfg);
let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
write_step(&mut f, &verts, &tris, &cfg.name)?;
f.flush()
}
fn write_step<W: Write>(
w: &mut W,
verts: &[Vec3],
tris: &[[usize; 3]],
name: &str,
) -> std::io::Result<()> {
writeln!(w, "ISO-10303-21;")?;
writeln!(w, "HEADER;")?;
writeln!(
w,
"FILE_DESCRIPTION(('ALICE-SDF v1.6 Faceted BREP'),'2;1');"
)?;
writeln!(
w,
"FILE_NAME('{name}.step','2026-06-06T00:00:00',('ALICE-SDF'),(''),'ALICE-SDF v1.6','','');",
)?;
writeln!(w, "FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));")?;
writeln!(w, "ENDSEC;")?;
writeln!(w, "DATA;")?;
let mut id = 1u32;
let mut next = || {
let i = id;
id += 1;
i
};
let mut point_ids = Vec::with_capacity(verts.len());
for v in verts {
let pid = next();
writeln!(
w,
"#{pid}=CARTESIAN_POINT('',({:.6},{:.6},{:.6}));",
v.x, v.y, v.z
)?;
point_ids.push(pid);
}
for tri in tris {
let p0 = point_ids[tri[0]];
let p1 = point_ids[tri[1]];
let p2 = point_ids[tri[2]];
let loop_id = next();
writeln!(w, "#{loop_id}=POLY_LOOP('',(#{p0},#{p1},#{p2}));")?;
let face_id = next();
writeln!(w, "#{face_id}=FACE_OUTER_BOUND('',#{loop_id},.T.);")?;
}
let prod_def_ctx = next();
writeln!(
w,
"#{prod_def_ctx}=PRODUCT_DEFINITION_CONTEXT('part definition',#0,'design');"
)?;
let prod = next();
writeln!(w, "#{prod}=PRODUCT('{name}','{name}','',(#0));")?;
writeln!(w, "ENDSEC;")?;
writeln!(w, "END-ISO-10303-21;")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::SdfNode;
#[test]
fn sphere_step_export() {
let path = std::env::temp_dir().join("alice_sdf_test.step");
let n = SdfNode::sphere(1.0);
let cfg = StepConfig {
bounds: (-2.0, 2.0),
resolution: 8,
name: "TestSphere".into(),
};
export_step(&path, &n, &cfg).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.starts_with("ISO-10303-21;"));
assert!(content.contains("END-ISO-10303-21;"));
assert!(content.contains("CARTESIAN_POINT"));
std::fs::remove_file(&path).ok();
}
#[test]
fn empty_node_writes_valid_header() {
let path = std::env::temp_dir().join("alice_sdf_empty.step");
let n = SdfNode::sphere(0.001);
let cfg = StepConfig {
bounds: (-2.0, 2.0),
resolution: 4,
name: "Empty".into(),
};
export_step(&path, &n, &cfg).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("DATA;"));
std::fs::remove_file(&path).ok();
}
#[test]
fn marching_cubes_produces_dense_mesh() {
let n = SdfNode::sphere(1.0);
let cfg = StepConfig {
bounds: (-1.5, 1.5),
resolution: 16,
name: "QualityCheck".into(),
};
let (verts, tris) = super::sdf_to_facets(&n, &cfg);
assert!(
verts.len() > 100,
"expected >100 verts from MC at res=16, got {}",
verts.len()
);
assert!(
!tris.is_empty(),
"expected non-empty triangulation, got {} tris",
tris.len()
);
}
#[test]
fn step_export_contains_one_point_per_vertex() {
let path = std::env::temp_dir().join("alice_sdf_count.step");
let n = SdfNode::sphere(1.0);
let cfg = StepConfig {
bounds: (-1.5, 1.5),
resolution: 8,
name: "Count".into(),
};
export_step(&path, &n, &cfg).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let pt_count = content.matches("CARTESIAN_POINT").count();
let (verts, _) = super::sdf_to_facets(&n, &cfg);
assert_eq!(
pt_count,
verts.len(),
"CARTESIAN_POINT count != vertex count"
);
std::fs::remove_file(&path).ok();
}
}