diff --git a/src/app/commands.rs b/src/app/commands.rs index a8905d98b..40f89cd57 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -978,6 +978,62 @@ impl H7CAD { } } + // ── Draw Order ──────────────────────────────────────────────── + cmd if cmd.starts_with("DRAWORDER") => { + use acadrust::objects::{ObjectType, SortEntitiesTable}; + let option = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); + let bring_front = match option.as_str() { + "F" | "FRONT" => Some(true), + "B" | "BACK" => Some(false), + _ => None, + }; + let i = self.active_tab; + let selected: Vec = self.tabs[i].scene + .selected_entities() + .iter() + .map(|(h, _)| *h) + .collect(); + if selected.is_empty() { + self.command_line.push_error("DRAWORDER: select entities first."); + } else if let Some(to_front) = bring_front { + self.push_undo_snapshot(i, "DRAWORDER"); + let block_handle = self.tabs[i].scene.current_layout_block_handle_pub(); + let doc = &mut self.tabs[i].scene.document; + let table_handle = doc.objects.iter() + .find_map(|(h, obj)| { + if let ObjectType::SortEntitiesTable(t) = obj { + if t.block_owner_handle == block_handle { Some(*h) } else { None } + } else { None } + }); + if let Some(th) = table_handle { + if let Some(ObjectType::SortEntitiesTable(table)) = + doc.objects.get_mut(&th) + { + for h in &selected { + if to_front { table.bring_to_front(*h); } + else { table.send_to_back(*h); } + } + } + } else { + let new_handle = acadrust::Handle::new(doc.next_handle()); + let mut table = SortEntitiesTable::for_block(block_handle); + table.handle = new_handle; + for h in &selected { + if to_front { table.bring_to_front(*h); } + else { table.send_to_back(*h); } + } + doc.objects.insert(new_handle, ObjectType::SortEntitiesTable(table)); + } + self.tabs[i].dirty = true; + let dir = if to_front { "front" } else { "back" }; + self.command_line.push_info(&format!( + "DRAWORDER: moved {} entities to {}.", selected.len(), dir + )); + } else { + self.command_line.push_info("Usage: DRAWORDER F (front) or DRAWORDER B (back)"); + } + } + // ── Plot / Page Setup ────────────────────────────────────────── "PRINT"|"PLOT"|"EXPORT" => { return Task::done(Message::PlotExport); diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs new file mode 100644 index 000000000..0f5b7dd75 --- /dev/null +++ b/src/entities/mesh.rs @@ -0,0 +1,380 @@ +use acadrust::entities::{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}; + +// ── Face3D ──────────────────────────────────────────────────────────────────── + +fn v3(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 { + 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 inv = self.invisible_edges; + + // Add edge as a line segment (separated by NaN from previous edges). + let mut pts: Vec<[f32; 3]> = Vec::new(); + let mut add_edge = |a: [f32; 3], b: [f32; 3]| { + if !pts.is_empty() { + pts.push([f32::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_f32; + pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]]; + } + + Some(TruckEntity { + object: TruckObject::Lines(pts), + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: vec![p0, p1, p2, p3], + }) + } +} + +impl Grippable for Face3D { + fn grips(&self) -> Vec { + vec![ + square_grip(0, Vec3::from(v3(&self.first_corner))), + square_grip(1, Vec3::from(v3(&self.second_corner))), + square_grip(2, Vec3::from(v3(&self.third_corner))), + square_grip(3, Vec3::from(v3(&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::() 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 { + 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| -> [f32; 3] { + let v = &self.vertices[i * n + j]; + [v.location.x as f32, v.location.y as f32, v.location.z as f32] + }; + + let mut pts: Vec<[f32; 3]> = Vec::new(); + + // Rows (M direction). + let m_end = if closed_m { m } else { m - 1 }; + for i in 0..m { + pts.push([f32::NAN; 3]); + for j in 0..n { + pts.push(pt(i, j)); + } + if closed_n { + pts.push(pt(i, 0)); + } + } + + // Columns (N direction). + let n_end = if closed_n { n } else { n - 1 }; + for j in 0..n { + pts.push([f32::NAN; 3]); + for i in 0..m { + pts.push(pt(i, j)); + } + if closed_m { + pts.push(pt(0, j)); + } + } + let _ = (m_end, n_end); // suppress warnings + + Some(TruckEntity { + object: TruckObject::Lines(pts), + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: vec![], + }) + } +} + +impl Grippable for PolygonMesh { + fn grips(&self) -> Vec { + 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 { + 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()), + ], + } + } + + 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 { + if self.vertices.is_empty() || self.faces.is_empty() { + return None; + } + + let get_v = |idx: i16| -> Option<[f32; 3]> { + let i = (idx.abs() as usize).checked_sub(1)?; + let v = self.vertices.get(i)?; + Some([v.location.x as f32, v.location.y as f32, v.location.z as f32]) + }; + + let mut pts: Vec<[f32; 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<[f32; 3]> = indices + .iter() + .filter(|&&i| i != 0) + .filter_map(|&i| get_v(i)) + .collect(); + + if verts.len() < 2 { + continue; + } + pts.push([f32::NAN; 3]); + for &p in &verts { + pts.push(p); + } + // Close the face polygon. + pts.push(verts[0]); + } + + Some(TruckEntity { + object: TruckObject::Lines(pts), + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: vec![], + }) + } +} + +impl Grippable for PolyfaceMesh { + fn grips(&self) -> Vec { + 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 { + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Vertices", "pfm_v", self.vertices.len().to_string()), + ro("Faces", "pfm_f", self.faces.len().to_string()), + ], + } + } + + 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, + ); + } + }); + } +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index d4518bc01..d596fd660 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -10,6 +10,7 @@ pub mod leader; pub mod line; pub mod multileader; pub mod lwpolyline; +pub mod mesh; pub mod mline; pub mod mtext; pub mod point; diff --git a/src/entities/traits.rs b/src/entities/traits.rs index d9497ef2c..829c1e3b3 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -52,6 +52,9 @@ impl EntityTypeOps for EntityType { EntityType::AttributeEntity(a) => TruckConvertible::to_truck(a, document), EntityType::MLine(ml) => TruckConvertible::to_truck(ml, document), EntityType::Tolerance(tol) => TruckConvertible::to_truck(tol, document), + EntityType::Face3D(f) => TruckConvertible::to_truck(f, document), + EntityType::PolygonMesh(pm) => TruckConvertible::to_truck(pm, document), + EntityType::PolyfaceMesh(pfm) => TruckConvertible::to_truck(pfm, document), EntityType::Text(text) => TruckConvertible::to_truck(text, document), EntityType::MText(text) => TruckConvertible::to_truck(text, document), EntityType::Leader(leader) => TruckConvertible::to_truck(leader, document), @@ -78,6 +81,9 @@ impl EntityTypeOps for EntityType { EntityType::AttributeEntity(a) => Grippable::grips(a), EntityType::MLine(ml) => Grippable::grips(ml), EntityType::Tolerance(tol) => Grippable::grips(tol), + EntityType::Face3D(f) => Grippable::grips(f), + EntityType::PolygonMesh(pm) => Grippable::grips(pm), + EntityType::PolyfaceMesh(pfm) => Grippable::grips(pfm), EntityType::Point(pt) => Grippable::grips(pt), EntityType::Spline(spline) => Grippable::grips(spline), EntityType::Text(text) => Grippable::grips(text), @@ -155,6 +161,18 @@ impl EntityTypeOps for EntityType { tol, text_style_names, )), + EntityType::Face3D(f) => Some(PropertyEditable::geometry_properties( + f, + text_style_names, + )), + EntityType::PolygonMesh(pm) => Some(PropertyEditable::geometry_properties( + pm, + text_style_names, + )), + EntityType::PolyfaceMesh(pfm) => Some(PropertyEditable::geometry_properties( + pfm, + text_style_names, + )), EntityType::Hatch(hatch) => Some(PropertyEditable::geometry_properties( hatch, text_style_names, @@ -216,6 +234,9 @@ impl EntityTypeOps for EntityType { EntityType::AttributeEntity(a) => PropertyEditable::apply_geom_prop(a, field, value), EntityType::MLine(ml) => PropertyEditable::apply_geom_prop(ml, field, value), EntityType::Tolerance(tol) => PropertyEditable::apply_geom_prop(tol, field, value), + EntityType::Face3D(f) => PropertyEditable::apply_geom_prop(f, field, value), + EntityType::PolygonMesh(pm) => PropertyEditable::apply_geom_prop(pm, field, value), + EntityType::PolyfaceMesh(pfm) => PropertyEditable::apply_geom_prop(pfm, field, value), EntityType::Hatch(hatch) => PropertyEditable::apply_geom_prop(hatch, field, value), EntityType::Point(pt) => PropertyEditable::apply_geom_prop(pt, field, value), EntityType::Spline(spline) => PropertyEditable::apply_geom_prop(spline, field, value), @@ -248,6 +269,9 @@ impl EntityTypeOps for EntityType { EntityType::AttributeEntity(a) => Grippable::apply_grip(a, grip_id, apply), EntityType::MLine(ml) => Grippable::apply_grip(ml, grip_id, apply), EntityType::Tolerance(tol) => Grippable::apply_grip(tol, grip_id, apply), + EntityType::Face3D(f) => Grippable::apply_grip(f, grip_id, apply), + EntityType::PolygonMesh(pm) => Grippable::apply_grip(pm, grip_id, apply), + EntityType::PolyfaceMesh(pfm) => Grippable::apply_grip(pfm, grip_id, apply), EntityType::Point(pt) => Grippable::apply_grip(pt, grip_id, apply), EntityType::Spline(spline) => Grippable::apply_grip(spline, grip_id, apply), EntityType::Text(text) => Grippable::apply_grip(text, grip_id, apply), @@ -280,6 +304,9 @@ impl EntityTypeOps for EntityType { EntityType::AttributeEntity(a) => Transformable::apply_transform(a, t), EntityType::MLine(ml) => Transformable::apply_transform(ml, t), EntityType::Tolerance(tol) => Transformable::apply_transform(tol, t), + EntityType::Face3D(f) => Transformable::apply_transform(f, t), + EntityType::PolygonMesh(pm) => Transformable::apply_transform(pm, t), + EntityType::PolyfaceMesh(pfm) => Transformable::apply_transform(pfm, t), EntityType::MText(text) => Transformable::apply_transform(text, t), EntityType::Point(pt) => Transformable::apply_transform(pt, t), EntityType::Spline(spline) => Transformable::apply_transform(spline, t),