Files
OneCAD-Draft/src/entities/mesh.rs
T
Hakan SevenandClaude Opus 5 8c7ebdc9f1 refactor: drop truck, and evaluate every curve through the kernel
The last four truck crates are gone from the manifest. What they were doing
falls into three parts, and each now has one answer instead of two.

NURBS evaluation. truck was carrying B-spline curves and surfaces for the
SPLINE entity, hatch boundaries, BLEND's endpoint frames, the spline preview
and ACIS spline-surface faces. The kernel now has both, over one de Boor
written across however many coordinates a control point holds — so a plane
curve and a space curve cannot drift apart, and a surface is the same
algorithm applied twice. The rational and the polynomial cases stop being
separate types: weights absent means polynomial.

The entity conversion. Every LINE, ARC, ELLIPSE, SPLINE and polyline was
built into truck topology purely so it could be sampled back into points.
They are sampled through `entities::curve` now, which is where each one's
geometry is already defined once and what EXTRUDE and REVOLVE read — so a
circle drawn on screen and a circle handed to the Model tab come from the
same definition. That retires four of TruckObject's variants and the module
is renamed for what it does.

SWEEP and LOFT. Both only ever produced a mesh, so both are built from point
lists: a band of quads per span, and a lid where a profile closes. Lofting
profiles of different densities resamples them by distance rather than by
index, so a circle lofted to a square no longer twists.

Two things worth noting for anyone reading the old comments. The tolerance
globals that lived in the tessellation module were never about truck at all —
they are the per-frame chord height, and they move to `curve_tol`. And the
rule that a profile handed over as `Lines` silently broke EXTRUDE and REVOLVE
no longer holds: those read the curve directly and never look at this channel.

The two `automation` test failures are unchanged from before this and are not
caused by it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 01:08:50 +03:00

