Files
OneCAD-Draft/src/entities/mesh.rs
T
Hakan SevenandClaude Opus 4.7 6110da99a5 feat(entities): surface remaining 3D fields + add Mesh render path
Closes out the 3D-entity audit. Mesh (modern SubD mesh) gained a full
render impl since it had no TruckConvertible / Grippable / PropertyEditable
hookup at all — it was silently dropped on open. Remaining mesh-style
entities pick up the few fields the parser populated:

- Mesh (SubD): wireframe + face-fan fill_tris rendering, per-vertex grips,
  Properties panel surfaces version, subdivision_level, blend_crease,
  creased-edge count, vertex / face / edge counts; wired into the
  Transformable / Grippable / PropertyEditable / TruckConvertible
  dispatch in entities/traits.rs
- Polyline3D: smooth_type, default_start_width, default_end_width,
  mesh_m_count, mesh_n_count, smooth_m_density, smooth_n_density
  (widths editable)
- PolygonMesh: smooth_type, m/n_smooth_density, elevation, normal
- PolyfaceMesh: smooth_surface, seqend_handle, elevation, normal,
  thickness

Solid3D / Region / Body's uid + silhouettes + history_handle were
included in the prior 2D commit since they share the same file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:35:14 +03:00

632 lines
22 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::command::EntityTransform;
use crate::entities::common::{ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::object::{GripApply, GripDef, PropSection};
use crate::scene::wire_model::SnapHint;
// ── Face3D ────────────────────────────────────────────────────────────────────
fn v3(v: &acadrust::types::Vector3) -> [f64; 3] {
[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 TruckConvertible for Face3D {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
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(TruckEntity {
object: TruckObject::Lines(pts),
snap_pts: vec![
(Vec3::from(p0f), SnapHint::Node),
(Vec3::from(p1f), SnapHint::Node),
(Vec3::from(p2f), SnapHint::Node),
(Vec3::from(p3f), 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, Vec3::from(v3f32(&self.first_corner))),
square_grip(1, Vec3::from(v3f32(&self.second_corner))),
square_grip(2, Vec3::from(v3f32(&self.third_corner))),
square_grip(3, Vec3::from(v3f32(&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]) -> PropSection {
use crate::entities::common::edit_prop as edit;
PropSection {
title: "Geometry".into(),
props: vec![
edit("P1 X", "f3_p1x", self.first_corner.x),
edit("P1 Y", "f3_p1y", self.first_corner.y),
edit("P1 Z", "f3_p1z", self.first_corner.z),
edit("P2 X", "f3_p2x", self.second_corner.x),
edit("P2 Y", "f3_p2y", self.second_corner.y),
edit("P2 Z", "f3_p2z", self.second_corner.z),
edit("P3 X", "f3_p3x", self.third_corner.x),
edit("P3 Y", "f3_p3y", self.third_corner.y),
edit("P3 Z", "f3_p3z", self.third_corner.z),
edit("P4 X", "f3_p4x", self.fourth_corner.x),
edit("P4 Y", "f3_p4y", self.fourth_corner.y),
edit("P4 Z", "f3_p4z", self.fourth_corner.z),
],
}
}
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::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::transform::reflect_xy_point(&mut corner.x, &mut corner.y, p1, p2);
}
});
}
}
// ── PolygonMesh (N×M grid) ────────────────────────────────────────────────────
impl TruckConvertible for PolygonMesh {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
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;
}
let closed_m = self
.flags
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_M);
let closed_n = self
.flags
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_N);
let pt = |i: usize, j: usize| -> [f64; 3] {
let v = &self.vertices[i * n + j];
[v.location.x, v.location.y, v.location.z]
};
let mut pts: Vec<[f64; 3]> = Vec::new();
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
// Rows (M direction).
for i in 0..m {
pts.push([f64::NAN; 3]);
for j in 0..n {
pts.push(pt(i, j));
}
if closed_n {
pts.push(pt(i, 0));
}
}
// Columns (N direction).
for j in 0..n {
pts.push([f64::NAN; 3]);
for i in 0..m {
pts.push(pt(i, j));
}
if closed_m {
pts.push(pt(0, j));
}
}
// Fill: triangulate each grid quad (two triangles per cell).
let mi = if closed_m { m } else { m - 1 };
let ni = if closed_n { n } else { n - 1 };
for i in 0..mi {
for j in 0..ni {
let p00 = pt(i, j);
let p10 = pt((i + 1) % m, j);
let p01 = pt(i, (j + 1) % n);
let p11 = pt((i + 1) % m, (j + 1) % n);
fill_tris.extend_from_slice(&[p00, p10, p11, p00, p11, p01]);
}
}
Some(TruckEntity {
object: TruckObject::Lines(pts),
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
fill_tris,
})
}
}
impl Grippable for PolygonMesh {
fn grips(&self) -> Vec<GripDef> {
self.vertices
.iter()
.enumerate()
.map(|(i, v)| {
square_grip(
i,
Vec3::new(
v.location.x as f32,
v.location.y as f32,
v.location.z as f32,
),
)
})
.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]) -> 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",
};
PropSection {
title: "Geometry".into(),
props: vec![
ro("M count", "pm_m", self.m_vertex_count.to_string()),
ro("N count", "pm_n", self.n_vertex_count.to_string()),
ro("Vertices", "pm_v", self.vertices.len().to_string()),
ro("Smooth Type", "pm_smooth", smooth),
ro(
"Smooth M Density",
"pm_smooth_m",
self.m_smooth_density.to_string(),
),
ro(
"Smooth N Density",
"pm_smooth_n",
self.n_smooth_density.to_string(),
),
ro("Elevation", "pm_elevation", format!("{:.4}", self.elevation)),
ro(
"Normal",
"pm_normal",
format!(
"{:.3}, {:.3}, {:.3}",
self.normal.x, self.normal.y, self.normal.z
),
),
],
}
}
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
}
impl Transformable for PolygonMesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::transform::reflect_xy_point(
&mut v.location.x,
&mut v.location.y,
p1,
p2,
);
}
});
}
}
// ── PolyfaceMesh (arbitrary faces with 1-based vertex indices) ────────────────
impl TruckConvertible for PolyfaceMesh {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
if self.vertices.is_empty() || self.faces.is_empty() {
return None;
}
let get_v = |idx: i16| -> Option<[f64; 3]> {
let i = (idx.abs() as usize).checked_sub(1)?;
let v = self.vertices.get(i)?;
Some([v.location.x, v.location.y, v.location.z])
};
let mut pts: Vec<[f64; 3]> = Vec::new();
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
for face in &self.faces {
// Indices: 0 means unused. Negative = invisible edge (still render for wireframe).
let indices = [face.index1, face.index2, face.index3, face.index4];
let verts: Vec<[f64; 3]> = indices
.iter()
.filter(|&&i| i != 0)
.filter_map(|&i| get_v(i))
.collect();
if verts.len() < 2 {
continue;
}
pts.push([f64::NAN; 3]);
for &p in &verts {
pts.push(p);
}
// Close the face polygon.
pts.push(verts[0]);
// Fan-triangulate the face for solid fill.
if verts.len() >= 3 {
for i in 1..(verts.len() - 1) {
fill_tris.push(verts[0]);
fill_tris.push(verts[i]);
fill_tris.push(verts[i + 1]);
}
}
}
Some(TruckEntity {
object: TruckObject::Lines(pts),
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
fill_tris,
})
}
}
impl Grippable for PolyfaceMesh {
fn grips(&self) -> Vec<GripDef> {
self.vertices
.iter()
.enumerate()
.map(|(i, v)| {
square_grip(
i,
Vec3::new(
v.location.x as f32,
v.location.y as f32,
v.location.z as f32,
),
)
})
.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]) -> 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",
};
PropSection {
title: "Geometry".into(),
props: vec![
ro("Vertices", "pfm_v", self.vertices.len().to_string()),
ro("Faces", "pfm_f", self.faces.len().to_string()),
ro("Smooth Surface", "pfm_smooth", smooth),
ro(
"Seqend",
"pfm_seqend_handle",
match self.seqend_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
},
),
ro("Elevation", "pfm_elevation", format!("{:.4}", self.elevation)),
ro(
"Normal",
"pfm_normal",
format!(
"{:.3}, {:.3}, {:.3}",
self.normal.x, self.normal.y, self.normal.z
),
),
ro("Thickness", "pfm_thickness", format!("{:.4}", self.thickness)),
],
}
}
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
}
impl Transformable for PolyfaceMesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::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 file's per-edge wireframe and triangulates each face into fill_tris so
// solid views still draw a shaded surface. Subdivision-level smoothing is
// honoured only as metadata; we don't run a Catmull-Clark refinement pass
// here yet.
impl TruckConvertible for Mesh {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
if self.vertices.is_empty() {
return None;
}
let get = |i: usize| -> Option<[f64; 3]> {
self.vertices.get(i).map(|v| [v.x, v.y, v.z])
};
let mut pts: Vec<[f64; 3]> = Vec::new();
if !self.edges.is_empty() {
for edge in &self.edges {
if let (Some(a), Some(b)) = (get(edge.start), get(edge.end)) {
pts.push([f64::NAN; 3]);
pts.push(a);
pts.push(b);
}
}
} else {
for face in &self.faces {
if face.vertices.len() < 2 {
continue;
}
pts.push([f64::NAN; 3]);
for &vi in &face.vertices {
if let Some(p) = get(vi) {
pts.push(p);
}
}
if let Some(first) = face.vertices.first().and_then(|&i| get(i)) {
pts.push(first);
}
}
}
// Fan-triangulate each face into fill_tris so shaded views render
// the mesh as a solid surface.
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
for face in &self.faces {
if face.vertices.len() < 3 {
continue;
}
let v0 = match get(face.vertices[0]) {
Some(p) => p,
None => continue,
};
for tri in 1..(face.vertices.len() - 1) {
let v1 = match get(face.vertices[tri]) {
Some(p) => p,
None => continue,
};
let v2 = match get(face.vertices[tri + 1]) {
Some(p) => p,
None => continue,
};
fill_tris.push(v0);
fill_tris.push(v1);
fill_tris.push(v2);
}
}
let snap_pts: Vec<(Vec3, SnapHint)> = self
.vertices
.iter()
.map(|v| (Vec3::new(v.x as f32, v.y as f32, v.z as f32), SnapHint::Node))
.collect();
let key_vertices: Vec<[f64; 3]> = self.vertices.iter().map(|v| [v.x, v.y, v.z]).collect();
Some(TruckEntity {
object: TruckObject::Lines(pts),
snap_pts,
tangent_geoms: vec![],
key_vertices,
fill_tris,
})
}
}
impl Grippable for Mesh {
fn grips(&self) -> Vec<GripDef> {
self.vertices
.iter()
.enumerate()
.map(|(i, v)| {
square_grip(i, Vec3::new(v.x as f32, v.y as f32, v.z as f32))
})
.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]) -> PropSection {
let creased_edges = self.edges.iter().filter(|e| e.has_crease()).count();
PropSection {
title: "Geometry".into(),
props: vec![
ro("Vertices", "msh_v", self.vertices.len().to_string()),
ro("Faces", "msh_f", self.faces.len().to_string()),
ro("Edges", "msh_e", self.edges.len().to_string()),
ro("Creased Edges", "msh_creased", creased_edges.to_string()),
ro("Version", "msh_version", self.version.to_string()),
ro(
"Subdivision Level",
"msh_subdiv",
self.subdivision_level.to_string(),
),
ro(
"Blend Crease",
"msh_blend_crease",
if self.blend_crease { "Yes" } else { "No" },
),
],
}
}
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
}
impl Transformable for Mesh {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
for v in &mut entity.vertices {
crate::scene::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
}
});
}
}