Files
OneCAD-Draft/src/entities/mesh.rs
T
Hakan SevenandClaude Opus 4.8 25715d1b03 fix(grips): compute grip positions in f64 so they sit on the wire
Grip markers drew visibly offset from the entity on large-coordinate
(UTM-scale) drawings: every Grippable impl cast the f64 entity coordinates
to f32 BEFORE the world-offset subtraction, and at 1e7 magnitude an f32
holds ~0.9 drawing units of rounding error. The wires subtract the offset
in f64 first (precision-preserving), so the grips landed up to ~1 unit
away from the geometry they belong to.

GripDef.world is now DVec3: producers fill it straight from the f64
entity data, refresh_selected_grips subtracts the world offset in f64,
and only the small offset-local result is cast to f32 for screen-space
projection (grips_to_screen / find_hit_grip) and drag state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:04:37 +03:00

641 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 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 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, 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]) -> 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,
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]) -> 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,
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]) -> 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, 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]) -> 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);
}
});
}
}