use crate::SdfNode;
use glam::Vec3;
use std::collections::{HashMap, HashSet};
use std::path::Path;
pub use alice_sdf::io::{export_3mf, export_fbx, export_stl, export_stl_ascii, FbxConfig};
pub use alice_sdf::mesh::{
dual_contouring, sdf_to_mesh, validate_mesh, DualContouringConfig, MarchingCubesConfig, Mesh,
MeshRepair, MeshValidation, Vertex,
};
const VERTEX_MERGE_CELL_RATIO: f32 = 0.04;
#[allow(clippy::cast_precision_loss)]
const fn resolution_as_f32(resolution: usize) -> f32 {
resolution as f32
}
const fn watertight_defects(v: &MeshValidation) -> usize {
v.boundary_edges + v.non_manifold_edges
}
fn referenced_vertices(m: &Mesh) -> Vec<bool> {
let mut used = vec![false; m.vertices.len()];
for &i in &m.indices {
used[i as usize] = true;
}
used
}
fn isolated_vertices(m: &Mesh) -> usize {
referenced_vertices(m).iter().filter(|&&u| !u).count()
}
fn euler_characteristic(m: &Mesh) -> i64 {
let v = referenced_vertices(m).iter().filter(|&&u| u).count();
let mut edges: HashSet<(u32, u32)> = HashSet::with_capacity(m.indices.len());
for t in m.indices.as_chunks::<3>().0 {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
edges.insert((a.min(b), a.max(b)));
}
}
let f = m.indices.len() / 3;
let as_i64 = |n: usize| i64::try_from(n).expect("要素数が i64 に収まらない");
as_i64(v) - as_i64(edges.len()) + as_i64(f)
}
fn repair_is_non_regressive(before: &Mesh, after: &Mesh) -> bool {
watertight_defects(&validate_mesh(after)) <= watertight_defects(&validate_mesh(before))
&& euler_characteristic(after) == euler_characteristic(before)
&& isolated_vertices(after) <= isolated_vertices(before)
}
fn signed_volume(m: &Mesh) -> f64 {
m.indices
.as_chunks::<3>()
.0
.iter()
.map(|t| {
let a = m.vertices[t[0] as usize].position.as_dvec3();
let b = m.vertices[t[1] as usize].position.as_dvec3();
let c = m.vertices[t[2] as usize].position.as_dvec3();
a.dot(b.cross(c)) / 6.0
})
.sum()
}
fn orient_outward(mut mesh: Mesh) -> Mesh {
if signed_volume(&mesh) >= 0.0 {
return mesh;
}
for tri in mesh.indices.as_chunks_mut::<3>().0 {
tri.swap(1, 2);
}
mesh
}
fn weld_coincident_vertices(raw: &Mesh) -> Mesh {
if u32::try_from(raw.vertices.len()).is_err() {
return raw.clone();
}
let mut id: HashMap<[u32; 3], usize> = HashMap::with_capacity(raw.vertices.len());
let mut out = Mesh::new();
let mut remap: Vec<usize> = Vec::with_capacity(raw.vertices.len());
for v in &raw.vertices {
let next = id.len();
let slot = *id
.entry(v.position.to_array().map(f32::to_bits))
.or_insert(next);
if slot == next {
out.vertices.push(*v);
}
remap.push(slot);
}
#[allow(
clippy::cast_possible_truncation,
reason = "頂点数が u32 に収まることは関数先頭で確認済み、統合でしか減らない"
)]
let remap: Vec<u32> = remap.into_iter().map(|i| i as u32).collect();
for t in raw.indices.as_chunks::<3>().0 {
let (a, b, c) = (
remap[t[0] as usize],
remap[t[1] as usize],
remap[t[2] as usize],
);
if a != b && b != c && a != c {
out.indices.extend_from_slice(&[a, b, c]);
}
}
if watertight_defects(&validate_mesh(&out)) <= watertight_defects(&validate_mesh(raw)) {
out
} else {
raw.clone()
}
}
fn repair_preserving_watertightness(raw: &Mesh, eps: f32) -> Mesh {
let raw = &weld_coincident_vertices(raw);
let before = validate_mesh(raw);
if watertight_defects(&before) == 0 {
return orient_outward(raw.clone());
}
let m = MeshRepair::remove_degenerate_triangles(raw);
let m = MeshRepair::merge_duplicate_vertices(&m, eps);
let m = MeshRepair::remove_degenerate_triangles(&m);
let m = MeshRepair::remove_duplicate_triangles(&m);
let candidate = if repair_is_non_regressive(raw, &m) {
MeshRepair::fix_normals(&m)
} else {
raw.clone()
};
orient_outward(candidate)
}
#[derive(Debug, Clone)]
pub struct PrintConfig {
pub resolution: usize,
pub bounds_min: Vec3,
pub bounds_max: Vec3,
pub scale_mm: f32,
}
impl Default for PrintConfig {
fn default() -> Self {
Self {
resolution: 128,
bounds_min: Vec3::splat(-2.0),
bounds_max: Vec3::splat(2.0),
scale_mm: 10.0,
}
}
}
impl PrintConfig {
#[must_use]
pub const fn preview() -> Self {
Self {
resolution: 64,
bounds_min: Vec3::splat(-2.0),
bounds_max: Vec3::splat(2.0),
scale_mm: 10.0,
}
}
#[must_use]
pub const fn high_quality() -> Self {
Self {
resolution: 256,
bounds_min: Vec3::splat(-2.0),
bounds_max: Vec3::splat(2.0),
scale_mm: 10.0,
}
}
#[must_use]
pub const fn ultra() -> Self {
Self {
resolution: 512,
bounds_min: Vec3::splat(-2.0),
bounds_max: Vec3::splat(2.0),
scale_mm: 10.0,
}
}
#[must_use]
pub const fn with_bounds(mut self, min: Vec3, max: Vec3) -> Self {
self.bounds_min = min;
self.bounds_max = max;
self
}
#[must_use]
pub const fn with_scale_mm(mut self, scale_mm: f32) -> Self {
self.scale_mm = scale_mm;
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ExportError {
Parse(crate::runtime_parser::ParseError),
Io(std::io::Error),
EmptyMesh,
}
impl std::fmt::Display for ExportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(e) => write!(f, "LOL parse error: {e}"),
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::EmptyMesh => write!(f, "generated mesh has no triangles"),
}
}
}
impl std::error::Error for ExportError {}
impl From<crate::runtime_parser::ParseError> for ExportError {
fn from(e: crate::runtime_parser::ParseError) -> Self {
Self::Parse(e)
}
}
impl From<std::io::Error> for ExportError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<alice_sdf::io::IoError> for ExportError {
fn from(e: alice_sdf::io::IoError) -> Self {
Self::Io(std::io::Error::other(e.to_string()))
}
}
#[must_use]
pub fn node_to_mesh(node: &SdfNode, config: &PrintConfig) -> Mesh {
let mc_config = MarchingCubesConfig {
resolution: config.resolution,
compute_normals: true,
..MarchingCubesConfig::default()
};
let raw = sdf_to_mesh(node, config.bounds_min, config.bounds_max, &mc_config);
let cell = (config.bounds_max - config.bounds_min).max_element()
/ resolution_as_f32(config.resolution.max(1));
let mut mesh = repair_preserving_watertightness(&raw, VERTEX_MERGE_CELL_RATIO * cell);
if (config.scale_mm - 1.0).abs() > f32::EPSILON {
for v in &mut mesh.vertices {
v.position *= config.scale_mm;
}
}
mesh
}
pub fn node_to_stl(
node: &SdfNode,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let mesh = node_to_mesh(node, config);
if mesh.indices.is_empty() {
return Err(ExportError::EmptyMesh);
}
let stats = ExportStats::from_mesh(&mesh, &path);
export_stl(&mesh, path)?;
Ok(stats)
}
pub fn node_to_3mf(
node: &SdfNode,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let mesh = node_to_mesh(node, config);
if mesh.indices.is_empty() {
return Err(ExportError::EmptyMesh);
}
let stats = ExportStats::from_mesh(&mesh, &path);
export_3mf(&mesh, path)?;
Ok(stats)
}
pub fn lol_to_stl(
lol_text: &str,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let node = crate::runtime_parser::parse_lol(lol_text)?;
node_to_stl(&node, path, config)
}
pub fn lol_to_3mf(
lol_text: &str,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let node = crate::runtime_parser::parse_lol(lol_text)?;
node_to_3mf(&node, path, config)
}
pub fn node_to_fbx(
node: &SdfNode,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let mesh = node_to_mesh(node, config);
if mesh.indices.is_empty() {
return Err(ExportError::EmptyMesh);
}
let stats = ExportStats::from_mesh(&mesh, &path);
export_fbx(&mesh, path, &FbxConfig::binary(), None)?;
Ok(stats)
}
pub fn lol_to_fbx(
lol_text: &str,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let node = crate::runtime_parser::parse_lol(lol_text)?;
node_to_fbx(&node, path, config)
}
#[must_use]
pub fn node_to_mesh_dual_contouring(node: &SdfNode, config: &PrintConfig) -> Mesh {
let dc_config = DualContouringConfig {
resolution: config.resolution,
compute_normals: true,
..DualContouringConfig::default()
};
let mut mesh = dual_contouring(node, config.bounds_min, config.bounds_max, &dc_config);
if (config.scale_mm - 1.0).abs() > f32::EPSILON {
for v in &mut mesh.vertices {
v.position *= config.scale_mm;
}
}
mesh
}
pub fn node_to_stl_dual_contouring(
node: &SdfNode,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let mesh = node_to_mesh_dual_contouring(node, config);
if mesh.indices.is_empty() {
return Err(ExportError::EmptyMesh);
}
let stats = ExportStats::from_mesh(&mesh, &path);
export_stl(&mesh, path)?;
Ok(stats)
}
pub fn node_to_3mf_dual_contouring(
node: &SdfNode,
path: impl AsRef<Path>,
config: &PrintConfig,
) -> Result<ExportStats, ExportError> {
let mesh = node_to_mesh_dual_contouring(node, config);
if mesh.indices.is_empty() {
return Err(ExportError::EmptyMesh);
}
let stats = ExportStats::from_mesh(&mesh, &path);
export_3mf(&mesh, path)?;
Ok(stats)
}
#[derive(Debug, Clone)]
pub struct ExportStats {
pub vertex_count: usize,
pub triangle_count: usize,
pub path: String,
}
impl ExportStats {
fn from_mesh(mesh: &Mesh, path: &impl AsRef<Path>) -> Self {
Self {
vertex_count: mesh.vertices.len(),
triangle_count: mesh.indices.len() / 3,
path: path.as_ref().display().to_string(),
}
}
}
impl std::fmt::Display for ExportStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {} vertices, {} triangles",
self.path, self.vertex_count, self.triangle_count
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mesh(points: &[[f32; 3]], indices: &[u32]) -> Mesh {
let mut m = Mesh::new();
for p in points {
m.vertices.push(Vertex::new(Vec3::from_array(*p), Vec3::Z));
}
m.indices.extend_from_slice(indices);
m
}
const TETRA_POINTS: [[f32; 3]; 4] = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
const TETRA_INDICES: [u32; 12] = [0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3];
fn tetra_with_extra_vertex() -> Mesh {
let mut points = TETRA_POINTS.to_vec();
points.push([5.0, 5.0, 5.0]);
mesh(&points, &TETRA_INDICES)
}
#[test]
fn euler_characteristic_counts_only_referenced_vertices() {
let tri = mesh(
&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
&[0, 1, 2],
);
assert_eq!(euler_characteristic(&tri), 1);
assert_eq!(isolated_vertices(&tri), 0);
let tri_plus = mesh(
&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[3.0, 3.0, 3.0],
],
&[0, 1, 2],
);
assert_eq!(
euler_characteristic(&tri_plus),
1,
"未参照の頂点が χ に入った"
);
assert_eq!(isolated_vertices(&tri_plus), 1);
let tetra = mesh(&TETRA_POINTS, &TETRA_INDICES);
assert_eq!(euler_characteristic(&tetra), 2);
assert_eq!(isolated_vertices(&tetra), 0);
let tetra_plus = tetra_with_extra_vertex();
assert_eq!(
euler_characteristic(&tetra_plus),
2,
"未参照の頂点が χ に入った"
);
assert_eq!(isolated_vertices(&tetra_plus), 1);
let gaps = mesh(
&[
[9.0, 9.0, 9.0],
[0.0, 0.0, 0.0],
[8.0, 8.0, 8.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
],
&[1, 3, 4],
);
assert_eq!(isolated_vertices(&gaps), 2);
assert_eq!(euler_characteristic(&gaps), 1);
}
#[test]
fn the_repair_gate_rejects_each_kind_of_regression_on_its_own() {
let tetra = mesh(&TETRA_POINTS, &TETRA_INDICES);
assert!(repair_is_non_regressive(&tetra, &tetra), "同じ mesh は通る");
assert!(
!repair_is_non_regressive(&tetra, &tetra_with_extra_vertex()),
"孤立頂点が増えたのに採られた"
);
assert!(repair_is_non_regressive(&tetra_with_extra_vertex(), &tetra));
let mut two_points = TETRA_POINTS.to_vec();
two_points.extend(TETRA_POINTS.iter().map(|p| [p[0] + 3.0, p[1], p[2]]));
let mut two_indices = TETRA_INDICES.to_vec();
two_indices.extend(TETRA_INDICES.iter().map(|i| i + 4));
let two = mesh(&two_points, &two_indices);
assert_eq!(euler_characteristic(&two), 4);
assert!(
!repair_is_non_regressive(&two, &tetra),
"χ が変わったのに採られた"
);
let tri = mesh(
&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
&[0, 1, 2],
);
let quad = mesh(
&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
],
&[0, 1, 2, 1, 3, 2],
);
assert_eq!(euler_characteristic(&quad), 1);
assert!(
!repair_is_non_regressive(&tri, &quad),
"境界 edge が増えたのに採られた"
);
assert!(
repair_is_non_regressive(&quad, &tri),
"境界 edge が減るのは通る"
);
}
}