#[cfg(feature = "color")]
use super::color::Color;
use glam::{DVec2, DVec3};
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct EdgeData {
pub polylines: Vec<Vec<DVec3>>,
}
#[derive(Debug, Clone)]
pub struct Mesh {
pub vertices: Vec<DVec3>,
pub uvs: Vec<DVec2>,
pub normals: Vec<DVec3>,
pub indices: Vec<usize>,
pub face_ids: Vec<u64>,
#[cfg(feature = "color")]
pub colormap: HashMap<u64, Color>,
pub edges: EdgeData,
}
impl Mesh {
pub fn write_stl<W: std::io::Write>(&self, writer: &mut W) -> Result<(), super::error::Error> {
let tri_count = self.indices.len() / 3;
writer.write_all(&[0u8; 80]).map_err(|_| super::error::Error::StlWriteFailed)?;
writer.write_all(&(tri_count as u32).to_le_bytes()).map_err(|_| super::error::Error::StlWriteFailed)?;
for ti in 0..tri_count {
let i0 = self.indices[ti * 3];
let i1 = self.indices[ti * 3 + 1];
let i2 = self.indices[ti * 3 + 2];
let v0 = self.vertices[i0];
let v1 = self.vertices[i1];
let v2 = self.vertices[i2];
let n = (v1 - v0).cross(v2 - v0).normalize_or_zero();
for c in [n.x, n.y, n.z] { writer.write_all(&(c as f32).to_le_bytes()).map_err(|_| super::error::Error::StlWriteFailed)?; }
for v in [v0, v1, v2] {
for c in [v.x, v.y, v.z] { writer.write_all(&(c as f32).to_le_bytes()).map_err(|_| super::error::Error::StlWriteFailed)?; }
}
#[cfg(feature = "color")]
let attr = {
let face_id = self.face_ids[ti];
if let Some(c) = self.colormap.get(&face_id) {
let r = (c.r * 31.0) as u16;
let g = (c.g * 31.0) as u16;
let b = (c.b * 31.0) as u16;
0x8000 | r | (g << 5) | (b << 10)
} else {
0u16
}
};
#[cfg(not(feature = "color"))]
let attr = 0u16;
writer.write_all(&attr.to_le_bytes()).map_err(|_| super::error::Error::StlWriteFailed)?;
}
Ok(())
}
}
impl Mesh {
pub fn write_svg<W: std::io::Write>(&self, direction: DVec3, hidden_lines: bool, shading: bool, writer: &mut W) -> Result<(), super::error::Error> {
writer.write_all(self.to_svg(direction, hidden_lines, shading).as_bytes()).map_err(|_| super::error::Error::SvgExportFailed)
}
pub fn to_svg(&self, direction: DVec3, hidden_lines: bool, shading: bool) -> String {
let dir = direction.normalize();
let (u, v) = projection_basis(dir);
let face_triangles = project_and_sort_triangles(self, dir, u, v, shading);
let silhouette_edges = detect_silhouette_edges(self, dir);
let all_edges: Vec<&Vec<DVec3>> = self.edges.polylines.iter().chain(silhouette_edges.iter()).collect();
let occlusion_tris = build_occlusion_data(self, dir, u, v);
let (visible, hidden) = classify_edges(&all_edges, &occlusion_tris, dir, u, v);
let hidden = if hidden_lines { hidden } else { Vec::new() };
build_svg(&face_triangles, &visible, &hidden)
}
}
struct SvgTriangle {
pts: [(f64, f64); 3],
depth: f64,
fill: String,
}
struct OcclusionTri {
pts: [(f64, f64); 3],
depths: [f64; 3],
}
fn projection_basis(dir: DVec3) -> (DVec3, DVec3) {
let (a, b, c) = (dir.x, dir.y, dir.z);
let (a_abs, b_abs, c_abs) = (a.abs(), b.abs(), c.abs());
let perp = if b_abs <= a_abs && b_abs <= c_abs {
if a_abs > c_abs {
DVec3::new(-c, 0.0, a)
} else {
DVec3::new(c, 0.0, -a)
}
} else if a_abs <= b_abs && a_abs <= c_abs {
if b_abs > c_abs {
DVec3::new(0.0, -c, b)
} else {
DVec3::new(0.0, c, -b)
}
} else {
if a_abs > b_abs {
DVec3::new(-b, a, 0.0)
} else {
DVec3::new(b, -a, 0.0)
}
};
let x_dir = perp.normalize();
let y_dir = dir.cross(x_dir);
(x_dir, y_dir)
}
fn project_and_sort_triangles(mesh: &Mesh, dir: DVec3, u: DVec3, v: DVec3, shading: bool) -> Vec<SvgTriangle> {
let tri_count = mesh.indices.len() / 3;
let mut triangles = Vec::with_capacity(tri_count);
for ti in 0..tri_count {
let i0 = mesh.indices[ti * 3];
let i1 = mesh.indices[ti * 3 + 1];
let i2 = mesh.indices[ti * 3 + 2];
let v0 = mesh.vertices[i0];
let v1 = mesh.vertices[i1];
let v2 = mesh.vertices[i2];
let avg_normal = (mesh.normals[i0] + mesh.normals[i1] + mesh.normals[i2]) / 3.0;
if avg_normal.dot(dir) < 0.0 {
continue;
}
let p0 = (v0.dot(u), v0.dot(v));
let p1 = (v1.dot(u), v1.dot(v));
let p2 = (v2.dot(u), v2.dot(v));
let depth = (v0.dot(dir) + v1.dot(dir) + v2.dot(dir)) / 3.0;
let shade = if shading {
let dot = avg_normal.normalize_or_zero().dot(dir).clamp(0.0, 1.0);
0.5 + 0.5 * dot
} else {
1.0
};
let gray = 0xdd as f64 / 255.0;
#[cfg(feature = "color")]
let (base_r, base_g, base_b) = {
let face_id = mesh.face_ids[ti];
if let Some(c) = mesh.colormap.get(&face_id) {
(c.r as f64, c.g as f64, c.b as f64)
} else {
(gray, gray, gray)
}
};
#[cfg(not(feature = "color"))]
let (base_r, base_g, base_b) = (gray, gray, gray);
let fill = format!(
"#{:02x}{:02x}{:02x}",
(base_r * shade * 255.0) as u8,
(base_g * shade * 255.0) as u8,
(base_b * shade * 255.0) as u8,
);
triangles.push(SvgTriangle { pts: [p0, p1, p2], depth, fill });
}
triangles.sort_by(|a, b| a.depth.partial_cmp(&b.depth).unwrap_or(std::cmp::Ordering::Equal));
triangles
}
fn build_occlusion_data(mesh: &Mesh, dir: DVec3, u: DVec3, v: DVec3) -> Vec<OcclusionTri> {
let tri_count = mesh.indices.len() / 3;
let mut tris = Vec::with_capacity(tri_count / 2);
for ti in 0..tri_count {
let i0 = mesh.indices[ti * 3];
let i1 = mesh.indices[ti * 3 + 1];
let i2 = mesh.indices[ti * 3 + 2];
let v0 = mesh.vertices[i0];
let v1 = mesh.vertices[i1];
let v2 = mesh.vertices[i2];
let avg_normal = (mesh.normals[i0] + mesh.normals[i1] + mesh.normals[i2]) / 3.0;
if avg_normal.dot(dir) <= 0.0 {
continue;
}
tris.push(OcclusionTri { pts: [(v0.dot(u), v0.dot(v)), (v1.dot(u), v1.dot(v)), (v2.dot(u), v2.dot(v))], depths: [v0.dot(dir), v1.dot(dir), v2.dot(dir)] });
}
tris
}
fn detect_silhouette_edges(mesh: &Mesh, dir: DVec3) -> Vec<Vec<DVec3>> {
let tri_count = mesh.indices.len() / 3;
let mut edge_tris: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
for ti in 0..tri_count {
let i0 = mesh.indices[ti * 3];
let i1 = mesh.indices[ti * 3 + 1];
let i2 = mesh.indices[ti * 3 + 2];
for &(a, b) in &[(i0, i1), (i1, i2), (i2, i0)] {
let key = if a < b { (a, b) } else { (b, a) };
edge_tris.entry(key).or_default().push(ti);
}
}
let mut silhouettes = Vec::new();
for (&(a, b), tris) in &edge_tris {
let is_silhouette = if tris.len() == 1 {
tri_facing(mesh, tris[0], dir)
} else if tris.len() == 2 {
tri_facing(mesh, tris[0], dir) != tri_facing(mesh, tris[1], dir)
} else {
false
};
if is_silhouette {
silhouettes.push(vec![mesh.vertices[a], mesh.vertices[b]]);
}
}
silhouettes
}
fn tri_facing(mesh: &Mesh, ti: usize, dir: DVec3) -> bool {
let i0 = mesh.indices[ti * 3];
let i1 = mesh.indices[ti * 3 + 1];
let i2 = mesh.indices[ti * 3 + 2];
let avg_normal = (mesh.normals[i0] + mesh.normals[i1] + mesh.normals[i2]) / 3.0;
avg_normal.dot(dir) > 0.0
}
fn classify_edges(edges: &[&Vec<DVec3>], occlusion_tris: &[OcclusionTri], dir: DVec3, u: DVec3, v: DVec3) -> (Vec<Vec<(f64, f64)>>, Vec<Vec<(f64, f64)>>) {
let mut visible_polylines = Vec::new();
let mut hidden_polylines = Vec::new();
for edge in edges {
if edge.len() < 2 {
continue;
}
let mut vis_line: Vec<(f64, f64)> = Vec::new();
let mut hid_line: Vec<(f64, f64)> = Vec::new();
for window in edge.windows(2) {
let a3d = window[0];
let b3d = window[1];
let mid = (a3d + b3d) * 0.5;
let mid_2d = (mid.dot(u), mid.dot(v));
let mid_depth = mid.dot(dir);
let a_2d = (a3d.dot(u), a3d.dot(v));
let b_2d = (b3d.dot(u), b3d.dot(v));
let hidden = is_point_occluded(mid_2d, mid_depth, occlusion_tris);
if hidden {
if vis_line.len() >= 2 {
visible_polylines.push(std::mem::take(&mut vis_line));
} else {
vis_line.clear();
}
if hid_line.is_empty() {
hid_line.push(a_2d);
}
hid_line.push(b_2d);
} else {
if hid_line.len() >= 2 {
hidden_polylines.push(std::mem::take(&mut hid_line));
} else {
hid_line.clear();
}
if vis_line.is_empty() {
vis_line.push(a_2d);
}
vis_line.push(b_2d);
}
}
if vis_line.len() >= 2 {
visible_polylines.push(vis_line);
}
if hid_line.len() >= 2 {
hidden_polylines.push(hid_line);
}
}
(visible_polylines, hidden_polylines)
}
fn is_point_occluded(point_2d: (f64, f64), point_depth: f64, tris: &[OcclusionTri]) -> bool {
let eps = 1e-4;
for tri in tris {
if let Some((w0, w1, w2)) = barycentric_2d(point_2d, tri.pts) {
let tri_depth = w0 * tri.depths[0] + w1 * tri.depths[1] + w2 * tri.depths[2];
if tri_depth > point_depth + eps {
return true; }
}
}
false
}
fn barycentric_2d(p: (f64, f64), t: [(f64, f64); 3]) -> Option<(f64, f64, f64)> {
let (px, py) = p;
let (x0, y0) = t[0];
let (x1, y1) = t[1];
let (x2, y2) = t[2];
let denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
if denom.abs() < 1e-12 {
return None; }
let w0 = ((y1 - y2) * (px - x2) + (x2 - x1) * (py - y2)) / denom;
let w1 = ((y2 - y0) * (px - x2) + (x0 - x2) * (py - y2)) / denom;
let w2 = 1.0 - w0 - w1;
if w0 >= -1e-8 && w1 >= -1e-8 && w2 >= -1e-8 {
Some((w0, w1, w2))
} else {
None
}
}
fn polylines_to_svg(svg: &mut String, polylines: &[Vec<(f64, f64)>], stroke: &str, dash: &str, width: Option<f64>) {
for line in polylines {
svg.push_str("<polyline points=\"");
for (i, &(x, y)) in line.iter().enumerate() {
let y = -y;
if i > 0 {
svg.push(' ');
}
svg.push_str(&format!("{x:.4},{y:.4}"));
}
svg.push_str("\" fill=\"none\" stroke=\"");
svg.push_str(stroke);
svg.push('"');
if let Some(w) = width {
svg.push_str(&format!(" stroke-width=\"{w:.4}\""));
}
if !dash.is_empty() {
svg.push_str(" stroke-dasharray=\"");
svg.push_str(dash);
svg.push('"');
}
svg.push_str("/>\n");
}
}
fn build_svg(triangles: &[SvgTriangle], visible_lines: &[Vec<(f64, f64)>], hidden_lines: &[Vec<(f64, f64)>]) -> String {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for tri in triangles {
for &(x, y) in &tri.pts {
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
}
for lines in [visible_lines, hidden_lines] {
for line in lines {
for &(x, y) in line {
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
}
}
if min_x > max_x {
min_x = 0.0;
max_x = 1.0;
min_y = 0.0;
max_y = 1.0;
}
let margin_frac = 0.05;
let w = max_x - min_x;
let h = max_y - min_y;
let margin = if w > h { w } else { h } * margin_frac;
let vx = min_x - margin;
let vy = -(max_y + margin);
let vw = w + margin * 2.0;
let vh = h + margin * 2.0;
let sw = (if w > h { w } else { h }) * 0.003;
let hidden_sw = sw * 0.6;
let dash_len = sw * 5.0;
let dash_gap = sw * 4.0;
let mut svg = String::with_capacity(4096 + triangles.len() * 120);
svg.push_str(&format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{vx:.4} {vy:.4} {vw:.4} {vh:.4}\" \
stroke-width=\"{sw:.4}\">\n"
));
for tri in triangles {
let [(x0, y0), (x1, y1), (x2, y2)] = tri.pts;
let y0 = -y0;
let y1 = -y1;
let y2 = -y2;
svg.push_str(&format!(
"<polygon points=\"{x0:.4},{y0:.4} {x1:.4},{y1:.4} {x2:.4},{y2:.4}\" \
fill=\"{}\" stroke=\"none\"/>\n",
tri.fill
));
}
polylines_to_svg(&mut svg, visible_lines, "black", "", None);
polylines_to_svg(&mut svg, hidden_lines, "#bbb", &format!("{dash_len:.4},{dash_gap:.4}"), Some(hidden_sw));
svg.push_str("</svg>\n");
svg
}