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
|
|
|
|
use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, PolyfaceMesh};
|
2026-04-02 19:13:37 +03:00
|
|
|
|
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};
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
|
|
|
|
|
|
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
|
|
|
|
|
use crate::scene::model::wire_model::SnapHint;
|
2026-04-02 19:13:37 +03:00
|
|
|
|
|
2026-07-04 23:01:59 +03:00
|
|
|
|
/// 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.
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 19:13:37 +03:00
|
|
|
|
// ── Face3D ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
fn v3(v: &acadrust::types::Vector3) -> [f64; 3] {
|
|
|
|
|
|
[v.x, v.y, v.z]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 00:04:37 +03:00
|
|
|
|
fn dvec3(v: &acadrust::types::Vector3) -> glam::DVec3 {
|
|
|
|
|
|
glam::DVec3::new(v.x, v.y, v.z)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
fn v3f32(v: &acadrust::types::Vector3) -> [f32; 3] {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
[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);
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let p0f = v3f32(&self.first_corner);
|
|
|
|
|
|
let p1f = v3f32(&self.second_corner);
|
|
|
|
|
|
let p2f = v3f32(&self.third_corner);
|
|
|
|
|
|
let p3f = v3f32(&self.fourth_corner);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
let inv = self.invisible_edges;
|
|
|
|
|
|
|
|
|
|
|
|
// Add edge as a line segment (separated by NaN from previous edges).
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let mut pts: Vec<[f64; 3]> = Vec::new();
|
|
|
|
|
|
let mut add_edge = |a: [f64; 3], b: [f64; 3]| {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
if !pts.is_empty() {
|
2026-05-12 23:47:40 +03:00
|
|
|
|
pts.push([f64::NAN; 3]);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let s = 0.1_f64;
|
2026-04-02 19:13:37 +03:00
|
|
|
|
pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Some(TruckEntity {
|
|
|
|
|
|
object: TruckObject::Lines(pts),
|
2026-05-06 10:00:37 +03:00
|
|
|
|
snap_pts: vec![
|
refactor(snap): producers emit f64 snap points (world_offset removal P3)
TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 22:10:29 +03:00
|
|
|
|
(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),
|
2026-05-06 10:00:37 +03:00
|
|
|
|
],
|
2026-04-02 19:13:37 +03:00
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![p0, p1, p2, p3],
|
2026-05-06 23:24:26 +03:00
|
|
|
|
fill_tris: vec![],
|
2026-04-02 19:13:37 +03:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Grippable for Face3D {
|
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
|
|
|
|
vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
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)),
|
2026-04-02 19:13:37 +03:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-07-02 22:46:19 +03:00
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
use crate::entities::common::edit_prop as edit;
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
let inv = self.invisible_edges;
|
|
|
|
|
|
let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" };
|
2026-07-02 22:46:19 +03:00
|
|
|
|
vec![PropSection {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
|
props: vec![
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
ro("Current vertex", "f3_current", String::new()),
|
|
|
|
|
|
edit("Vertex 1 X", "f3_p1x", self.first_corner.x),
|
|
|
|
|
|
edit("Vertex 1 Y", "f3_p1y", self.first_corner.y),
|
|
|
|
|
|
edit("Vertex 1 Z", "f3_p1z", self.first_corner.z),
|
|
|
|
|
|
edit("Vertex 2 X", "f3_p2x", self.second_corner.x),
|
|
|
|
|
|
edit("Vertex 2 Y", "f3_p2y", self.second_corner.y),
|
|
|
|
|
|
edit("Vertex 2 Z", "f3_p2z", self.second_corner.z),
|
|
|
|
|
|
edit("Vertex 3 X", "f3_p3x", self.third_corner.x),
|
|
|
|
|
|
edit("Vertex 3 Y", "f3_p3y", self.third_corner.y),
|
|
|
|
|
|
edit("Vertex 3 Z", "f3_p3z", self.third_corner.z),
|
|
|
|
|
|
edit("Vertex 4 X", "f3_p4x", self.fourth_corner.x),
|
|
|
|
|
|
edit("Vertex 4 Y", "f3_p4y", self.fourth_corner.y),
|
|
|
|
|
|
edit("Vertex 4 Z", "f3_p4z", self.fourth_corner.z),
|
|
|
|
|
|
ro("Edge 1", "f3_edge1", edge(inv.is_first_invisible())),
|
|
|
|
|
|
ro("Edge 2", "f3_edge2", edge(inv.is_second_invisible())),
|
|
|
|
|
|
ro("Edge 3", "f3_edge3", edge(inv.is_third_invisible())),
|
|
|
|
|
|
ro("Edge 4", "f3_edge4", edge(inv.is_fourth_invisible())),
|
2026-04-02 19:13:37 +03:00
|
|
|
|
],
|
2026-07-02 22:46:19 +03:00
|
|
|
|
}]
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let Ok(v) = value.trim().parse::<f64>() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
2026-04-02 19:13:37 +03:00
|
|
|
|
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) {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
for corner in [
|
|
|
|
|
|
&mut entity.first_corner,
|
|
|
|
|
|
&mut entity.second_corner,
|
|
|
|
|
|
&mut entity.third_corner,
|
|
|
|
|
|
&mut entity.fourth_corner,
|
|
|
|
|
|
] {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(&mut corner.x, &mut corner.y, p1, p2);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let closed_m = self
|
|
|
|
|
|
.flags
|
|
|
|
|
|
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_M);
|
|
|
|
|
|
let closed_n = self
|
|
|
|
|
|
.flags
|
|
|
|
|
|
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_N);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let pt = |i: usize, j: usize| -> [f64; 3] {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
let v = &self.vertices[i * n + j];
|
2026-05-12 23:47:40 +03:00
|
|
|
|
[v.location.x, v.location.y, v.location.z]
|
2026-04-02 19:13:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let mut pts: Vec<[f64; 3]> = Vec::new();
|
|
|
|
|
|
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
2026-04-02 19:13:37 +03:00
|
|
|
|
|
|
|
|
|
|
// Rows (M direction).
|
|
|
|
|
|
for i in 0..m {
|
2026-05-12 23:47:40 +03:00
|
|
|
|
pts.push([f64::NAN; 3]);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
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 {
|
2026-05-12 23:47:40 +03:00
|
|
|
|
pts.push([f64::NAN; 3]);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
for i in 0..m {
|
|
|
|
|
|
pts.push(pt(i, j));
|
|
|
|
|
|
}
|
|
|
|
|
|
if closed_m {
|
|
|
|
|
|
pts.push(pt(0, j));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-06 23:24:26 +03:00
|
|
|
|
|
|
|
|
|
|
// 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]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 19:13:37 +03:00
|
|
|
|
|
|
|
|
|
|
Some(TruckEntity {
|
|
|
|
|
|
object: TruckObject::Lines(pts),
|
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
2026-05-06 23:24:26 +03:00
|
|
|
|
fill_tris,
|
2026-04-02 19:13:37 +03:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Grippable for PolygonMesh {
|
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
|
|
|
|
self.vertices
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.enumerate()
|
|
|
|
|
|
.map(|(i, v)| {
|
|
|
|
|
|
square_grip(
|
|
|
|
|
|
i,
|
2026-06-11 00:04:37 +03:00
|
|
|
|
glam::DVec3::new(v.location.x, v.location.y, v.location.z),
|
2026-04-02 19:13:37 +03:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.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 {
|
2026-07-02 22:46:19 +03:00
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
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
|
|
|
|
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",
|
|
|
|
|
|
};
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
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: "Geometry".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Vertex", "pm_vertex", String::new()),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Vertex X",
|
|
|
|
|
|
"pm_vx",
|
|
|
|
|
|
first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(),
|
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
|
|
|
|
),
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
ro(
|
|
|
|
|
|
"Vertex Y",
|
|
|
|
|
|
"pm_vy",
|
|
|
|
|
|
first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Vertex Z",
|
|
|
|
|
|
"pm_vz",
|
|
|
|
|
|
first.map(|v| format!("{:.4}", v.location.z)).unwrap_or_default(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro("M vertex count", "pm_m", self.m_vertex_count.to_string()),
|
|
|
|
|
|
ro("N vertex count", "pm_n", self.n_vertex_count.to_string()),
|
|
|
|
|
|
ro("M closed", "pm_closed_m", yesno(self.is_closed_m())),
|
|
|
|
|
|
ro("N closed", "pm_closed_n", yesno(self.is_closed_n())),
|
|
|
|
|
|
ro("M density", "pm_smooth_m", self.m_smooth_density.to_string()),
|
|
|
|
|
|
ro("N density", "pm_smooth_n", self.n_smooth_density.to_string()),
|
|
|
|
|
|
ro("Vertex count", "pm_v", self.vertices.len().to_string()),
|
|
|
|
|
|
ro("Face count", "pm_faces", face_count.to_string()),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Misc".into(),
|
|
|
|
|
|
props: vec![ro("Fit/smooth", "pm_smooth", smooth)],
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Transformable for PolygonMesh {
|
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
for v in &mut entity.vertices {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
2026-04-02 19:13:37 +03:00
|
|
|
|
&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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let get_v = |idx: i16| -> Option<[f64; 3]> {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
let i = (idx.abs() as usize).checked_sub(1)?;
|
|
|
|
|
|
let v = self.vertices.get(i)?;
|
2026-05-12 23:47:40 +03:00
|
|
|
|
Some([v.location.x, v.location.y, v.location.z])
|
2026-04-02 19:13:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let mut pts: Vec<[f64; 3]> = Vec::new();
|
|
|
|
|
|
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
2026-04-02 19:13:37 +03:00
|
|
|
|
|
|
|
|
|
|
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];
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let verts: Vec<[f64; 3]> = indices
|
2026-04-02 19:13:37 +03:00
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|&&i| i != 0)
|
|
|
|
|
|
.filter_map(|&i| get_v(i))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
if verts.len() < 2 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-05-12 23:47:40 +03:00
|
|
|
|
pts.push([f64::NAN; 3]);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
for &p in &verts {
|
|
|
|
|
|
pts.push(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Close the face polygon.
|
|
|
|
|
|
pts.push(verts[0]);
|
2026-05-06 23:24:26 +03:00
|
|
|
|
|
2026-07-04 23:01:59 +03:00
|
|
|
|
// Ear-clip the face for solid fill (handles concave faces).
|
2026-05-06 23:24:26 +03:00
|
|
|
|
if verts.len() >= 3 {
|
2026-07-04 23:01:59 +03:00
|
|
|
|
fill_tris.extend(triangulate_planar(&verts));
|
2026-05-06 23:24:26 +03:00
|
|
|
|
}
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Some(TruckEntity {
|
|
|
|
|
|
object: TruckObject::Lines(pts),
|
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
2026-05-06 23:24:26 +03:00
|
|
|
|
fill_tris,
|
2026-04-02 19:13:37 +03:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Grippable for PolyfaceMesh {
|
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
|
|
|
|
self.vertices
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.enumerate()
|
|
|
|
|
|
.map(|(i, v)| {
|
|
|
|
|
|
square_grip(
|
|
|
|
|
|
i,
|
2026-06-11 00:04:37 +03:00
|
|
|
|
glam::DVec3::new(v.location.x, v.location.y, v.location.z),
|
2026-04-02 19:13:37 +03:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.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 {
|
2026-07-02 22:46:19 +03:00
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
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
|
|
|
|
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",
|
|
|
|
|
|
};
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
let first = self.vertices.first();
|
|
|
|
|
|
vec![
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Vertex", "pfm_vertex", String::new()),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Vertex X",
|
|
|
|
|
|
"pfm_vx",
|
|
|
|
|
|
first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(),
|
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
|
|
|
|
),
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
ro(
|
|
|
|
|
|
"Vertex Y",
|
|
|
|
|
|
"pfm_vy",
|
|
|
|
|
|
first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Vertex Z",
|
|
|
|
|
|
"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("M vertex count", "pfm_m", String::new()),
|
|
|
|
|
|
ro("N vertex count", "pfm_n", String::new()),
|
|
|
|
|
|
ro("M closed", "pfm_closed_m", String::new()),
|
|
|
|
|
|
ro("N closed", "pfm_closed_n", String::new()),
|
|
|
|
|
|
ro("M density", "pfm_density_m", String::new()),
|
|
|
|
|
|
ro("N density", "pfm_density_n", String::new()),
|
|
|
|
|
|
ro("Vertex count", "pfm_v", self.vertices.len().to_string()),
|
|
|
|
|
|
ro("Face count", "pfm_f", self.faces.len().to_string()),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Misc".into(),
|
|
|
|
|
|
props: vec![ro("Fit/smooth", "pfm_smooth", smooth)],
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Transformable for PolyfaceMesh {
|
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
2026-04-02 19:13:37 +03:00
|
|
|
|
for v in &mut entity.vertices {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
2026-04-02 19:13:37 +03:00
|
|
|
|
&mut v.location.x,
|
|
|
|
|
|
&mut v.location.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
// ── 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;
|
|
|
|
|
|
}
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let get = |i: usize| -> Option<[f64; 3]> { self.vertices.get(i).map(|v| [v.x, v.y, v.z]) };
|
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
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 23:01:59 +03:00
|
|
|
|
// Ear-clip each face into fill_tris so shaded views render the mesh as
|
|
|
|
|
|
// a solid surface (fan triangulation spills outside concave faces).
|
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
|
|
|
|
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
|
|
|
|
|
for face in &self.faces {
|
2026-07-04 23:01:59 +03:00
|
|
|
|
let verts: Vec<[f64; 3]> = face.vertices.iter().filter_map(|&vi| get(vi)).collect();
|
|
|
|
|
|
if verts.len() >= 3 {
|
|
|
|
|
|
fill_tris.extend(triangulate_planar(&verts));
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(snap): producers emit f64 snap points (world_offset removal P3)
TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 22:10:29 +03:00
|
|
|
|
let snap_pts: Vec<(glam::DVec3, SnapHint)> = self
|
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
|
|
|
|
.vertices
|
|
|
|
|
|
.iter()
|
refactor(snap): producers emit f64 snap points (world_offset removal P3)
TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 22:10:29 +03:00
|
|
|
|
.map(|v| (glam::DVec3::new(v.x, v.y, v.z), SnapHint::Node))
|
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
|
|
|
|
.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()
|
2026-06-11 00:04:37 +03:00
|
|
|
|
.map(|(i, v)| square_grip(i, glam::DVec3::new(v.x, v.y, v.z)))
|
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
|
|
|
|
.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 {
|
2026-07-02 22:46:19 +03:00
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
// 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);
|
2026-07-02 22:46:19 +03:00
|
|
|
|
vec![PropSection {
|
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
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro(
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
"Level of Smoothness",
|
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
|
|
|
|
"msh_subdiv",
|
|
|
|
|
|
self.subdivision_level.to_string(),
|
|
|
|
|
|
),
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
ro("Number of Faces", "msh_f", self.faces.len().to_string()),
|
|
|
|
|
|
ro("Number of Grips", "msh_grips", self.vertices.len().to_string()),
|
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
|
|
|
|
ro(
|
feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00
|
|
|
|
"Watertight",
|
|
|
|
|
|
"msh_watertight",
|
|
|
|
|
|
if watertight { "Yes" } else { "No" },
|
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
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-07-02 22:46:19 +03:00
|
|
|
|
}]
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Transformable for Mesh {
|
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
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
|
|
|
|
for v in &mut entity.vertices {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
|
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
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-04 23:01:59 +03:00
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod triangulate_tests {
|
|
|
|
|
|
use super::triangulate_planar;
|
|
|
|
|
|
|
|
|
|
|
|
fn poly_area2d(p: &[[f64; 2]]) -> f64 {
|
|
|
|
|
|
let n = p.len();
|
|
|
|
|
|
let mut a = 0.0;
|
|
|
|
|
|
for i in 0..n {
|
|
|
|
|
|
let b = p[(i + 1) % n];
|
|
|
|
|
|
a += p[i][0] * b[1] - b[0] * p[i][1];
|
|
|
|
|
|
}
|
|
|
|
|
|
(a * 0.5).abs()
|
|
|
|
|
|
}
|
|
|
|
|
|
fn tri_area(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
|
|
|
|
|
|
// area in XY plane (test polygons are in Z=0)
|
|
|
|
|
|
(((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])) * 0.5).abs()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn concave_l_shape_no_spillover() {
|
|
|
|
|
|
// L-shaped hexagon (concave at the inner corner).
|
|
|
|
|
|
let poly: Vec<[f64; 3]> = vec![
|
|
|
|
|
|
[0.0, 0.0, 0.0], [4.0, 0.0, 0.0], [4.0, 2.0, 0.0],
|
|
|
|
|
|
[2.0, 2.0, 0.0], [2.0, 4.0, 0.0], [0.0, 4.0, 0.0],
|
|
|
|
|
|
];
|
|
|
|
|
|
let p2: Vec<[f64; 2]> = poly.iter().map(|p| [p[0], p[1]]).collect();
|
|
|
|
|
|
let want = poly_area2d(&p2); // = 12.0
|
|
|
|
|
|
let tris = triangulate_planar(&poly);
|
|
|
|
|
|
assert_eq!(tris.len() % 3, 0);
|
|
|
|
|
|
assert_eq!(tris.len() / 3, poly.len() - 2, "expected n-2 triangles");
|
|
|
|
|
|
let got: f64 = tris.chunks(3).map(|t| tri_area(t[0], t[1], t[2])).sum();
|
|
|
|
|
|
eprintln!("concave L: want_area={want} got_area={got} tris={}", tris.len() / 3);
|
|
|
|
|
|
// Fan would overshoot (triangles outside the L). Ear clip = exact.
|
|
|
|
|
|
assert!((got - want).abs() < 1e-6, "triangle area {got} != polygon area {want} (spillover)");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|