1215 lines
43 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, PolyfaceMesh};
use glam::Vec3;
use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{parse_f64, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
/// Triangulate a planar (possibly concave) polygon into a flat triangle-soup
/// (3 vertices per triangle), preserving the polygon's winding. A simple fan
/// from vertex 0 is only valid for convex faces — a concave face (e.g. an
/// L-shaped mesh face) fans into triangles that spill outside the outline. Ear
/// clipping handles both. Falls back to a fan when the polygon is degenerate.
pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> {
let n = poly.len();
if n < 3 {
return Vec::new();
}
if n == 3 {
return vec![poly[0], poly[1], poly[2]];
}
let cross = |a: [f64; 3], b: [f64; 3]| {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
};
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let fan = || {
let mut out = Vec::new();
for i in 1..n - 1 {
out.push(poly[0]);
out.push(poly[i]);
out.push(poly[i + 1]);
}
out
};
// Face normal via Newell's method (robust for near-planar polygons).
let mut normal = [0.0f64; 3];
for i in 0..n {
let a = poly[i];
let b = poly[(i + 1) % n];
normal[0] += (a[1] - b[1]) * (a[2] + b[2]);
normal[1] += (a[2] - b[2]) * (a[0] + b[0]);
normal[2] += (a[0] - b[0]) * (a[1] + b[1]);
}
let nlen = dot(normal, normal).sqrt();
if nlen < 1e-12 {
return fan();
}
let normal = [normal[0] / nlen, normal[1] / nlen, normal[2] / nlen];
// Orthonormal in-plane basis.
let seed = if normal[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
let mut u = cross(seed, normal);
let ul = dot(u, u).sqrt();
if ul < 1e-12 {
return fan();
}
u = [u[0] / ul, u[1] / ul, u[2] / ul];
let v = cross(normal, u);
let p2: Vec<[f64; 2]> = poly.iter().map(|&p| [dot(p, u), dot(p, v)]).collect();
// Signed area → winding (CCW when positive in the (u, v) frame).
let mut area = 0.0;
for i in 0..n {
let a = p2[i];
let b = p2[(i + 1) % n];
area += a[0] * b[1] - b[0] * a[1];
}
let ccw = area > 0.0;
let tri_area2 = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
};
let in_tri = |p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
let d1 = tri_area2(a, b, p);
let d2 = tri_area2(b, c, p);
let d3 = tri_area2(c, a, p);
let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
!(neg && pos)
};
let mut idx: Vec<usize> = (0..n).collect();
let mut out: Vec<[f64; 3]> = Vec::with_capacity((n - 2) * 3);
let mut guard = 0usize;
while idx.len() > 3 && guard < n * n {
guard += 1;
let m = idx.len();
let mut clipped = false;
for k in 0..m {
let i0 = idx[(k + m - 1) % m];
let i1 = idx[k];
let i2 = idx[(k + 1) % m];
let (a, b, c) = (p2[i0], p2[i1], p2[i2]);
let convex = if ccw { tri_area2(a, b, c) > 0.0 } else { tri_area2(a, b, c) < 0.0 };
if !convex {
continue;
}
let mut contains = false;
for &j in &idx {
if j == i0 || j == i1 || j == i2 {
continue;
}
if in_tri(p2[j], a, b, c) {
contains = true;
break;
}
}
if contains {
continue;
}
out.push(poly[i0]);
out.push(poly[i1]);
out.push(poly[i2]);
idx.remove(k);
clipped = true;
break;
}
if !clipped {
// No ear found (self-intersecting / numerically degenerate) — bail
// to a fan of the remainder rather than loop forever.
for i in 1..idx.len() - 1 {
out.push(poly[idx[0]]);
out.push(poly[idx[i]]);
out.push(poly[idx[i + 1]]);
}
return out;
}
}
if idx.len() == 3 {
out.push(poly[idx[0]]);
out.push(poly[idx[1]]);
out.push(poly[idx[2]]);
}
out
}
// ── Face3D ────────────────────────────────────────────────────────────────────
fn v3(v: &acadrust::types::Vector3) -> [f64; 3] {
[v.x, v.y, v.z]
}
fn dvec3(v: &acadrust::types::Vector3) -> glam::DVec3 {
glam::DVec3::new(v.x, v.y, v.z)
}
fn v3f32(v: &acadrust::types::Vector3) -> [f32; 3] {
[v.x as f32, v.y as f32, v.z as f32]
}
impl RenderConvertible for Face3D {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
let p0 = v3(&self.first_corner);
let p1 = v3(&self.second_corner);
let p2 = v3(&self.third_corner);
let p3 = v3(&self.fourth_corner);
let p0f = v3f32(&self.first_corner);
let p1f = v3f32(&self.second_corner);
let p2f = v3f32(&self.third_corner);
let p3f = v3f32(&self.fourth_corner);
let inv = self.invisible_edges;
// Add edge as a line segment (separated by NaN from previous edges).
let mut pts: Vec<[f64; 3]> = Vec::new();
let mut add_edge = |a: [f64; 3], b: [f64; 3]| {
if !pts.is_empty() {
pts.push([f64::NAN; 3]);
}
pts.push(a);
pts.push(b);
};
if !inv.is_first_invisible() {
add_edge(p0, p1);
}
if !inv.is_second_invisible() {
add_edge(p1, p2);
}
if !inv.is_third_invisible() {
add_edge(p2, p3);
}
if !inv.is_fourth_invisible() {
add_edge(p3, p0);
}
if pts.is_empty() {
// All edges invisible — show a tiny cross at centroid.
let cx = (p0[0] + p1[0] + p2[0] + p3[0]) / 4.0;
let cy = (p0[1] + p1[1] + p2[1] + p3[1]) / 4.0;
let cz = (p0[2] + p1[2] + p2[2] + p3[2]) / 4.0;
let s = 0.1_f64;
pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]];
}
Some(RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::Lines(pts),
snap_pts: vec![
(Vec3::from(p0f).as_dvec3(), SnapHint::Node),
(Vec3::from(p1f).as_dvec3(), SnapHint::Node),
(Vec3::from(p2f).as_dvec3(), SnapHint::Node),
(Vec3::from(p3f).as_dvec3(), SnapHint::Node),
],
tangent_geoms: vec![],
key_vertices: vec![p0, p1, p2, p3],
fill_tris: vec![],
})
}
}
impl Grippable for Face3D {
fn grips(&self) -> Vec<GripDef> {
vec![
square_grip(0, dvec3(&self.first_corner)),
square_grip(1, dvec3(&self.second_corner)),
square_grip(2, dvec3(&self.third_corner)),
square_grip(3, dvec3(&self.fourth_corner)),
]
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
let corner = match grip_id {
0 => &mut self.first_corner,
1 => &mut self.second_corner,
2 => &mut self.third_corner,
3 => &mut self.fourth_corner,
_ => return,
};
match apply {
GripApply::Translate(d) => {
corner.x += d.x as f64;
corner.y += d.y as f64;
corner.z += d.z as f64;
}
GripApply::Absolute(p) => {
corner.x = p.x as f64;
corner.y = p.y as f64;
corner.z = p.z as f64;
}
}
}
}
impl PropertyEditable for Face3D {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
use crate::entities::common::edit_prop as edit;
let inv = self.invisible_edges;
let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" };
vec![PropSection {
title: t!("Geometry").into_owned(),
props: vec![
ro(t!("Current vertex").as_ref(), "f3_current", String::new()),
edit(t!("Vertex 1 X").as_ref(), "f3_p1x", self.first_corner.x),
edit(t!("Vertex 1 Y").as_ref(), "f3_p1y", self.first_corner.y),
edit(t!("Vertex 1 Z").as_ref(), "f3_p1z", self.first_corner.z),
edit(t!("Vertex 2 X").as_ref(), "f3_p2x", self.second_corner.x),
edit(t!("Vertex 2 Y").as_ref(), "f3_p2y", self.second_corner.y),
edit(t!("Vertex 2 Z").as_ref(), "f3_p2z", self.second_corner.z),
edit(t!("Vertex 3 X").as_ref(), "f3_p3x", self.third_corner.x),
edit(t!("Vertex 3 Y").as_ref(), "f3_p3y", self.third_corner.y),
edit(t!("Vertex 3 Z").as_ref(), "f3_p3z", self.third_corner.z),
edit(t!("Vertex 4 X").as_ref(), "f3_p4x", self.fourth_corner.x),
edit(t!("Vertex 4 Y").as_ref(), "f3_p4y", self.fourth_corner.y),
edit(t!("Vertex 4 Z").as_ref(), "f3_p4z", self.fourth_corner.z),
ro(t!("Edge 1").as_ref(), "f3_edge1", edge(inv.is_first_invisible())),
ro(t!("Edge 2").as_ref(), "f3_edge2", edge(inv.is_second_invisible())),
ro(t!("Edge 3").as_ref(), "f3_edge3", edge(inv.is_third_invisible())),
ro(t!("Edge 4").as_ref(), "f3_edge4", edge(inv.is_fourth_invisible())),
],
}]
}
fn apply_geom_prop(&mut self, field: &str, value: &str) {
let Ok(v) = value.trim().parse::<f64>() else {
return;
};
match field {
"f3_p1x" => self.first_corner.x = v,
"f3_p1y" => self.first_corner.y = v,
"f3_p1z" => self.first_corner.z = v,
"f3_p2x" => self.second_corner.x = v,
"f3_p2y" => self.second_corner.y = v,
"f3_p2z" => self.second_corner.z = v,
"f3_p3x" => self.third_corner.x = v,
"f3_p3y" => self.third_corner.y = v,
"f3_p3z" => self.third_corner.z = v,
"f3_p4x" => self.fourth_corner.x = v,
"f3_p4y" => self.fourth_corner.y = v,
"f3_p4z" => self.fourth_corner.z = v,
_ => {}
}
}
}
impl Transformable for Face3D {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for corner in [
&mut entity.first_corner,
&mut entity.second_corner,
&mut entity.third_corner,
&mut entity.fourth_corner,
] {
crate::scene::view::transform::reflect_xy_point(&mut corner.x, &mut corner.y, p1, p2);
}
});
}
}
// ── PolygonMesh (N×M grid) ────────────────────────────────────────────────────
impl RenderConvertible for PolygonMesh {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
let m = self.m_vertex_count as usize;
let n = self.n_vertex_count as usize;
if m == 0 || n == 0 || self.vertices.len() < m * n {
return None;
}
Some(RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::Lines(Vec::new()),
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
fill_tris: vec![],
})
}
}
impl Grippable for PolygonMesh {
fn grips(&self) -> Vec<GripDef> {
self.vertices
.iter()
.enumerate()
.map(|(i, v)| {
square_grip(
i,
glam::DVec3::new(v.location.x, v.location.y, v.location.z),
)
})
.collect()
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
if let Some(v) = self.vertices.get_mut(grip_id) {
match apply {
GripApply::Translate(d) => {
v.location.x += d.x as f64;
v.location.y += d.y as f64;
v.location.z += d.z as f64;
}
GripApply::Absolute(p) => {
v.location.x = p.x as f64;
v.location.y = p.y as f64;
v.location.z = p.z as f64;
}
}
}
}
}
impl PropertyEditable for PolygonMesh {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let smooth = match self.smooth_type {
acadrust::entities::polygon_mesh::SurfaceSmoothType::NoSmooth => "None",
acadrust::entities::polygon_mesh::SurfaceSmoothType::Quadratic => "Quadratic",
acadrust::entities::polygon_mesh::SurfaceSmoothType::Cubic => "Cubic",
acadrust::entities::polygon_mesh::SurfaceSmoothType::Bezier => "Bezier",
};
let yesno = |b: bool| if b { "Yes" } else { "No" };
let first = self.vertices.first();
// Grid faces: one quad per cell; closed direction adds a wrap row/column.
let m = self.m_vertex_count.max(0) as i64;
let n = self.n_vertex_count.max(0) as i64;
let cells_m = if self.is_closed_m() { m } else { (m - 1).max(0) };
let cells_n = if self.is_closed_n() { n } else { (n - 1).max(0) };
let face_count = cells_m * cells_n;
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
ro(t!("Vertex").as_ref(), "pm_vertex", String::new()),
ro(t!("Vertex X").as_ref(),
"pm_vx",
first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(),
),
ro(t!("Vertex Y").as_ref(),
"pm_vy",
first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(),
),
ro(t!("Vertex Z").as_ref(),
"pm_vz",
first.map(|v| format!("{:.4}", v.location.z)).unwrap_or_default(),
),
ro(t!("M vertex count").as_ref(), "pm_m", self.m_vertex_count.to_string()),
ro(t!("N vertex count").as_ref(), "pm_n", self.n_vertex_count.to_string()),
ro(t!("M closed").as_ref(), "pm_closed_m", yesno(self.is_closed_m())),
ro(t!("N closed").as_ref(), "pm_closed_n", yesno(self.is_closed_n())),
ro(t!("M density").as_ref(), "pm_smooth_m", self.m_smooth_density.to_string()),
ro(t!("N density").as_ref(), "pm_smooth_n", self.n_smooth_density.to_string()),
ro(t!("Vertex count").as_ref(), "pm_v", self.vertices.len().to_string()),
ro(t!("Face count").as_ref(), "pm_faces", face_count.to_string()),
],
},
PropSection {
title: t!("Misc").into_owned(),
props: vec![ro(t!("Fit/smooth").as_ref(), "pm_smooth", smooth)],
},
]
}
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
}
impl Transformable for PolygonMesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
});
}
}
// ── PolyfaceMesh (arbitrary faces with 1-based vertex indices) ────────────────
impl RenderConvertible for PolyfaceMesh {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
if self.vertices.is_empty() || self.faces.is_empty() {
return None;
}
Some(RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::Lines(Vec::new()),
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
fill_tris: vec![],
})
}
}
impl Grippable for PolyfaceMesh {
fn grips(&self) -> Vec<GripDef> {
self.vertices
.iter()
.enumerate()
.map(|(i, v)| {
square_grip(
i,
glam::DVec3::new(v.location.x, v.location.y, v.location.z),
)
})
.collect()
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
if let Some(v) = self.vertices.get_mut(grip_id) {
match apply {
GripApply::Translate(d) => {
v.location.x += d.x as f64;
v.location.y += d.y as f64;
v.location.z += d.z as f64;
}
GripApply::Absolute(p) => {
v.location.x = p.x as f64;
v.location.y = p.y as f64;
v.location.z = p.z as f64;
}
}
}
}
}
impl PropertyEditable for PolyfaceMesh {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let smooth = match self.smooth_surface {
acadrust::entities::PolyfaceSmoothType::None => "None",
acadrust::entities::PolyfaceSmoothType::Quadratic => "Quadratic",
acadrust::entities::PolyfaceSmoothType::Cubic => "Cubic",
acadrust::entities::PolyfaceSmoothType::Bezier => "Bezier",
};
let first = self.vertices.first();
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
ro(t!("Vertex").as_ref(), "pfm_vertex", String::new()),
ro(t!("Vertex X").as_ref(),
"pfm_vx",
first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(),
),
ro(t!("Vertex Y").as_ref(),
"pfm_vy",
first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(),
),
ro(t!("Vertex Z").as_ref(),
"pfm_vz",
first.map(|v| format!("{:.4}", v.location.z)).unwrap_or_default(),
),
// Polyface meshes store an explicit vertex/face list rather
// than an M×N grid, so the grid-only rows are not applicable.
ro(t!("M vertex count").as_ref(), "pfm_m", String::new()),
ro(t!("N vertex count").as_ref(), "pfm_n", String::new()),
ro(t!("M closed").as_ref(), "pfm_closed_m", String::new()),
ro(t!("N closed").as_ref(), "pfm_closed_n", String::new()),
ro(t!("M density").as_ref(), "pfm_density_m", String::new()),
ro(t!("N density").as_ref(), "pfm_density_n", String::new()),
ro(t!("Vertex count").as_ref(), "pfm_v", self.vertices.len().to_string()),
ro(t!("Face count").as_ref(), "pfm_f", self.faces.len().to_string()),
],
},
PropSection {
title: t!("Misc").into_owned(),
props: vec![ro(t!("Fit/smooth").as_ref(), "pfm_smooth", smooth)],
},
]
}
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
}
impl Transformable for PolyfaceMesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
});
}
}
// ── Mesh (SubD mesh) ──────────────────────────────────────────────────────────
//
// Modern subdivision mesh — distinct from PolygonMesh. The render path emits
// the refined per-edge wireframe and triangulates each face into fill_tris so
// solid views draw the same Catmull-Clark surface described by the DWG.
#[derive(Clone)]
struct RefinedMesh {
vertices: Vec<[f64; 3]>,
faces: Vec<Vec<usize>>,
creases: std::collections::HashMap<(usize, usize), f64>,
}
fn edge_key(a: usize, b: usize) -> (usize, usize) {
if a < b { (a, b) } else { (b, a) }
}
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn mul3(a: [f64; 3], scale: f64) -> [f64; 3] {
[a[0] * scale, a[1] * scale, a[2] * scale]
}
fn mix3(a: [f64; 3], b: [f64; 3], amount: f64) -> [f64; 3] {
add3(mul3(a, 1.0 - amount), mul3(b, amount))
}
fn mean_points<'a>(points: impl Iterator<Item = &'a [f64; 3]>) -> [f64; 3] {
let mut sum = [0.0; 3];
let mut count = 0usize;
for point in points {
sum = add3(sum, *point);
count += 1;
}
if count == 0 {
sum
} else {
mul3(sum, 1.0 / count as f64)
}
}
fn base_refined_mesh(mesh: &Mesh) -> RefinedMesh {
let vertices: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| [v.x, v.y, v.z]).collect();
let faces = mesh
.faces
.iter()
.filter_map(|face| {
let valid: Vec<usize> = face
.vertices
.iter()
.copied()
.filter(|&index| index < vertices.len())
.collect();
(valid.len() >= 3).then_some(valid)
})
.collect();
let creases = mesh
.edges
.iter()
.filter_map(|edge| {
let sharpness = edge.crease?;
(edge.start < vertices.len() && edge.end < vertices.len() && sharpness > 0.0)
.then_some((edge_key(edge.start, edge.end), sharpness))
})
.collect();
RefinedMesh {
vertices,
faces,
creases,
}
}
fn subdivide_catmull_clark(mesh: &RefinedMesh, blend_crease: bool) -> RefinedMesh {
use std::collections::{HashMap, HashSet};
if mesh.faces.is_empty() {
return mesh.clone();
}
let face_points: Vec<[f64; 3]> = mesh
.faces
.iter()
.map(|face| mean_points(face.iter().filter_map(|&index| mesh.vertices.get(index))))
.collect();
let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
let mut vertex_faces: Vec<Vec<usize>> = vec![Vec::new(); mesh.vertices.len()];
let mut vertex_edges: Vec<HashSet<(usize, usize)>> = vec![HashSet::new(); mesh.vertices.len()];
for (face_index, face) in mesh.faces.iter().enumerate() {
for &vertex in face {
if let Some(faces) = vertex_faces.get_mut(vertex) {
faces.push(face_index);
}
}
for corner in 0..face.len() {
let a = face[corner];
let b = face[(corner + 1) % face.len()];
if a == b {
continue;
}
let key = edge_key(a, b);
edge_faces.entry(key).or_default().push(face_index);
if let Some(edges) = vertex_edges.get_mut(a) {
edges.insert(key);
}
if let Some(edges) = vertex_edges.get_mut(b) {
edges.insert(key);
}
}
}
let mut vertex_points = Vec::with_capacity(mesh.vertices.len());
for (vertex_index, &point) in mesh.vertices.iter().enumerate() {
let incident_edges = &vertex_edges[vertex_index];
let incident_faces = &vertex_faces[vertex_index];
if incident_edges.is_empty() || incident_faces.is_empty() {
vertex_points.push(point);
continue;
}
let face_average =
mean_points(incident_faces.iter().filter_map(|&index| face_points.get(index)));
let edge_midpoints: Vec<[f64; 3]> = incident_edges
.iter()
.filter_map(|&(a, b)| {
let other = if a == vertex_index { b } else { a };
mesh.vertices
.get(other)
.map(|&p| mul3(add3(point, p), 0.5))
})
.collect();
let edge_average = mean_points(edge_midpoints.iter());
let n = incident_faces.len() as f64;
let smooth = mul3(
add3(add3(face_average, mul3(edge_average, 2.0)), mul3(point, n - 3.0)),
1.0 / n,
);
let mut sharp_neighbours: Vec<(f64, [f64; 3])> = incident_edges
.iter()
.filter_map(|&key| {
let boundary = edge_faces.get(&key).is_some_and(|faces| faces.len() == 1);
let sharpness = if boundary {
f64::INFINITY
} else {
mesh.creases.get(&key).copied().unwrap_or(0.0)
};
if sharpness <= 0.0 {
return None;
}
let other = if key.0 == vertex_index { key.1 } else { key.0 };
mesh.vertices.get(other).copied().map(|p| (sharpness, p))
})
.collect();
sharp_neighbours.sort_by(|a, b| b.0.total_cmp(&a.0));
let crease_point = match sharp_neighbours.as_slice() {
[(_, first), (_, second), ..] => mul3(
add3(add3(mul3(point, 6.0), *first), *second),
1.0 / 8.0,
),
_ => smooth,
};
let corner_point = if sharp_neighbours.len() >= 3 {
point
} else {
crease_point
};
let sharpness = sharp_neighbours
.get(1)
.map(|item| item.0)
.unwrap_or(0.0);
let amount = if blend_crease {
sharpness.clamp(0.0, 1.0)
} else if sharpness > 0.0 {
1.0
} else {
0.0
};
vertex_points.push(mix3(smooth, corner_point, amount));
}
let mut edge_point_indices = HashMap::with_capacity(edge_faces.len());
let mut vertices = vertex_points;
for (&key, adjacent_faces) in &edge_faces {
let Some((&a, &b)) = mesh.vertices.get(key.0).zip(mesh.vertices.get(key.1)) else {
continue;
};
let midpoint = mul3(add3(a, b), 0.5);
let smooth = if adjacent_faces.len() == 2 {
let f0 = face_points[adjacent_faces[0]];
let f1 = face_points[adjacent_faces[1]];
mul3(add3(add3(a, b), add3(f0, f1)), 0.25)
} else {
midpoint
};
let boundary = adjacent_faces.len() != 2;
let sharpness = mesh.creases.get(&key).copied().unwrap_or(0.0);
let amount = if boundary {
1.0
} else if blend_crease {
sharpness.clamp(0.0, 1.0)
} else if sharpness > 0.0 {
1.0
} else {
0.0
};
edge_point_indices.insert(key, vertices.len());
vertices.push(mix3(smooth, midpoint, amount));
}
let face_point_start = vertices.len();
vertices.extend(face_points.iter().copied());
let mut faces = Vec::new();
let mut creases = HashMap::new();
for (face_index, face) in mesh.faces.iter().enumerate() {
for corner in 0..face.len() {
let vertex = face[corner];
let next_key = edge_key(vertex, face[(corner + 1) % face.len()]);
let prev_key = edge_key(face[(corner + face.len() - 1) % face.len()], vertex);
let (Some(&next_edge), Some(&prev_edge)) = (
edge_point_indices.get(&next_key),
edge_point_indices.get(&prev_key),
) else {
continue;
};
faces.push(vec![
vertex,
next_edge,
face_point_start + face_index,
prev_edge,
]);
}
}
for (&key, &sharpness) in &mesh.creases {
let Some(&edge_point) = edge_point_indices.get(&key) else {
continue;
};
let child_sharpness = (sharpness - 1.0).max(0.0);
if child_sharpness > 0.0 {
creases.insert(edge_key(key.0, edge_point), child_sharpness);
creases.insert(edge_key(edge_point, key.1), child_sharpness);
}
}
RefinedMesh {
vertices,
faces,
creases,
}
}
fn display_mesh(mesh: &Mesh) -> RefinedMesh {
let mut refined = base_refined_mesh(mesh);
for _ in 0..mesh.subdivision_level.clamp(0, 4) {
refined = subdivide_catmull_clark(&refined, mesh.blend_crease);
}
refined
}
fn face_triangle_indices(
vertices: &[[f64; 3]],
faces: &[Vec<usize>],
) -> (Vec<u32>, Vec<usize>) {
let mut indices = Vec::new();
let mut triangle_faces = Vec::new();
for (face_index, face) in faces.iter().enumerate() {
let polygon: Vec<[f64; 3]> = face
.iter()
.filter_map(|&index| vertices.get(index).copied())
.collect();
if polygon.len() < 3 {
continue;
}
for triangle in triangulate_planar(&polygon).chunks_exact(3) {
let mut mapped = [0u32; 3];
let mut valid = true;
for corner in 0..3 {
let Some(local) = polygon.iter().position(|point| *point == triangle[corner]) else {
valid = false;
break;
};
mapped[corner] = face[local] as u32;
}
if valid && mapped[0] != mapped[1] && mapped[1] != mapped[2] && mapped[2] != mapped[0] {
indices.extend(mapped);
triangle_faces.push(face_index);
}
}
}
(indices, triangle_faces)
}
fn vertex_normals(vertices: &[[f64; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
let mut normals = vec![[0.0f64; 3]; vertices.len()];
for triangle in indices.chunks_exact(3) {
let (Some(&a), Some(&b), Some(&c)) = (
vertices.get(triangle[0] as usize),
vertices.get(triangle[1] as usize),
vertices.get(triangle[2] as usize),
) else {
continue;
};
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let normal = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
for &index in triangle {
if let Some(sum) = normals.get_mut(index as usize) {
*sum = add3(*sum, normal);
}
}
}
normals
.into_iter()
.map(|normal| {
let length =
(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
if length > 1e-15 {
[
(normal[0] / length) as f32,
(normal[1] / length) as f32,
(normal[2] / length) as f32,
]
} else {
[0.0, 0.0, 1.0]
}
})
.collect()
}
fn append_feature_edge(
edge_verts: &mut Vec<[f32; 3]>,
edge_verts_low: &mut Vec<[f32; 3]>,
vertices: &[[f64; 3]],
a: usize,
b: usize,
) {
for index in [a, b] {
let Some(&[x, y, z]) = vertices.get(index) else {
continue;
};
let high = [x as f32, y as f32, z as f32];
edge_verts.push(high);
edge_verts_low.push([
(x - high[0] as f64) as f32,
(y - high[1] as f64) as f32,
(z - high[2] as f64) as f32,
]);
}
}
fn make_mesh_lod_set(
name: String,
color: [f32; 4],
vertices: Vec<[f64; 3]>,
faces: Vec<Vec<usize>>,
visible_edges: std::collections::HashSet<(usize, usize)>,
face_colors: &[Option<[f32; 4]>],
) -> Option<crate::scene::model::mesh_model::MeshLodSet> {
let (indices, triangle_faces) = face_triangle_indices(&vertices, &faces);
if indices.is_empty() {
return None;
}
let normals = vertex_normals(&vertices, &indices);
let triangle_colors = triangle_faces
.iter()
.map(|&face| face_colors.get(face).copied().flatten())
.collect();
let model = crate::scene::convert::solid3d_tess::finalize_mesh(
name,
vertices.clone(),
normals,
indices,
Vec::new(),
triangle_colors,
color,
None,
);
let mut set = crate::scene::model::mesh_model::MeshLodSet::from_lods(vec![model]);
for (a, b) in visible_edges {
append_feature_edge(
&mut set.edge_verts,
&mut set.edge_verts_low,
&vertices,
a,
b,
);
}
Some(set)
}
/// Convert every standard DWG mesh family to the material-aware shaded mesh
/// pipeline. The retained wire entity carries snaps/grips only; fill, normals,
/// depth, materials and face colours live in `MeshLodSet`.
pub(crate) fn tessellate_shaded_mesh(
entity: &acadrust::EntityType,
color: [f32; 4],
) -> Option<crate::scene::model::mesh_model::MeshLodSet> {
use std::collections::HashSet;
match entity {
acadrust::EntityType::Mesh(mesh) => {
let display = display_mesh(mesh);
let mut edges = HashSet::new();
for face in &display.faces {
for corner in 0..face.len() {
edges.insert(edge_key(face[corner], face[(corner + 1) % face.len()]));
}
}
make_mesh_lod_set(
mesh.common.handle.value().to_string(),
color,
display.vertices,
display.faces,
edges,
&[],
)
}
acadrust::EntityType::PolygonMesh(mesh) => {
let m = mesh.m_vertex_count.max(0) as usize;
let n = mesh.n_vertex_count.max(0) as usize;
if m == 0 || n == 0 || mesh.vertices.len() < m.saturating_mul(n) {
return None;
}
let vertices: Vec<[f64; 3]> = mesh
.vertices
.iter()
.take(m * n)
.map(|vertex| {
[
vertex.location.x,
vertex.location.y,
vertex.location.z,
]
})
.collect();
let closed_m = mesh.is_closed_m();
let closed_n = mesh.is_closed_n();
let m_cells = if closed_m { m } else { m.saturating_sub(1) };
let n_cells = if closed_n { n } else { n.saturating_sub(1) };
let mut faces = Vec::with_capacity(m_cells.saturating_mul(n_cells));
let mut edges = HashSet::new();
for i in 0..m_cells {
for j in 0..n_cells {
let face = vec![
i * n + j,
((i + 1) % m) * n + j,
((i + 1) % m) * n + (j + 1) % n,
i * n + (j + 1) % n,
];
for corner in 0..4 {
edges.insert(edge_key(face[corner], face[(corner + 1) % 4]));
}
faces.push(face);
}
}
make_mesh_lod_set(
mesh.common.handle.value().to_string(),
color,
vertices,
faces,
edges,
&[],
)
}
acadrust::EntityType::PolyfaceMesh(mesh) => {
let vertices: Vec<[f64; 3]> = mesh
.vertices
.iter()
.map(|vertex| {
[
vertex.location.x,
vertex.location.y,
vertex.location.z,
]
})
.collect();
let mut faces = Vec::new();
let mut face_colors = Vec::new();
let mut edges = HashSet::new();
for face in &mesh.faces {
let raw = [face.index1, face.index2, face.index3, face.index4];
let indices: Vec<usize> = raw
.iter()
.filter(|&&index| index != 0)
.filter_map(|&index| (index.unsigned_abs() as usize).checked_sub(1))
.filter(|&index| index < vertices.len())
.collect();
if indices.len() < 3 {
continue;
}
for corner in 0..indices.len() {
if raw[corner] > 0 {
edges.insert(edge_key(indices[corner], indices[(corner + 1) % indices.len()]));
}
}
faces.push(indices);
face_colors.push(face.color.as_ref().map(crate::scene::convert::tess_util::aci_to_rgba));
}
make_mesh_lod_set(
mesh.common.handle.value().to_string(),
color,
vertices,
faces,
edges,
&face_colors,
)
}
_ => None,
}
}
impl RenderConvertible for Mesh {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
if self.vertices.is_empty() {
return None;
}
// Per-vertex snap tables cost ~56 B/vertex and are retained in every
// copy of the wire set. On NWD-scale imports (#358: tens of millions
// of mesh vertices) that is GBs for snap targets far too dense to
// pick apart on screen — so past this size ship none, which is what
// PolyfaceMesh / PolygonMesh already do at any size.
const SNAP_TABLE_MAX_VERTICES: usize = 50_000;
let (snap_pts, key_vertices) = if self.vertices.len() > SNAP_TABLE_MAX_VERTICES {
(Vec::new(), Vec::new())
} else {
(
self.vertices
.iter()
.map(|v| (glam::DVec3::new(v.x, v.y, v.z), SnapHint::Node))
.collect(),
self.vertices.iter().map(|v| [v.x, v.y, v.z]).collect(),
)
};
Some(RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::Lines(Vec::new()),
snap_pts,
tangent_geoms: vec![],
key_vertices,
fill_tris: vec![],
})
}
}
impl Grippable for Mesh {
fn grips(&self) -> Vec<GripDef> {
self.vertices
.iter()
.enumerate()
.map(|(i, v)| square_grip(i, glam::DVec3::new(v.x, v.y, v.z)))
.collect()
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
if let Some(v) = self.vertices.get_mut(grip_id) {
match apply {
GripApply::Translate(d) => {
v.x += d.x as f64;
v.y += d.y as f64;
v.z += d.z as f64;
}
GripApply::Absolute(p) => {
v.x = p.x as f64;
v.y = p.y as f64;
v.z = p.z as f64;
}
}
}
}
}
impl PropertyEditable for Mesh {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
// Watertight when every face edge is shared by exactly two faces
// (closed manifold). Empty meshes are not watertight.
let mut edge_use: std::collections::HashMap<(usize, usize), u32> =
std::collections::HashMap::new();
for face in &self.faces {
let vs = &face.vertices;
for i in 0..vs.len() {
let a = vs[i];
let b = vs[(i + 1) % vs.len()];
let key = if a < b { (a, b) } else { (b, a) };
*edge_use.entry(key).or_insert(0) += 1;
}
}
let watertight =
!self.faces.is_empty() && edge_use.values().all(|&c| c == 2);
vec![PropSection {
title: t!("Geometry").into_owned(),
props: vec![
Property {
label: t!("Level of Smoothness").into_owned(),
field: "msh_subdiv_edit",
value: PropValue::EditText(self.subdivision_level.to_string()),
},
Property {
label: t!("Blend Creases").into_owned(),
field: "msh_blend_crease",
value: PropValue::BoolToggle {
field: "msh_blend_crease",
value: self.blend_crease,
},
},
ro(t!("Number of Faces").as_ref(), "msh_f", self.faces.len().to_string()),
ro(t!("Number of Vertices").as_ref(), "msh_v", self.vertices.len().to_string()),
ro(t!("Number of Edges").as_ref(), "msh_e", self.edges.len().to_string()),
ro(t!("Creased Edges").as_ref(),
"msh_creased",
self.edges
.iter()
.filter(|edge| edge.crease.is_some_and(|value| value > 0.0))
.count()
.to_string(),
),
ro(t!("Override Option").as_ref(),
"msh_override",
self.override_option.to_string(),
),
ro(t!("Number of Grips").as_ref(), "msh_grips", self.vertices.len().to_string()),
ro(t!("Watertight").as_ref(),
"msh_watertight",
if watertight { "Yes" } else { "No" },
),
],
}]
}
fn apply_geom_prop(&mut self, field: &str, value: &str) {
match field {
"msh_subdiv_edit" => {
if let Some(value) = parse_f64(value) {
self.subdivision_level = (value.round() as i32).clamp(0, 4);
}
}
"msh_blend_crease" => {
self.blend_crease = match value {
"toggle" => !self.blend_crease,
"On" | "Yes" | "true" | "1" => true,
"Off" | "No" | "false" | "0" => false,
_ => self.blend_crease,
};
}
_ => {}
}
}
}
impl Transformable for Mesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
}
});
}
}