#[cfg(feature = "color")]
use super::color::Color;
use glam::{DVec2, DVec3};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Mesh {
pub vertices: Vec<DVec3>,
pub uvs: Vec<DVec2>,
pub indices: Vec<usize>,
pub face_ids: Vec<u64>,
#[cfg(feature = "color")]
pub colormap: HashMap<u64, Color>,
pub edges: Vec<DVec3>,
}
#[derive(Debug, Clone)]
pub struct Scene2D {
pub triangles: Vec<[DVec2; 3]>,
pub color: Vec<[u8; 3]>,
pub edges_visible: Vec<DVec2>,
pub edges_hidden: Vec<DVec2>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SceneOption {
pub view: DVec3,
pub up: DVec3,
pub hidden_edges: bool,
pub shading: bool,
}
impl Default for SceneOption {
fn default() -> Self {
Self { view: DVec3::ONE, up: DVec3::Z, hidden_edges: true, shading: false }
}
}
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 = self.colormap.get(&self.face_ids[ti]).map_or(0, Color::as_u16);
#[cfg(not(feature = "color"))]
let attr = 0u16;
writer.write_all(&attr.to_le_bytes()).map_err(|_| super::error::Error::StlWriteFailed)?;
}
Ok(())
}
pub fn write_gltf_binary<W: std::io::Write>(&self, writer: &mut W) -> Result<(), super::error::Error> {
use super::error::Error;
let mut bin: Vec<u8> = Vec::new();
let mut buffer_views: Vec<String> = Vec::new();
let mut accessors: Vec<String> = Vec::new();
let mut primitives: Vec<String> = Vec::new();
#[allow(unused_mut)]
let mut materials: Vec<String> = Vec::new();
let tri_groups = self.gltf_triangle_groups(&mut materials);
if !tri_groups.is_empty() && !self.vertices.is_empty() {
let mut bytes = Vec::with_capacity(self.vertices.len() * 12);
let (mut min, mut max) = ([f32::INFINITY; 3], [f32::NEG_INFINITY; 3]);
for v in &self.vertices {
let f = [v.x as f32, v.y as f32, v.z as f32];
for k in 0..3 { min[k] = min[k].min(f[k]); max[k] = max[k].max(f[k]); }
for c in f { bytes.extend_from_slice(&c.to_le_bytes()); }
}
let pos_bv = push_buffer_view(&mut buffer_views, &mut bin, &bytes, 34962);
let pos_acc = push_accessor_vec3(&mut accessors, pos_bv, self.vertices.len(), min, max);
for (indices, material) in tri_groups {
if indices.is_empty() { continue; }
let mut bytes = Vec::with_capacity(indices.len() * 4);
for &i in &indices { bytes.extend_from_slice(&i.to_le_bytes()); }
let bv = push_buffer_view(&mut buffer_views, &mut bin, &bytes, 34963);
let acc = push_accessor_scalar_u32(&mut accessors, bv, indices.len());
let mat = material.map_or(String::new(), |m| format!(r#","material":{}"#, m));
primitives.push(format!(r#"{{"attributes":{{"POSITION":{}}},"indices":{},"mode":4{}}}"#, pos_acc, acc, mat));
}
}
let (epos, eidx) = self.gltf_edge_buffers();
if !eidx.is_empty() {
let mut pbytes = Vec::with_capacity(epos.len() * 12);
let (mut min, mut max) = ([f32::INFINITY; 3], [f32::NEG_INFINITY; 3]);
for p in &epos {
for k in 0..3 { min[k] = min[k].min(p[k]); max[k] = max[k].max(p[k]); }
for c in p { pbytes.extend_from_slice(&c.to_le_bytes()); }
}
let pos_bv = push_buffer_view(&mut buffer_views, &mut bin, &pbytes, 34962);
let pos_acc = push_accessor_vec3(&mut accessors, pos_bv, epos.len(), min, max);
let mut ibytes = Vec::with_capacity(eidx.len() * 4);
for &i in &eidx { ibytes.extend_from_slice(&i.to_le_bytes()); }
let idx_bv = push_buffer_view(&mut buffer_views, &mut bin, &ibytes, 34963);
let idx_acc = push_accessor_scalar_u32(&mut accessors, idx_bv, eidx.len());
primitives.push(format!(r#"{{"attributes":{{"POSITION":{}}},"indices":{},"mode":1,"extras":{{"cadrum":"edges"}}}}"#, pos_acc, idx_acc));
}
let mut members: Vec<String> = vec![r#""asset":{"version":"2.0","generator":"cadrum"}"#.to_string()];
if !bin.is_empty() { members.push(format!(r#""buffers":[{{"byteLength":{}}}]"#, bin.len())); }
if !buffer_views.is_empty() { members.push(format!(r#""bufferViews":[{}]"#, buffer_views.join(","))); }
if !accessors.is_empty() { members.push(format!(r#""accessors":[{}]"#, accessors.join(","))); }
if !materials.is_empty() {
members.push(format!(r#""materials":[{}]"#, materials.join(",")));
members.push(r#""extensionsUsed":["KHR_materials_unlit"]"#.to_string());
}
if !primitives.is_empty() {
members.push(format!(r#""meshes":[{{"primitives":[{}]}}]"#, primitives.join(",")));
members.push(r#""nodes":[{"mesh":0}]"#.to_string());
members.push(r#""scenes":[{"nodes":[0]}]"#.to_string());
members.push(r#""scene":0"#.to_string());
}
let json = format!("{{{}}}", members.join(","));
let mut json_bytes = json.into_bytes();
while json_bytes.len() % 4 != 0 { json_bytes.push(b' '); }
while bin.len() % 4 != 0 { bin.push(0); }
let mut total = 12 + 8 + json_bytes.len();
if !bin.is_empty() { total += 8 + bin.len(); }
let mut w = |b: &[u8]| writer.write_all(b).map_err(|_| Error::GltfWriteFailed);
w(&0x46546C67u32.to_le_bytes())?; w(&2u32.to_le_bytes())?; w(&(total as u32).to_le_bytes())?;
w(&(json_bytes.len() as u32).to_le_bytes())?;
w(&0x4E4F534Au32.to_le_bytes())?; w(&json_bytes)?;
if !bin.is_empty() {
w(&(bin.len() as u32).to_le_bytes())?;
w(&0x004E4942u32.to_le_bytes())?; w(&bin)?;
}
Ok(())
}
#[cfg(feature = "color")]
fn gltf_triangle_groups(&self, materials: &mut Vec<String>) -> Vec<(Vec<u32>, Option<usize>)> {
const DEFAULT: [f32; 3] = [0.8667, 0.8667, 0.8667]; let tri_count = self.indices.len() / 3;
let mut groups: Vec<(Vec<u32>, [f32; 3])> = Vec::new();
let mut key_to_group: HashMap<[u32; 3], usize> = HashMap::new();
for ti in 0..tri_count {
let col = self.colormap.get(&self.face_ids[ti]).map_or(DEFAULT, |c| [c.r, c.g, c.b]);
let key = [col[0].to_bits(), col[1].to_bits(), col[2].to_bits()];
let g = *key_to_group.entry(key).or_insert_with(|| {
groups.push((Vec::new(), col));
groups.len() - 1
});
groups[g].0.extend_from_slice(&[self.indices[ti * 3] as u32, self.indices[ti * 3 + 1] as u32, self.indices[ti * 3 + 2] as u32]);
}
groups
.into_iter()
.map(|(indices, col)| {
let mat = materials.len();
materials.push(format!(r#"{{"pbrMetallicRoughness":{{"baseColorFactor":[{},{},{},1.0],"metallicFactor":0.0,"roughnessFactor":1.0}},"alphaMode":"OPAQUE","doubleSided":true,"extensions":{{"KHR_materials_unlit":{{}}}}}}"#, col[0], col[1], col[2]));
(indices, Some(mat))
})
.collect()
}
#[cfg(not(feature = "color"))]
fn gltf_triangle_groups(&self, _materials: &mut Vec<String>) -> Vec<(Vec<u32>, Option<usize>)> {
let indices: Vec<u32> = self.indices.iter().map(|&i| i as u32).collect();
if indices.is_empty() { Vec::new() } else { vec![(indices, None)] }
}
fn gltf_edge_buffers(&self) -> (Vec<[f32; 3]>, Vec<u32>) {
let mut pos: Vec<[f32; 3]> = Vec::new();
let mut idx: Vec<u32> = Vec::new();
let mut prev: Option<u32> = None;
for p in &self.edges {
if p.is_nan() {
prev = None;
continue;
}
let i = pos.len() as u32;
pos.push([p.x as f32, p.y as f32, p.z as f32]);
if let Some(pr) = prev {
idx.push(pr);
idx.push(i);
}
prev = Some(i);
}
(pos, idx)
}
pub fn scene(&self, option: SceneOption) -> Scene2D {
let SceneOption { view, up, hidden_edges, shading } = option;
let (u, v, dir) = projection_basis(view, up);
let (triangles, color) = project_and_sort_triangles(self, dir, u, v, shading);
let silhouette_edges = detect_silhouette_edges(self, dir);
let all_edges: Vec<&Vec<DVec3>> = silhouette_edges.iter().collect();
let occlusion_tris = build_occlusion_data(self, dir, u, v);
let (edges_visible, hidden) = classify_edges(&all_edges, &occlusion_tris, dir, u, v);
let edges_hidden = if hidden_edges { hidden } else { Vec::new() };
Scene2D { triangles, color, edges_visible, edges_hidden }
}
}
fn push_buffer_view(views: &mut Vec<String>, bin: &mut Vec<u8>, data: &[u8], target: u32) -> usize {
while bin.len() % 4 != 0 {
bin.push(0);
}
let offset = bin.len();
bin.extend_from_slice(data);
let idx = views.len();
views.push(format!(r#"{{"buffer":0,"byteOffset":{},"byteLength":{},"target":{}}}"#, offset, data.len(), target));
idx
}
fn push_accessor_vec3(accs: &mut Vec<String>, bv: usize, count: usize, min: [f32; 3], max: [f32; 3]) -> usize {
let idx = accs.len();
accs.push(format!(
r#"{{"bufferView":{},"componentType":5126,"count":{},"type":"VEC3","min":[{},{},{}],"max":[{},{},{}]}}"#,
bv, count, min[0], min[1], min[2], max[0], max[1], max[2]
));
idx
}
fn push_accessor_scalar_u32(accs: &mut Vec<String>, bv: usize, count: usize) -> usize {
let idx = accs.len();
accs.push(format!(r#"{{"bufferView":{},"componentType":5125,"count":{},"type":"SCALAR"}}"#, bv, count));
idx
}
fn tri_normal(mesh: &Mesh, ti: usize) -> DVec3 {
let i0 = mesh.indices[ti * 3];
let i1 = mesh.indices[ti * 3 + 1];
let i2 = mesh.indices[ti * 3 + 2];
(mesh.vertices[i1] - mesh.vertices[i0]).cross(mesh.vertices[i2] - mesh.vertices[i0])
}
struct OcclusionTri {
pts: [DVec2; 3],
depths: [f64; 3],
}
fn projection_basis(view: DVec3, up: DVec3) -> (DVec3, DVec3, DVec3) {
let dir = view.try_normalize().expect("write_svg: view is zero");
let v = (up - dir * up.dot(dir))
.try_normalize()
.expect("write_svg: up is zero or parallel to view");
let u = v.cross(dir);
(u, v, dir)
}
fn project_and_sort_triangles(mesh: &Mesh, dir: DVec3, u: DVec3, v: DVec3, shading: bool) -> (Vec<[DVec2; 3]>, Vec<[u8; 3]>) {
let tri_count = mesh.indices.len() / 3;
let mut buf: Vec<([DVec2; 3], [u8; 3], f64)> = 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 face_normal = tri_normal(mesh, ti);
if face_normal.dot(dir) < 0.0 {
continue;
}
let p0 = DVec2::new(v0.dot(u), v0.dot(v));
let p1 = DVec2::new(v1.dot(u), v1.dot(v));
let p2 = DVec2::new(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 = face_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 color = [
(base_r * shade * 255.0) as u8,
(base_g * shade * 255.0) as u8,
(base_b * shade * 255.0) as u8,
];
buf.push(([p0, p1, p2], color, depth));
}
buf.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
let mut triangles = Vec::with_capacity(buf.len());
let mut colors = Vec::with_capacity(buf.len());
for (pts, color, _) in buf {
triangles.push(pts);
colors.push(color);
}
(triangles, colors)
}
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];
if tri_normal(mesh, ti).dot(dir) <= 0.0 {
continue;
}
tris.push(OcclusionTri {
pts: [DVec2::new(v0.dot(u), v0.dot(v)), DVec2::new(v1.dot(u), v1.dot(v)), DVec2::new(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 {
tri_normal(mesh, ti).dot(dir) > 0.0
}
fn classify_edges(edges: &[&Vec<DVec3>], occlusion_tris: &[OcclusionTri], dir: DVec3, u: DVec3, v: DVec3) -> (Vec<DVec2>, Vec<DVec2>) {
let mut visible: Vec<DVec2> = Vec::new();
let mut hidden: Vec<DVec2> = Vec::new();
for edge in edges {
if edge.len() < 2 {
continue;
}
let mut vis_line: Vec<DVec2> = Vec::new();
let mut hid_line: Vec<DVec2> = 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 = DVec2::new(mid.dot(u), mid.dot(v));
let mid_depth = mid.dot(dir);
let a_2d = DVec2::new(a3d.dot(u), a3d.dot(v));
let b_2d = DVec2::new(b3d.dot(u), b3d.dot(v));
let is_hidden = is_point_occluded(mid_2d, mid_depth, occlusion_tris);
if is_hidden {
flush_polyline(&mut visible, &mut vis_line);
if hid_line.is_empty() {
hid_line.push(a_2d);
}
hid_line.push(b_2d);
} else {
flush_polyline(&mut hidden, &mut hid_line);
if vis_line.is_empty() {
vis_line.push(a_2d);
}
vis_line.push(b_2d);
}
}
flush_polyline(&mut visible, &mut vis_line);
flush_polyline(&mut hidden, &mut hid_line);
}
(visible, hidden)
}
fn flush_polyline(out: &mut Vec<DVec2>, staging: &mut Vec<DVec2>) {
if staging.len() < 2 {
staging.clear();
return;
}
if !out.is_empty() {
out.push(DVec2::NAN);
}
out.append(staging);
}
fn is_point_occluded(p: DVec2, point_depth: f64, tris: &[OcclusionTri]) -> bool {
let eps = 1e-4;
for tri in tris {
if let Some((w0, w1, w2)) = barycentric_2d(p, 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: DVec2, t: [DVec2; 3]) -> Option<(f64, f64, f64)> {
let denom = (t[1].y - t[2].y) * (t[0].x - t[2].x) + (t[2].x - t[1].x) * (t[0].y - t[2].y);
if denom.abs() < 1e-12 {
return None;
}
let w0 = ((t[1].y - t[2].y) * (p.x - t[2].x) + (t[2].x - t[1].x) * (p.y - t[2].y)) / denom;
let w1 = ((t[2].y - t[0].y) * (p.x - t[2].x) + (t[0].x - t[2].x) * (p.y - t[2].y)) / denom;
let w2 = 1.0 - w0 - w1;
if w0 >= -1e-8 && w1 >= -1e-8 && w2 >= -1e-8 {
Some((w0, w1, w2))
} else {
None
}
}
struct Layout {
vx: f64,
vy: f64,
vw: f64,
vh: f64,
stroke_width: f64,
hidden_stroke_width: f64,
dash_len: f64,
dash_gap: f64,
}
impl Scene2D {
pub fn viewbox(&self) -> [DVec2; 2] {
let init = (DVec2::splat(f64::INFINITY), DVec2::splat(f64::NEG_INFINITY));
let (min, max) = self.triangles.iter().flatten().copied()
.fold(init, |(mn, mx), p| (mn.min(p), mx.max(p)));
if min.x > max.x { [DVec2::ZERO, DVec2::ONE] } else { [min, max] }
}
fn layout(&self) -> Layout {
let [min, max] = self.viewbox();
let margin_frac = 0.05;
let w = max.x - min.x;
let h = max.y - min.y;
let span = w.max(h);
let margin = span * margin_frac;
let stroke_width = span * 0.003;
Layout {
vx: min.x - margin,
vy: -(max.y + margin),
vw: w + margin * 2.0,
vh: h + margin * 2.0,
stroke_width,
hidden_stroke_width: stroke_width * 0.6,
dash_len: stroke_width * 5.0,
dash_gap: stroke_width * 4.0,
}
}
}
impl Scene2D {
pub fn write_svg<W: std::io::Write>(&self, writer: &mut W) -> Result<(), super::error::Error> {
let Layout { vx, vy, vw, vh, stroke_width: sw, hidden_stroke_width: hidden_sw, dash_len, dash_gap } = self.layout();
let mut svg = String::with_capacity(4096 + self.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, color) in self.triangles.iter().zip(self.color.iter()) {
let [p0, p1, p2] = *tri;
let [r, g, b] = *color;
svg.push_str(&format!(
"<polygon points=\"{:.4},{:.4} {:.4},{:.4} {:.4},{:.4}\" \
fill=\"#{r:02x}{g:02x}{b:02x}\" stroke=\"none\"/>\n",
p0.x, -p0.y, p1.x, -p1.y, p2.x, -p2.y,
));
}
polylines_to_svg(&mut svg, &self.edges_visible, "black", "", None);
polylines_to_svg(&mut svg, &self.edges_hidden, "#bbb", &format!("{dash_len:.4},{dash_gap:.4}"), Some(hidden_sw));
svg.push_str("</svg>\n");
writer.write_all(svg.as_bytes()).map_err(|_| super::error::Error::SvgExportFailed)
}
}
fn polylines_to_svg(svg: &mut String, polylines: &[DVec2], stroke: &str, dash: &str, width: Option<f64>) {
let mut start = 0;
for i in 0..=polylines.len() {
let is_sep = i == polylines.len() || polylines[i].is_nan();
if is_sep {
let line = &polylines[start..i];
if line.len() >= 2 {
emit_polyline(svg, line, stroke, dash, width);
}
start = i + 1;
}
}
}
fn emit_polyline(svg: &mut String, line: &[DVec2], stroke: &str, dash: &str, width: Option<f64>) {
svg.push_str("<polyline points=\"");
for (i, p) in line.iter().enumerate() {
if i > 0 {
svg.push(' ');
}
svg.push_str(&format!("{:.4},{:.4}", p.x, -p.y));
}
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");
}
impl Scene2D {
#[cfg(feature = "png")]
pub fn write_png<W: std::io::Write>(&self, dimensions: [usize; 2], writer: &mut W) -> Result<(), super::error::Error> {
use tiny_skia::{Pixmap, Transform};
let [width, height] = dimensions;
let layout = self.layout();
let pps = ((width as f64) / layout.vw).min((height as f64) / layout.vh);
let off_x = (width as f64 - layout.vw * pps) / 2.0;
let off_y = (height as f64 - layout.vh * pps) / 2.0;
let s = pps as f32;
let tx = -(layout.vx as f32) * s + off_x as f32;
let ty = -(layout.vy as f32) * s + off_y as f32;
let transform = Transform::from_row(s, 0.0, 0.0, -s, tx, ty);
let mut pixmap = Pixmap::new(width as u32, height as u32).ok_or(super::error::Error::PngExportFailed)?;
self.render_to_pixmap(
&mut pixmap,
transform,
layout.stroke_width as f32,
layout.hidden_stroke_width as f32,
layout.dash_len as f32,
layout.dash_gap as f32,
);
let png_bytes = pixmap.encode_png().map_err(|_| super::error::Error::PngExportFailed)?;
writer.write_all(&png_bytes).map_err(|_| super::error::Error::PngExportFailed)
}
#[cfg(feature = "png")]
pub(crate) fn render_to_pixmap(
&self,
pixmap: &mut tiny_skia::Pixmap,
transform: tiny_skia::Transform,
stroke_width: f32,
hidden_stroke_width: f32,
dash_len: f32,
dash_gap: f32,
) {
use tiny_skia::{FillRule, Paint, PathBuilder, Stroke, StrokeDash};
for (tri, color) in self.triangles.iter().zip(self.color.iter()) {
let mut pb = PathBuilder::new();
pb.move_to(tri[0].x as f32, tri[0].y as f32);
pb.line_to(tri[1].x as f32, tri[1].y as f32);
pb.line_to(tri[2].x as f32, tri[2].y as f32);
pb.close();
if let Some(path) = pb.finish() {
let mut paint = Paint::default();
paint.set_color_rgba8(color[0], color[1], color[2], 255);
paint.anti_alias = true;
pixmap.fill_path(&path, &paint, FillRule::Winding, transform, None);
}
}
let mut visible_paint = Paint::default();
visible_paint.set_color_rgba8(0, 0, 0, 255);
visible_paint.anti_alias = true;
let visible_stroke = Stroke { width: stroke_width, ..Stroke::default() };
Self::stroke_polylines(pixmap, &self.edges_visible, &visible_paint, &visible_stroke, transform);
let mut hidden_paint = Paint::default();
hidden_paint.set_color_rgba8(0xbb, 0xbb, 0xbb, 255);
hidden_paint.anti_alias = true;
let hidden_stroke = Stroke {
width: hidden_stroke_width,
dash: StrokeDash::new(vec![dash_len, dash_gap], 0.0),
..Stroke::default()
};
Self::stroke_polylines(pixmap, &self.edges_hidden, &hidden_paint, &hidden_stroke, transform);
}
#[cfg(feature = "png")]
fn stroke_polylines(
pixmap: &mut tiny_skia::Pixmap,
polylines: &[DVec2],
paint: &tiny_skia::Paint,
stroke: &tiny_skia::Stroke,
transform: tiny_skia::Transform,
) {
let mut start = 0;
for i in 0..=polylines.len() {
let is_sep = i == polylines.len() || polylines[i].is_nan();
if is_sep {
let line = &polylines[start..i];
if line.len() >= 2 {
let mut pb = tiny_skia::PathBuilder::new();
pb.move_to(line[0].x as f32, line[0].y as f32);
for p in &line[1..] {
pb.line_to(p.x as f32, p.y as f32);
}
if let Some(path) = pb.finish() {
pixmap.stroke_path(&path, paint, stroke, transform, None);
}
}
start = i + 1;
}
}
}
}
impl Mesh {
#[cfg(feature = "png")]
pub fn write_multiview_png<W: std::io::Write>(&self, writer: &mut W) -> Result<(), super::error::Error> {
use tiny_skia::{Pixmap, Transform};
const IMG_SIZE: u32 = 1024;
const H_SCALE: f64 = 1.05;
const GNOMON_SIZE: f32 = 48.0;
const GNOMON_INSET: f32 = 24.0;
const TICK_SIZE: f32 = 12.0;
const LABEL_SIZE: f32 = 16.0;
let views: [(DVec3, DVec3); 4] = [
(DVec3::new(1.0, 1.0, 1.0), DVec3::Z), (DVec3::Z, -DVec3::Y), (DVec3::X, DVec3::Z), (DVec3::Y, DVec3::Z), ];
let bases: [(DVec3, DVec3, DVec3); 4] = std::array::from_fn(|i| projection_basis(views[i].0, views[i].1));
let scenes: [Scene2D; 4] = std::array::from_fn(|i| self.scene(SceneOption { view: views[i].0, up: views[i].1, ..Default::default() }));
let h = scenes.iter()
.map(|v| v.viewbox())
.flat_map(|[a,b]| [a.x,a.y,b.x,b.y])
.map(|x|x.abs()*H_SCALE)
.reduce(f64::max)
.unwrap_or(1.0);
let panel_w = (IMG_SIZE as f32) / 2.0;
let panel_h = (IMG_SIZE as f32) / 2.0;
let pps = (panel_w.min(panel_h) as f64) / (2.0 * h);
let mut pixmap = Pixmap::new(IMG_SIZE, IMG_SIZE).ok_or(super::error::Error::PngExportFailed)?;
let stroke_px = 1.5_f32;
let scene_stroke = stroke_px / (pps as f32);
let scene_hidden_stroke = scene_stroke * 0.6;
let scene_dash_len = scene_stroke * 4.0;
let scene_dash_gap = scene_stroke * 3.0;
for (i, scene) in scenes.iter().enumerate() {
let (col, row) = (i % 2, i / 2);
let px0 = (col as f32) * panel_w;
let py0 = (row as f32) * panel_h;
let cx = px0 + panel_w / 2.0;
let cy = py0 + panel_h / 2.0;
let s = pps as f32;
let transform = Transform::from_row(s, 0.0, 0.0, -s, cx, cy);
scene.render_to_pixmap(&mut pixmap, transform, scene_stroke, scene_hidden_stroke, scene_dash_len, scene_dash_gap);
let (u_basis, v_basis, _) = bases[i];
let g_origin = (px0 + panel_w - GNOMON_SIZE - GNOMON_INSET, py0 + panel_h - GNOMON_SIZE - GNOMON_INSET);
draw_gnomon(&mut pixmap, g_origin, GNOMON_SIZE, LABEL_SIZE, u_basis, v_basis);
}
preview_path(&mut pixmap, [
[0.0, panel_h, IMG_SIZE as f32, panel_h],
[panel_w, 0.0, panel_w, IMG_SIZE as f32],
], 0xcccccc, 1.0);
let step1 = nice_step(h * 1.6);
let step2 = nice_step(h * 0.7);
let boundary_y = panel_h; for (step, center_x) in [(step1, panel_w / 2.0), (step2, panel_w * 1.5)] {
let bar_px = (step * pps) as f32;
let x0 = center_x - bar_px / 2.0;
let x1 = center_x + bar_px / 2.0;
preview_path(&mut pixmap, [
[x0, boundary_y, x1, boundary_y],
[x0, boundary_y - TICK_SIZE / 2.0, x0, boundary_y + TICK_SIZE / 2.0],
[x1, boundary_y - TICK_SIZE / 2.0, x1, boundary_y + TICK_SIZE / 2.0],
], 0x1f3a8a, 2.0);
let label = format!("{}", step);
let glyph_w = LABEL_SIZE * 0.6;
let text_w = (label.chars().count() as f32) * glyph_w * 1.2 - glyph_w * 0.2;
draw_text(&mut pixmap, &label, center_x - text_w / 2.0, boundary_y - LABEL_SIZE - 4.0, LABEL_SIZE, 0x1f3a8a);
}
let png_bytes = pixmap.encode_png().map_err(|_| super::error::Error::PngExportFailed)?;
writer.write_all(&png_bytes).map_err(|_| super::error::Error::PngExportFailed)
}
}
fn nice_step(target: f64) -> f64 {
if !target.is_finite() || target <= 0.0 {
return 1.0;
}
let exp = target.log10().floor() as i32;
let pow = 10f64.powi(exp);
let m = target / pow;
let nice = if m < 2.0 { 1.0 } else if m < 5.0 { 2.0 } else { 5.0 };
nice * pow
}
fn glyph_polyline(c: char) -> &'static [[f32; 2]] {
match c {
'0' => &[[0.0,0.0],[1.0,0.0],[1.0,1.0],[0.0,1.0],[0.0,0.0]],
'1' => &[[0.2,0.8],[0.5,1.0],[0.5,0.0]],
'2' => &[[0.0,1.0],[1.0,1.0],[1.0,0.5],[0.0,0.5],[0.0,0.0],[1.0,0.0]],
'5' => &[[1.0,1.0],[0.0,1.0],[0.0,0.5],[1.0,0.5],[1.0,0.0],[0.0,0.0]],
'.' => &[[0.4,0.0],[0.6,0.0],[0.6,0.15],[0.4,0.15],[0.4,0.0]],
'X' => &[[0.0,0.0],[1.0,1.0],[0.5,0.5],[0.0,1.0],[1.0,0.0]],
'Y' => &[[0.0,1.0],[0.5,0.5],[0.5,0.0],[0.5,0.5],[1.0,1.0]],
'Z' => &[[0.0,1.0],[1.0,1.0],[0.0,0.0],[1.0,0.0]],
_ => &[],
}
}
#[cfg(feature = "png")]
fn preview_path(pixmap: &mut tiny_skia::Pixmap, segments: impl IntoIterator<Item = [f32; 4]>, color: u32, stroke_width: f32) {
use tiny_skia::{Paint, PathBuilder, Stroke, Transform};
let mut pb = PathBuilder::new();
for [x0, y0, x1, y1] in segments {
pb.move_to(x0, y0);
pb.line_to(x1, y1);
}
let Some(path) = pb.finish() else { return };
let mut paint = Paint::default();
paint.set_color_rgba8(((color >> 16) & 0xff) as u8, ((color >> 8) & 0xff) as u8, (color & 0xff) as u8, 255);
paint.anti_alias = true;
let stroke = Stroke { width: stroke_width, ..Stroke::default() };
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
#[cfg(feature = "png")]
fn draw_text(pixmap: &mut tiny_skia::Pixmap, text: &str, x: f32, y: f32, size: f32, color: u32) {
let glyph_w = size * 0.6;
let advance = glyph_w * 1.2;
let mut segments: Vec<[f32; 4]> = Vec::new();
let mut cursor = x;
for ch in text.chars() {
for w in glyph_polyline(ch).windows(2) {
segments.push([
cursor + w[0][0] * glyph_w, y + (1.0 - w[0][1]) * size,
cursor + w[1][0] * glyph_w, y + (1.0 - w[1][1]) * size,
]);
}
cursor += advance;
}
preview_path(pixmap, segments, color, (size * 0.1).max(1.0));
}
#[cfg(feature = "png")]
fn draw_gnomon(pixmap: &mut tiny_skia::Pixmap, origin: (f32, f32), size: f32, text_size: f32, u: DVec3, v: DVec3) {
let cx = origin.0 + size / 2.0;
let cy = origin.1 + size / 2.0;
let axes: [(DVec3, &str, u32); 3] = [
(DVec3::X, "X", 0xc0392b),
(DVec3::Y, "Y", 0x27ae60),
(DVec3::Z, "Z", 0x2980b9),
];
for (axis, label, color) in axes {
let p = DVec2::new(axis.dot(u), axis.dot(v));
let len = p.length();
if len < 0.15 {
continue;
}
let ex = cx + (p.x as f32) * (size / 2.0);
let ey = cy - (p.y as f32) * (size / 2.0);
preview_path(pixmap, [[cx, cy, ex, ey]], color, 1.5);
let label_off = text_size;
let dir_x = (ex - cx) / (len.max(1e-9) as f32 * (size / 2.0));
let dir_y = (ey - cy) / (len.max(1e-9) as f32 * (size / 2.0));
let lx = ex + dir_x * label_off - text_size * 0.3;
let ly = ey + dir_y * label_off - text_size * 0.5;
draw_text(pixmap, label, lx, ly, text_size, color);
}
}