diff --git a/Cargo.lock b/Cargo.lock index 477f5cbdb..7e9cfbb71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1283cce45439abecdfd49828c7d6b84cb8ba0181#1283cce45439abecdfd49828c7d6b84cb8ba0181" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=44fb4783b3e847a6ac35d9e0cf468886cb5a06d1#44fb4783b3e847a6ac35d9e0cf468886cb5a06d1" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 999471a6c..ed14714a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "9a332bb3c48eca90c3eefaa17aed514da0b8694a", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1283cce45439abecdfd49828c7d6b84cb8ba0181", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "44fb4783b3e847a6ac35d9e0cf468886cb5a06d1", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index d9edbc862..d74a2258f 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -454,6 +454,31 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } + "3DFACE" => { + use crate::modules::draw::draw::face3d::Face3dCommand; + let new_cmd = Face3dCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "EDGE" => { + use crate::modules::draw::draw::face3d::FaceEdgeCommand; + let faces = self.tabs[i] + .scene + .document + .entities() + .filter_map(|entity| match entity { + acadrust::EntityType::Face3D(face) => { + Some((face.common.handle, face.clone())) + } + _ => None, + }) + .collect(); + let new_cmd = FaceEdgeCommand::new(faces); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + // 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is // currently the shaded-display toggle (token collision tracked). "SOLID" | "SOLID2D" => { diff --git a/src/app/properties.rs b/src/app/properties.rs index e5287698a..3a405ed86 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -125,6 +125,9 @@ impl OpenCADStudio { crate::entities::polyline::polyline3d_control_vertex_count(polyline), ), acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()), + acadrust::EntityType::Face3D(face) => { + Some(if face.is_triangle() { 3 } else { 4 }) + } acadrust::EntityType::Spline(spline) => { Some(if crate::entities::spline::shows_fit_points(spline) { spline.fit_points.len() diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 56772b291..a6efbf5c9 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4922,11 +4922,14 @@ impl OpenCADStudio { Message::PropVertexStep(delta) => { let i = self.active_tab; let handles = self.property_target_handles(i); - // Vertex navigation applies to a single selected polyline. + // Navigate the active vertex or table cell. let n = if handles.len() == 1 { match self.tabs[i].scene.document.get_entity(handles[0]) { Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(), Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(), + Some(acadrust::EntityType::Face3D(face)) => { + if face.is_triangle() { 3 } else { 4 } + } Some(acadrust::EntityType::Polyline3D(p)) => { crate::entities::polyline::polyline3d_control_vertex_count(p) } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 36d15d5f1..9c75b76d5 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -433,7 +433,8 @@ impl OpenCADStudio { Some(acadrust::EntityType::LwPolyline(_)) | Some(acadrust::EntityType::Polyline2D(_)) | Some(acadrust::EntityType::Polyline3D(_)) - | Some(acadrust::EntityType::Spline(_)) => true, + | Some(acadrust::EntityType::Spline(_)) + | Some(acadrust::EntityType::Face3D(_)) => true, _ => false, }; indexed.then_some(tab.properties.prop_vertex) diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index 664ab8757..01d192ae2 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -2,141 +2,56 @@ use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, Polyface use cadkernel::geom2d::{triangulate, Tolerance}; use cadkernel::space::{polygon, Plane, Vec3 as KernelVec3}; use glam::Vec3; -use crate::t; use crate::command::EntityTransform; -use crate::entities::common::{parse_f64, ro_prop as ro, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::entities::common::{ + edit_prop as edit, parse_f64, ro_prop as ro, stepper_prop as stepper, square_grip, +}; +use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable}; use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; +use crate::t; -/// 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. +/// Triangulate a planar polygon through the kernel. pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> { - let n = poly.len(); - if n < 3 { + if poly.len() < 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 origin = KernelVec3::from(poly[0]); + let tolerance = Tolerance::default().linear(); + let Some(axis) = poly[1..] + .iter() + .map(|point| KernelVec3::from(*point) - origin) + .find(|axis| axis.length() > tolerance) + else { + return Vec::new(); }; - 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 + let Some(normal) = polygon::normal(poly) else { + return Vec::new(); }; - // 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 Some(plane) = Plane::orthonormal(poly[0], axis.to_array(), normal) else { + return Vec::new(); }; - 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 = (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; - } + let projected: Vec<[f64; 2]> = poly + .iter() + .filter_map(|&point| plane.project(point)) + .collect(); + if projected.len() != poly.len() { + return Vec::new(); } - if idx.len() == 3 { - out.push(poly[idx[0]]); - out.push(poly[idx[1]]); - out.push(poly[idx[2]]); + let (points, triangles) = triangulate(&projected, &[]); + let source: Vec = points + .iter() + .filter_map(|point| projected.iter().position(|candidate| candidate == point)) + .collect(); + if source.len() != points.len() { + return Vec::new(); } - out + triangles + .into_iter() + .flat_map(|triangle| triangle.map(|index| poly[source[index]])) + .collect() } // ── Face3D ──────────────────────────────────────────────────────────────────── @@ -174,32 +89,7 @@ fn face3d_fill(corners: [[f64; 3]; 4]) -> Vec<[f64; 3]> { return Vec::new(); } - let Some(normal) = polygon::normal(&ring) else { - return Vec::new(); - }; - let axis = (KernelVec3::from(ring[1]) - KernelVec3::from(ring[0])).to_array(); - let Some(plane) = Plane::orthonormal(ring[0], axis, normal) else { - return Vec::new(); - }; - let projected: Vec<[f64; 2]> = ring - .iter() - .filter_map(|&point| plane.project(point)) - .collect(); - if projected.len() != ring.len() { - return Vec::new(); - } - let (points, triangles) = triangulate(&projected, &[]); - let source: Vec = points - .iter() - .filter_map(|point| projected.iter().position(|candidate| candidate == point)) - .collect(); - if source.len() != points.len() { - return Vec::new(); - } - triangles - .into_iter() - .flat_map(|triangle| triangle.map(|index| ring[source[index]])) - .collect() + triangulate_planar(&ring) } impl RenderConvertible for Face3D { @@ -238,15 +128,6 @@ impl RenderConvertible for Face3D { add_edge(p3, p0); } - if pts.is_empty() { - // All edges invisible — show a tiny cross at centroid. - let cx = (p0[0] + p1[0] + p2[0] + p3[0]) / 4.0; - let cy = (p0[1] + p1[1] + p2[1] + p3[1]) / 4.0; - let cz = (p0[2] + p1[2] + p2[2] + p3[2]) / 4.0; - let s = 0.1_f64; - pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]]; - } - Some(RenderEntity { pick_tris: Vec::new(), object: RenderObject::Lines(pts), @@ -265,15 +146,22 @@ impl RenderConvertible for Face3D { impl Grippable for Face3D { fn grips(&self) -> Vec { - vec![ + let mut grips = 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)), - ] + ]; + if !self.is_triangle() { + grips.push(square_grip(3, dvec3(&self.fourth_corner))); + } + grips } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + let was_triangle = self.is_triangle(); + if was_triangle && grip_id == 3 { + return; + } let corner = match grip_id { 0 => &mut self.first_corner, 1 => &mut self.second_corner, @@ -281,69 +169,107 @@ impl Grippable for Face3D { 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; - } + let current = dvec3(corner); + let updated = match apply { + GripApply::Translate(delta) => current + delta, + GripApply::Absolute(position) => position, + }; + if !updated.is_finite() { + return; + } + corner.x = updated.x; + corner.y = updated.y; + corner.z = updated.z; + if was_triangle && grip_id == 2 { + self.fourth_corner = self.third_corner; } } } impl PropertyEditable for Face3D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - use crate::entities::common::edit_prop as edit; + let was_triangle = self.is_triangle(); + let vertex_count = if was_triangle { 3 } else { 4 }; + let current = crate::scene::view::dispatch::prop_current_vertex() + .min(vertex_count - 1); + let corner = match current { + 0 => &self.first_corner, + 1 => &self.second_corner, + 2 => &self.third_corner, + _ => &self.fourth_corner, + }; let inv = self.invisible_edges; - let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" }; + let edge = |label: &str, field: &'static str, hidden: bool| Property { + label: label.to_string(), + field, + value: PropValue::Choice { + selected: if hidden { + t!("Invisible").into_owned() + } else { + t!("Visible").into_owned() + }, + options: vec![t!("Visible").into_owned(), t!("Invisible").into_owned()], + }, + }; vec![PropSection { title: t!("Geometry").into_owned(), props: vec![ - ro(t!("Current vertex").as_ref(), "f3_current", String::new()), - edit(t!("Vertex 1 X").as_ref(), "f3_p1x", self.first_corner.x), - edit(t!("Vertex 1 Y").as_ref(), "f3_p1y", self.first_corner.y), - edit(t!("Vertex 1 Z").as_ref(), "f3_p1z", self.first_corner.z), - edit(t!("Vertex 2 X").as_ref(), "f3_p2x", self.second_corner.x), - edit(t!("Vertex 2 Y").as_ref(), "f3_p2y", self.second_corner.y), - edit(t!("Vertex 2 Z").as_ref(), "f3_p2z", self.second_corner.z), - edit(t!("Vertex 3 X").as_ref(), "f3_p3x", self.third_corner.x), - edit(t!("Vertex 3 Y").as_ref(), "f3_p3y", self.third_corner.y), - edit(t!("Vertex 3 Z").as_ref(), "f3_p3z", self.third_corner.z), - edit(t!("Vertex 4 X").as_ref(), "f3_p4x", self.fourth_corner.x), - edit(t!("Vertex 4 Y").as_ref(), "f3_p4y", self.fourth_corner.y), - edit(t!("Vertex 4 Z").as_ref(), "f3_p4z", self.fourth_corner.z), - ro(t!("Edge 1").as_ref(), "f3_edge1", edge(inv.is_first_invisible())), - ro(t!("Edge 2").as_ref(), "f3_edge2", edge(inv.is_second_invisible())), - ro(t!("Edge 3").as_ref(), "f3_edge3", edge(inv.is_third_invisible())), - ro(t!("Edge 4").as_ref(), "f3_edge4", edge(inv.is_fourth_invisible())), + stepper( + t!("Current vertex").as_ref(), + "current_vertex", + format!("{}", current + 1), + ), + edit(t!("Vertex X").as_ref(), "f3_vertex_x", corner.x), + edit(t!("Vertex Y").as_ref(), "f3_vertex_y", corner.y), + edit(t!("Vertex Z").as_ref(), "f3_vertex_z", corner.z), + edge(t!("Edge 1").as_ref(), "f3_edge1", inv.is_first_invisible()), + edge(t!("Edge 2").as_ref(), "f3_edge2", inv.is_second_invisible()), + edge(t!("Edge 3").as_ref(), "f3_edge3", inv.is_third_invisible()), + edge(t!("Edge 4").as_ref(), "f3_edge4", inv.is_fourth_invisible()), ], }] } fn apply_geom_prop(&mut self, field: &str, value: &str) { - let Ok(v) = value.trim().parse::() else { + if field.starts_with("f3_edge") { + let invisible = if value == t!("Invisible").as_ref() { + true + } else if value == t!("Visible").as_ref() { + false + } else { + return; + }; + match field { + "f3_edge1" => self.invisible_edges.set_first_invisible(invisible), + "f3_edge2" => self.invisible_edges.set_second_invisible(invisible), + "f3_edge3" => self.invisible_edges.set_third_invisible(invisible), + "f3_edge4" => self.invisible_edges.set_fourth_invisible(invisible), + _ => {} + } + return; + } + let Some(value) = parse_f64(value).filter(|value| value.is_finite()) else { return; }; + let was_triangle = self.is_triangle(); + let vertex_count = if was_triangle { 3 } else { 4 }; + let current = crate::scene::view::dispatch::prop_current_vertex() + .min(vertex_count - 1); + let corner = match current { + 0 => &mut self.first_corner, + 1 => &mut self.second_corner, + 2 => &mut self.third_corner, + _ => &mut self.fourth_corner, + }; 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, + "f3_vertex_x" => corner.x = value, + "f3_vertex_y" => corner.y = value, + "f3_vertex_z" => corner.z = value, _ => {} } + if was_triangle && current == 2 { + self.fourth_corner = self.third_corner; + } } } diff --git a/src/modules/draw/draw/face3d.rs b/src/modules/draw/draw/face3d.rs new file mode 100644 index 000000000..97c61335a --- /dev/null +++ b/src/modules/draw/draw/face3d.rs @@ -0,0 +1,500 @@ +use acadrust::entities::{face3d::InvisibleEdgeFlags, Face3D}; +use acadrust::types::Vector3; +use acadrust::{EntityType, Handle}; +use cadkernel::geom2d::Tolerance; +use cadkernel::space::{curve::segments_overlap_collinearly, Vec3 as KernelVec3}; +use glam::DVec3; +use std::collections::{HashMap, HashSet}; + +use crate::command::{CadCommand, CmdOption, CmdResult}; +use crate::scene::model::wire_model::WireModel; +use crate::t; + +fn vector(point: DVec3) -> Vector3 { + Vector3::new(point.x, point.y, point.z) +} + +fn point(value: Vector3) -> DVec3 { + DVec3::new(value.x, value.y, value.z) +} + +fn set_edge_invisible(flags: &mut InvisibleEdgeFlags, edge: usize, invisible: bool) { + match edge { + 0 => flags.set_first_invisible(invisible), + 1 => flags.set_second_invisible(invisible), + 2 => flags.set_third_invisible(invisible), + 3 => flags.set_fourth_invisible(invisible), + _ => {} + } +} + +fn edge_is_invisible(flags: InvisibleEdgeFlags, edge: usize) -> bool { + match edge { + 0 => flags.is_first_invisible(), + 1 => flags.is_second_invisible(), + 2 => flags.is_third_invisible(), + 3 => flags.is_fourth_invisible(), + _ => false, + } +} + +fn append_segment(points: &mut Vec<[f64; 3]>, start: [f64; 3], end: [f64; 3]) { + if !points.is_empty() { + points.push([f64::NAN; 3]); + } + points.push(start); + points.push(end); +} + +pub struct Face3dCommand { + points: Vec, + invisible: [bool; 4], + pending_invisible: bool, +} + +impl Face3dCommand { + pub fn new() -> Self { + Self { + points: Vec::new(), + invisible: [false; 4], + pending_invisible: false, + } + } + + fn build(&self, fourth: DVec3) -> Option { + if self.points.len() != 3 { + return None; + } + let mut face = Face3D::new( + vector(self.points[0]), + vector(self.points[1]), + vector(self.points[2]), + vector(fourth), + ); + for edge in 0..4 { + set_edge_invisible(&mut face.invisible_edges, edge, self.invisible[edge]); + } + Some(EntityType::Face3D(face)) + } + + fn continue_from_last_edge(&mut self, fourth: DVec3) { + let third = self.points[2]; + let shared_edge_invisible = self.invisible[2]; + self.points.clear(); + self.points.push(third); + self.points.push(fourth); + self.invisible = [shared_edge_invisible, false, false, false]; + self.pending_invisible = false; + } + + fn continue_from_triangle(&mut self) { + let second = self.points[1]; + let third = self.points[2]; + let shared_edge_invisible = self.invisible[1]; + self.points.clear(); + self.points.push(second); + self.points.push(third); + self.invisible = [shared_edge_invisible, false, false, false]; + self.pending_invisible = false; + } +} + +impl CadCommand for Face3dCommand { + fn name(&self) -> &'static str { + "3DFACE" + } + + fn prompt(&self) -> String { + match self.points.len() { + 0 => t!("3DFACE Specify first point or [Invisible]:").into_owned(), + 1 => t!("3DFACE Specify second point or [Invisible]:").into_owned(), + 2 => t!("3DFACE Specify third point or [Invisible] :").into_owned(), + _ => t!("3DFACE Specify fourth point or [Invisible] :") + .into_owned(), + } + } + + fn options(&self) -> Vec { + vec![CmdOption::new(t!("Invisible").as_ref(), "I")] + } + + fn on_point(&mut self, picked: DVec3) -> CmdResult { + let point_index = self.points.len(); + if self.pending_invisible && point_index < 4 { + self.invisible[point_index] = true; + } + self.pending_invisible = false; + if self.points.len() == 3 { + let entity = self.build(picked); + self.continue_from_last_edge(picked); + return entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity); + } + self.points.push(picked); + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + if self.points.len() != 3 { + return CmdResult::Cancel; + } + if self.pending_invisible { + self.invisible[3] = true; + } + let third = self.points[2]; + let entity = self.build(third); + self.continue_from_triangle(); + entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity) + } + + fn wants_text_input(&self) -> bool { + true + } + + fn point_step_accepts_keywords(&self) -> bool { + true + } + + fn on_text_input(&mut self, value: &str) -> Option { + match value.trim().to_ascii_uppercase().as_str() { + "I" | "INVISIBLE" => { + self.pending_invisible = true; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + fn on_mouse_move(&mut self, cursor: DVec3) -> Option { + if self.points.is_empty() { + return None; + } + let mut preview = Vec::new(); + for edge in 0..self.points.len().saturating_sub(1) { + if !self.invisible[edge] { + append_segment( + &mut preview, + self.points[edge].to_array(), + self.points[edge + 1].to_array(), + ); + } + } + let pending_edge = self.points.len() - 1; + if !self.invisible[pending_edge] { + append_segment( + &mut preview, + self.points.last()?.to_array(), + cursor.to_array(), + ); + } + if self.points.len() == 3 && !(self.invisible[3] || self.pending_invisible) { + append_segment(&mut preview, cursor.to_array(), self.points[0].to_array()); + } + Some(WireModel::solid_f64( + "face3d_preview".to_string(), + preview, + WireModel::CYAN, + false, + )) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum EdgeMode { + Toggle, + Display, + DisplaySelect, +} + +pub struct FaceEdgeCommand { + faces: Vec<(Handle, Face3D)>, + mode: EdgeMode, + show_all_hidden: bool, + displayed_faces: HashSet, + pending_replacements: HashMap, +} + +impl FaceEdgeCommand { + pub fn new(faces: Vec<(Handle, Face3D)>) -> Self { + Self { + faces, + mode: EdgeMode::Toggle, + show_all_hidden: false, + displayed_faces: HashSet::new(), + pending_replacements: HashMap::new(), + } + } + + fn corners(face: &Face3D) -> [KernelVec3; 4] { + [ + KernelVec3::from(point(face.first_corner).to_array()), + KernelVec3::from(point(face.second_corner).to_array()), + KernelVec3::from(point(face.third_corner).to_array()), + KernelVec3::from(point(face.fourth_corner).to_array()), + ] + } + + fn nearest_edge(face: &Face3D, picked: KernelVec3) -> Option { + let corners = Self::corners(face); + let tolerance = Tolerance::default().linear(); + let mut best = None; + for edge in 0..4 { + let start = corners[edge]; + let end = corners[(edge + 1) % 4]; + if start.distance(end) <= tolerance { + continue; + } + let distance = picked.distance_to_segment(start, end); + if best.is_none_or(|(_, best_distance)| distance < best_distance) { + best = Some((edge, distance)); + } + } + best.map(|(edge, _)| edge) + } + + fn edge_wire(face: &Face3D, edge: usize, name: &str) -> WireModel { + let corners = Self::corners(face); + WireModel::solid_f64( + name.to_string(), + vec![corners[edge].to_array(), corners[(edge + 1) % 4].to_array()], + WireModel::CYAN, + false, + ) + } + + fn collinear_replacements( + &mut self, + selected_face: usize, + selected_edge: usize, + ) -> Vec<(Handle, Vec)> { + let selected_corners = Self::corners(&self.faces[selected_face].1); + let selected_start = selected_corners[selected_edge]; + let selected_end = selected_corners[(selected_edge + 1) % 4]; + let make_invisible = !edge_is_invisible( + self.faces[selected_face].1.invisible_edges, + selected_edge, + ); + let mut replacements = Vec::new(); + self.pending_replacements.clear(); + for (handle, face) in &self.faces { + let corners = Self::corners(face); + let mut updated = face.clone(); + let mut changed = false; + for edge in 0..4 { + if segments_overlap_collinearly( + selected_start.to_array(), + selected_end.to_array(), + corners[edge].to_array(), + corners[(edge + 1) % 4].to_array(), + Tolerance::default().linear(), + ) && edge_is_invisible(updated.invisible_edges, edge) != make_invisible + { + set_edge_invisible(&mut updated.invisible_edges, edge, make_invisible); + changed = true; + } + } + if changed { + self.pending_replacements.insert(*handle, updated.clone()); + replacements.push((*handle, vec![EntityType::Face3D(updated)])); + } + } + replacements + } + + fn hidden_edges_wire(&self) -> WireModel { + let mut points = Vec::new(); + for (handle, face) in &self.faces { + if !self.show_all_hidden && !self.displayed_faces.contains(handle) { + continue; + } + let corners = Self::corners(face); + for edge in 0..4 { + if edge_is_invisible(face.invisible_edges, edge) + && corners[edge].distance(corners[(edge + 1) % 4]) + > Tolerance::default().linear() + { + append_segment( + &mut points, + corners[edge].to_array(), + corners[(edge + 1) % 4].to_array(), + ); + } + } + } + WireModel::solid_f64( + "face3d_hidden_edges".to_string(), + points, + WireModel::CYAN, + false, + ) + } +} + +impl CadCommand for FaceEdgeCommand { + fn name(&self) -> &'static str { + "EDGE" + } + + fn prompt(&self) -> String { + match self.mode { + EdgeMode::Toggle => { + t!("EDGE Select edge of 3D face to toggle visibility or [Display]:").into_owned() + } + EdgeMode::Display => { + t!("EDGE Display invisible edges [All/Select]