diff --git a/Cargo.lock b/Cargo.lock index 395e2d542..640d6be60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=8a92803#8a9280381d75a52ac5a35663449bdd44827e1463" dependencies = [ "ahash 0.8.12", "anyhow", @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ebeb2ec#ebeb2ecc2d17d92c588b0fa8bbe3864b156bc71f" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=b1901dd#b1901dde3351fc4bbcdffbdcf7328a5dec4d28d3" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 8cbeac47b..329787163 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0908da7", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ebeb2ec", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a92803", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b1901dd", 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/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 3d393aa20..dd65e80be 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0908da7", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a92803", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -37,7 +37,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0908da7", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a92803", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 671ef9eae..e80e1d9bc 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0908da7", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a92803", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/src/entities/solid.rs b/src/entities/solid.rs index 89505d3cf..bb4972f51 100644 --- a/src/entities/solid.rs +++ b/src/entities/solid.rs @@ -1,28 +1,35 @@ -// SOLID entity — 2D filled quadrilateral (or triangle when p3 == p4). -// -// Wireframe: the 4 perimeter edges as RenderObject::Lines. -// Filled: two triangles on `fill_tris`, preserving the entity's full WCS -// plane both at top level and through block expansion. The scene -// keeps a separate 2-D HatchModel only for plot projection; screen -// rendering filters that flattened copy out. -// Grips: 4 corner grip points. +// SOLID geometry comes from the kernel; entity coordinates stay in WCS here. use acadrust::entities::Solid; use crate::t; use crate::command::EntityTransform; -use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{edit_prop as edit, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; -fn dvec3(v: &acadrust::types::Vector3) -> glam::DVec3 { - glam::DVec3::new(v.x, v.y, v.z) +type Edge = ([f64; 3], [f64; 3]); + +struct SolidGeometry { + edges: Vec, + fill_tris: Vec<[f64; 3]>, + extruded: bool, +} + +fn normal_tuple(solid: &Solid) -> (f64, f64, f64) { + let normal = glam::DVec3::new(solid.normal.x, solid.normal.y, solid.normal.z) + .normalize_or(glam::DVec3::Z); + (normal.x, normal.y, normal.z) +} + +fn dvec3(v: [f64; 3]) -> glam::DVec3 { + glam::DVec3::from_array(v) } pub(crate) fn wcs_corners(solid: &Solid) -> [[f64; 3]; 4] { - let n = (solid.normal.x, solid.normal.y, solid.normal.z); + let n = normal_tuple(solid); let w = |v: &acadrust::types::Vector3| { let (x, y, z) = crate::scene::view::transform::ocs_point_to_wcs((v.x, v.y, v.z), n); [x, y, z] @@ -35,140 +42,244 @@ pub(crate) fn wcs_corners(solid: &Solid) -> [[f64; 3]; 4] { ] } -/// Return a non-self-intersecting perimeter for either conventional DXF SOLID -/// Z-order or older/perimeter-ordered data. Prefer DXF order when both are -/// valid, but recover legacy solids whose 1-2-4-3 walk forms a bow-tie. -pub(crate) fn perimeter_indices(corners: &[[f64; 3]; 4]) -> [usize; 4] { - let p = |index: usize| glam::DVec3::from_array(corners[index]); - let edge = p(1) - p(0); - let mut normal = edge.cross(p(2) - p(0)); - if normal.length_squared() < 1.0e-20 { - normal = edge.cross(p(3) - p(0)); - } - if normal.length_squared() < 1.0e-20 { - return [0, 1, 3, 2]; - } - let orient = |a: usize, b: usize, c: usize| { - (p(b) - p(a)).cross(p(c) - p(a)).dot(normal) +fn set_wcs_corner(solid: &mut Solid, index: usize, point: glam::DVec3) { + let n = normal_tuple(solid); + let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs( + (point.x, point.y, point.z), + n, + ); + let corner = match index { + 0 => &mut solid.first_corner, + 1 => &mut solid.second_corner, + 2 => &mut solid.third_corner, + 3 => &mut solid.fourth_corner, + _ => return, }; - let segments_cross = |a: usize, b: usize, c: usize, d: usize| { - let ab_c = orient(a, b, c); - let ab_d = orient(a, b, d); - let cd_a = orient(c, d, a); - let cd_b = orient(c, d, b); - ((ab_c > 0.0 && ab_d < 0.0) || (ab_c < 0.0 && ab_d > 0.0)) - && ((cd_a > 0.0 && cd_b < 0.0) || (cd_a < 0.0 && cd_b > 0.0)) - }; - let order_crosses = |order: [usize; 4]| { - segments_cross(order[0], order[1], order[2], order[3]) - || segments_cross(order[1], order[2], order[3], order[0]) - }; - let dxf = [0, 1, 3, 2]; - let perimeter = [0, 1, 2, 3]; - if order_crosses(dxf) && !order_crosses(perimeter) { - perimeter + corner.x = x; + corner.y = y; + corner.z = z; +} + +fn push_edge(lines: &mut Vec<[f64; 3]>, start: [f64; 3], end: [f64; 3]) { + lines.extend([start, end, [f64::NAN; 3]]); +} + +fn boundary(solid: &Solid) -> Vec<[f64; 3]> { + let corners = wcs_corners(solid); + if solid.is_triangle() { + vec![corners[0], corners[1], corners[2]] } else { - dxf + vec![corners[0], corners[1], corners[3], corners[2]] } } +fn boundary_edges(points: &[[f64; 3]]) -> Vec { + (0..points.len()) + .map(|index| (points[index], points[(index + 1) % points.len()])) + .collect() +} + +fn kernel_tolerance(points: &[[f64; 3]]) -> f64 { + cadkernel::space::coplanarity_tolerance(points).max(1.0e-12) +} + +fn extrusion_body( + solid: &Solid, + base: &[[f64; 3]], + tolerance: f64, + profile_valid: bool, +) -> Option { + if solid.thickness.abs() <= 1.0e-10 || !profile_valid { + return None; + } + let normal = glam::DVec3::new(solid.normal.x, solid.normal.y, solid.normal.z) + .normalize_or(glam::DVec3::Z); + let origin = glam::DVec3::from_array(base[0]); + let axis = base + .iter() + .skip(1) + .map(|point| glam::DVec3::from_array(*point) - origin) + .find(|axis| axis.length() > tolerance)?; + let plane = cadkernel::space::Plane::orthonormal( + base[0], + axis.to_array(), + normal.to_array(), + )?; + if base + .iter() + .any(|point| !plane.contains(*point, tolerance)) + { + return None; + } + let projected = base + .iter() + .map(|point| plane.project(*point)) + .collect::>>()?; + let profile = (0..projected.len()) + .map(|index| { + cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line { + start: projected[index], + end: projected[(index + 1) % projected.len()], + }) + }) + .collect::>(); + cadkernel::brep::extrude( + plane, + &profile, + (normal * solid.thickness).to_array(), + ) +} + +fn solid_geometry(solid: &Solid) -> Option { + let base = boundary(solid); + let tolerance = kernel_tolerance(&base); + let fill_tris = cadkernel::space::polygon::triangulate( + &base, + cadkernel::geom2d::Tolerance::new(tolerance), + ); + if solid.thickness.abs() <= 1.0e-10 { + return Some(SolidGeometry { + edges: boundary_edges(&base), + fill_tris, + extruded: false, + }); + } + let Some(body) = extrusion_body(solid, &base, tolerance, !fill_tris.is_empty()) else { + return Some(SolidGeometry { + edges: boundary_edges(&base), + fill_tris: Vec::new(), + extruded: false, + }); + }; + let mesh = cadkernel::brep::mesh::tessellate( + &body, + cadkernel::brep::mesh::TessellationTolerance::new( + cadkernel::tessellation::DEFAULT_ANGLE, + tolerance, + ), + ); + if !mesh.missing_faces.is_empty() || mesh.mesh.is_empty() { + return Some(SolidGeometry { + edges: boundary_edges(&base), + fill_tris: Vec::new(), + extruded: false, + }); + } + let edges = mesh + .edges + .iter() + .flat_map(|edge| edge.positions.windows(2).map(|pair| (pair[0], pair[1]))) + .collect(); + let mut fill_tris = Vec::with_capacity(mesh.mesh.triangles.len() * 3); + for triangle in mesh.mesh.triangles { + fill_tris.extend(triangle.map(|index| mesh.mesh.positions[index])); + } + Some(SolidGeometry { + edges, + fill_tris, + extruded: true, + }) +} + +pub(crate) fn wcs_bounds(solid: &Solid) -> Option { + let base = boundary(solid); + let tolerance = kernel_tolerance(&base); + let profile_valid = !cadkernel::space::polygon::triangulate( + &base, + cadkernel::geom2d::Tolerance::new(tolerance), + ) + .is_empty(); + extrusion_body(solid, &base, tolerance, profile_valid) + .and_then(|body| cadkernel::brep::body_bounds(&body)) + .or_else(|| cadkernel::brep::Aabb::around(base)) +} + impl RenderConvertible for Solid { fn to_render(&self, _document: &acadrust::CadDocument) -> Option { - // SOLID corners are OCS. Map them to WCS, then resolve either the DXF - // Z-order or legacy perimeter order before building edges and fill. - let corners = wcs_corners(self); - let order = perimeter_indices(&corners); - let [p0, p1, p2, p3] = order.map(|index| corners[index]); - let pts = vec![ - p0, - p1, - [f64::NAN; 3], - p1, - p2, - [f64::NAN; 3], - p2, - p3, - [f64::NAN; 3], - p3, - p0, - ]; - - let dvp = |p: [f64; 3]| glam::DVec3::from_array(p); - let snap = corners - .iter() - .copied() - .map(|point| (dvp(point), SnapHint::Node)) - .collect(); - - // Fill the resolved perimeter as two triangles. For a triangle the last - // two points coincide and the second triangle degenerates harmlessly. - let fill_tris = vec![p0, p1, p2, p0, p2, p3]; + let geometry = solid_geometry(self)?; + let mut lines = Vec::new(); + for &(start, end) in &geometry.edges { + push_edge(&mut lines, start, end); + } + let mut nodes = Vec::new(); + for &(start, end) in &geometry.edges { + for point in [start, end] { + if !nodes.contains(&point) { + nodes.push(point); + } + } + } + let mut snap = Vec::with_capacity(nodes.len() * 2 + geometry.edges.len()); + for point in nodes { + snap.push((dvec3(point), SnapHint::Node)); + snap.push((dvec3(point), SnapHint::Endpoint)); + } + snap.extend(geometry.edges.iter().map(|(start, end)| { + ((dvec3(*start) + dvec3(*end)) * 0.5, SnapHint::Midpoint) + })); + let pick_tris = geometry + .extruded + .then(|| geometry.fill_tris.clone()) + .unwrap_or_default(); Some(RenderEntity { - pick_tris: Vec::new(), - object: RenderObject::Lines(pts), + pick_tris, + object: RenderObject::Lines(lines), snap_pts: snap, tangent_geoms: vec![], - key_vertices: corners.to_vec(), - fill_tris, + key_vertices: Vec::new(), + fill_tris: geometry.fill_tris, }) } } impl Grippable for Solid { fn grips(&self) -> Vec { + let corners = wcs_corners(self); 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)), + square_grip(0, dvec3(corners[0])), + square_grip(1, dvec3(corners[1])), + square_grip(2, dvec3(corners[2])), + square_grip(3, dvec3(corners[3])), ] } 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, + let Some(current) = wcs_corners(self).get(grip_id).copied() else { + 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 point = match apply { + GripApply::Translate(delta) => dvec3(current) + delta, + GripApply::Absolute(point) => point, + }; + set_wcs_corner(self, grip_id, point); } } impl PropertyEditable for Solid { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - // Elevation is the OCS Z shared by the planar corners (no dedicated - // field on the entity); reported from the first corner's Z. + let corners = wcs_corners(self); let elevation = self.first_corner.z; vec![PropSection { title: t!("Geometry").into_owned(), props: vec![ - edit(t!("Point 1 X").as_ref(), "sl_p1x", self.first_corner.x), - edit(t!("Point 1 Y").as_ref(), "sl_p1y", self.first_corner.y), - edit(t!("Point 1 Z").as_ref(), "sl_p1z", self.first_corner.z), - edit(t!("Point 2 X").as_ref(), "sl_p2x", self.second_corner.x), - edit(t!("Point 2 Y").as_ref(), "sl_p2y", self.second_corner.y), - edit(t!("Point 2 Z").as_ref(), "sl_p2z", self.second_corner.z), - edit(t!("Point 3 X").as_ref(), "sl_p3x", self.third_corner.x), - edit(t!("Point 3 Y").as_ref(), "sl_p3y", self.third_corner.y), - edit(t!("Point 3 Z").as_ref(), "sl_p3z", self.third_corner.z), - edit(t!("Point 4 X").as_ref(), "sl_p4x", self.fourth_corner.x), - edit(t!("Point 4 Y").as_ref(), "sl_p4y", self.fourth_corner.y), - edit(t!("Point 4 Z").as_ref(), "sl_p4z", self.fourth_corner.z), - ro(t!("Elevation").as_ref(), "sl_elev", format!("{:.4}", elevation)), + edit(t!("Point 1 X").as_ref(), "sl_p1x", corners[0][0]), + edit(t!("Point 1 Y").as_ref(), "sl_p1y", corners[0][1]), + edit(t!("Point 1 Z").as_ref(), "sl_p1z", corners[0][2]), + edit(t!("Point 2 X").as_ref(), "sl_p2x", corners[1][0]), + edit(t!("Point 2 Y").as_ref(), "sl_p2y", corners[1][1]), + edit(t!("Point 2 Z").as_ref(), "sl_p2z", corners[1][2]), + edit(t!("Point 3 X").as_ref(), "sl_p3x", corners[2][0]), + edit(t!("Point 3 Y").as_ref(), "sl_p3y", corners[2][1]), + edit(t!("Point 3 Z").as_ref(), "sl_p3z", corners[2][2]), + edit(t!("Point 4 X").as_ref(), "sl_p4x", corners[3][0]), + edit(t!("Point 4 Y").as_ref(), "sl_p4y", corners[3][1]), + edit(t!("Point 4 Z").as_ref(), "sl_p4z", corners[3][2]), + edit(t!("Elevation").as_ref(), "sl_elev", elevation), + edit(t!("Thickness").as_ref(), "sl_thickness", self.thickness), + edit(t!("Normal X").as_ref(), "sl_normal_x", self.normal.x), + edit(t!("Normal Y").as_ref(), "sl_normal_y", self.normal.y), + edit(t!("Normal Z").as_ref(), "sl_normal_z", self.normal.z), ], }] } @@ -177,19 +288,56 @@ impl PropertyEditable for Solid { let Ok(v) = value.trim().parse::() else { return; }; + let point_field = match field { + "sl_p1x" => Some((0, 0)), + "sl_p1y" => Some((0, 1)), + "sl_p1z" => Some((0, 2)), + "sl_p2x" => Some((1, 0)), + "sl_p2y" => Some((1, 1)), + "sl_p2z" => Some((1, 2)), + "sl_p3x" => Some((2, 0)), + "sl_p3y" => Some((2, 1)), + "sl_p3z" => Some((2, 2)), + "sl_p4x" => Some((3, 0)), + "sl_p4y" => Some((3, 1)), + "sl_p4z" => Some((3, 2)), + _ => None, + }; + if let Some((point_index, component)) = point_field { + let mut point = dvec3(wcs_corners(self)[point_index]); + point[component] = v; + set_wcs_corner(self, point_index, point); + return; + } + match field { - "sl_p1x" => self.first_corner.x = v, - "sl_p1y" => self.first_corner.y = v, - "sl_p1z" => self.first_corner.z = v, - "sl_p2x" => self.second_corner.x = v, - "sl_p2y" => self.second_corner.y = v, - "sl_p2z" => self.second_corner.z = v, - "sl_p3x" => self.third_corner.x = v, - "sl_p3y" => self.third_corner.y = v, - "sl_p3z" => self.third_corner.z = v, - "sl_p4x" => self.fourth_corner.x = v, - "sl_p4y" => self.fourth_corner.y = v, - "sl_p4z" => self.fourth_corner.z = v, + "sl_elev" => { + let delta = v - self.first_corner.z; + self.first_corner.z += delta; + self.second_corner.z += delta; + self.third_corner.z += delta; + self.fourth_corner.z += delta; + } + "sl_thickness" => self.thickness = v, + "sl_normal_x" | "sl_normal_y" | "sl_normal_z" => { + let world = wcs_corners(self); + let mut normal = glam::DVec3::new(self.normal.x, self.normal.y, self.normal.z); + match field { + "sl_normal_x" => normal.x = v, + "sl_normal_y" => normal.y = v, + "sl_normal_z" => normal.z = v, + _ => {} + } + if normal.length_squared() > 1.0e-20 { + normal = normal.normalize(); + self.normal.x = normal.x; + self.normal.y = normal.y; + self.normal.z = normal.z; + for (index, point) in world.into_iter().enumerate() { + set_wcs_corner(self, index, dvec3(point)); + } + } + } _ => {} } } diff --git a/src/modules/draw/draw/solid2d.rs b/src/modules/draw/draw/solid2d.rs index 8a8221bac..170b5b068 100644 --- a/src/modules/draw/draw/solid2d.rs +++ b/src/modules/draw/draw/solid2d.rs @@ -1,11 +1,10 @@ // 2D solid tool — interactive command. // -// Command: SOLID (reachable as SO / SOLID2D — the bare SOLID verb currently -// toggles the shaded display) — pick three or four corner points and commit a -// filled triangle or quadrilateral. Four points may be picked either around -// the perimeter (the natural order shown by the rubber band) or in the -// traditional Z pattern. The command normalizes both forms to the DXF corner -// order before committing. Enter after the third point commits a triangle. +// Command: SOLID (reachable as SO / SOLID2D) — pick three or four corner points +// and commit a filled triangle or quadrilateral. Four-point input is preserved +// in the entity's documented Z order; after a quadrilateral, its opposite edge +// starts the next connected shape. Enter after the third point commits a +// triangle. use acadrust::entities::Solid; use acadrust::types::Vector3; @@ -49,42 +48,12 @@ impl Solid2dCommand { Vector3::new(p.x, p.y, p.z) } - /// True when walking the four points in pick order produces a bow-tie. - /// SOLID2D currently commits into the default world-XY OCS, so use that - /// same plane when deciding whether the last two points need swapping. - fn pick_order_crosses(points: &[DVec3]) -> bool { - if points.len() != 4 { - return false; - } - let orient = |a: DVec3, b: DVec3, c: DVec3| { - (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) - }; - let crosses = |a: DVec3, b: DVec3, c: DVec3, d: DVec3| { - let ab_c = orient(a, b, c); - let ab_d = orient(a, b, d); - let cd_a = orient(c, d, a); - let cd_b = orient(c, d, b); - ((ab_c > 0.0 && ab_d < 0.0) || (ab_c < 0.0 && ab_d > 0.0)) - && ((cd_a > 0.0 && cd_b < 0.0) || (cd_a < 0.0 && cd_b > 0.0)) - }; - crosses(points[0], points[1], points[2], points[3]) - || crosses(points[1], points[2], points[3], points[0]) - } - - /// Convert either perimeter picks (1-2-3-4) or classic SOLID Z picks - /// (1-2-4-3 around the perimeter) into the entity's DXF Z order. fn solid_from_four_points(points: &[DVec3]) -> Solid { - let perimeter_order = !Self::pick_order_crosses(points); - let (third, fourth) = if perimeter_order { - (points[3], points[2]) - } else { - (points[2], points[3]) - }; Solid::new( Self::v3(points[0]), Self::v3(points[1]), - Self::v3(third), - Self::v3(fourth), + Self::v3(points[2]), + Self::v3(points[3]), ) } } @@ -126,7 +95,10 @@ impl CadCommand for Solid2dCommand { .map(|point| self.plane.to_local(*point)) .collect(); let solid = Self::solid_from_four_points(&local); - CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Solid(solid))) + let next_edge = [self.points[2], self.points[3]]; + self.points.clear(); + self.points.extend(next_edge); + CmdResult::CommitEntity(self.plane.place_entity(EntityType::Solid(solid))) } else { CmdResult::NeedPoint } @@ -154,14 +126,22 @@ impl CadCommand for Solid2dCommand { if self.points.is_empty() { return None; } - // Outline the corners picked so far plus the cursor, closed back to the - // first point, as a rubber-band hint. + // Outline the exact Z-ordered boundary that would be committed. Do not + // reorder a crossing shape: point order is intentional geometry. let mut preview = self.points.clone(); preview.push(pt); - if preview.len() == 4 && Self::pick_order_crosses(&preview) { - preview.swap(2, 3); - } - let mut pts: Vec<[f64; 3]> = preview.iter().map(|p| [p.x, p.y, p.z]).collect(); + let display_order: Vec = if preview.len() == 4 { + vec![0, 1, 3, 2] + } else { + (0..preview.len()).collect() + }; + let mut pts: Vec<[f64; 3]> = display_order + .iter() + .map(|index| { + let point = preview[*index]; + [point.x, point.y, point.z] + }) + .collect(); pts.push([preview[0].x, preview[0].y, preview[0].z]); Some(WireModel::solid_f64( "rubber_band".to_string(), diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index fe3d3ca94..b0feb44a0 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1704,12 +1704,25 @@ pub(crate) fn set_wire_aabb(w: &mut WireModel, entity_box: [f32; 4]) { w.aabb = out; } +fn entity_bounds(e: &acadrust::EntityType) -> ([f64; 3], [f64; 3]) { + if let acadrust::EntityType::Solid(solid) = e { + if let Some(bounds) = crate::entities::solid::wcs_bounds(solid) { + return (bounds.min, bounds.max); + } + } + let bounds = e.as_entity().bounding_box(); + ( + [bounds.min.x, bounds.min.y, bounds.min.z], + [bounds.max.x, bounds.max.y, bounds.max.z], + ) +} + pub(crate) fn entity_aabb(e: &acadrust::EntityType) -> [f32; 4] { - let bbox = e.as_entity().bounding_box(); - let min_x = (bbox.min.x) as f32; - let min_y = (bbox.min.y) as f32; - let max_x = (bbox.max.x) as f32; - let max_y = (bbox.max.y) as f32; + let (min, max) = entity_bounds(e); + let min_x = min[0] as f32; + let min_y = min[1] as f32; + let max_x = max[0] as f32; + let max_y = max[1] as f32; // The all-zero box is bounding_box()'s Default — returned by entities with // no usable box (unimplemented) — so treat it as UNBOUNDED (never // pre-rejected). A genuinely zero-size box *away* from the origin (e.g. a @@ -1727,8 +1740,8 @@ pub(crate) fn entity_aabb(e: &acadrust::EntityType) -> [f32; 4] { /// indexing uses this so changing `world_offset` doesn't invalidate /// the index. pub(crate) fn entity_world_aabb_f64(e: &acadrust::EntityType) -> Option<[f64; 4]> { - let bbox = e.as_entity().bounding_box(); - let (xmin, ymin, xmax, ymax) = (bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y); + let (min, max) = entity_bounds(e); + let (xmin, ymin, xmax, ymax) = (min[0], min[1], max[0], max[1]); if xmin == xmax && ymin == ymax { return None; } diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 619dd248c..1c4dfa837 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1260,7 +1260,7 @@ pub fn tessellate( | EntityType::PolyfaceMesh(_) | EntityType::PolygonMesh(_) | EntityType::Mesh(_) - ); + ) || matches!(entity, EntityType::Solid(solid) if solid.thickness.abs() > 1.0e-10); // Thickness walls ride on the wire that carries their edges, not // on a wire of their own: they are pick geometry for that entity, // and `fill_tris` below deliberately splits off into a fill-only diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 160ee2345..6045e881b 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -2222,8 +2222,8 @@ impl Scene { } /// Build a solid-fill HatchModel for a DXF Solid entity. - /// Conventional DXF SOLID corners use Z-order; legacy entities may already - /// be in perimeter order. Use the same non-crossing resolver as wire fill. + /// SOLID corners use Z-order. Preserve it in the projected hatch as well so + /// intentionally crossing geometry is not silently rewritten. pub(super) fn solid_hatch_model(solid: &DxfSolid, color: [f32; 4]) -> HatchModel { // Keep the corners in f64 until the AABB centre is known, then store // each as a small f32 offset from it — same precision-preserving anchor @@ -2231,7 +2231,7 @@ impl Scene { // to f32 costs ~0.06 units of resolution at UTM magnitudes (~1e6), so // the quad snapped to a grid and the fill drifted off its outline. let wcs = crate::entities::solid::wcs_corners(solid); - let order = crate::entities::solid::perimeter_indices(&wcs); + let order = [0, 1, 3, 2]; let corners: [[f64; 2]; 4] = order.map(|index| [wcs[index][0], wcs[index][1]]); let mut min = [f64::INFINITY; 2]; let mut max = [f64::NEG_INFINITY; 2]; diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index afb58bc6d..929d83d73 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -22,6 +22,48 @@ use crate::scene::model::wire_model::WireModel; use iced::wgpu; use iced::wgpu::util::DeviceExt; +pub(crate) fn planar_solid_faces_view(wire: &WireModel, view_dir: glam::Vec3) -> bool { + if !wire.fill_is_2d_solid || wire.fill_is_3d { + return true; + } + let view = view_dir.as_dvec3().normalize_or_zero(); + let point = |index: usize| { + let high = wire.fill_tris[index]; + let low = wire + .fill_tris_low + .get(index) + .copied() + .unwrap_or([0.0; 3]); + glam::DVec3::new( + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + high[2] as f64 + low[2] as f64, + ) + }; + (0..wire.fill_tris.len() / 3).any(|triangle| { + let first = point(triangle * 3); + let second = point(triangle * 3 + 1); + let third = point(triangle * 3 + 2); + let normal = (second - first).cross(third - first).normalize_or_zero(); + normal.length_squared() > 0.0 && normal.dot(view).abs() >= 1.0 - 1.0e-5 + }) +} + +pub(crate) fn planar_solid_visibility_key( + wires: &[WireModel], + view_dir: glam::Vec3, +) -> u64 { + let mut key = 0xcbf29ce484222325_u64; + for wire in wires + .iter() + .filter(|wire| wire.fill_is_2d_solid && !wire.fill_is_3d) + { + key ^= planar_solid_faces_view(wire, view_dir) as u64; + key = key.wrapping_mul(0x100000001b3); + } + key +} + // ── Vertex layout ────────────────────────────────────────────────────────── #[repr(C)] @@ -74,8 +116,8 @@ impl Face3DVertex { // ── GPU handle ───────────────────────────────────────────────────────────── /// One vertex buffer + draw count. Fill data is split into as many chunks -/// as the device's `max_buffer_size` requires: a mesh-heavy DWG (e.g. a -/// Navisworks import) can hold tens of millions of fill triangles, and at +/// as the device's `max_buffer_size` requires: a mesh-heavy drawing can hold +/// tens of millions of fill triangles, and at /// 44 B/vertex a single batched buffer blows past the default 256 MB limit /// once enough layers are thawed — wgpu then raises an uncaptured /// validation error and aborts the process (#358). Same scheme as the @@ -149,6 +191,7 @@ impl Face3DGpu { all_wires: &[WireModel], keep_3d_mesh_fills: bool, show_2d_solid_fills: bool, + view_dir: glam::Vec3, depth_map: &rustc_hash::FxHashMap, ) -> Self { let depth_of = @@ -200,6 +243,9 @@ impl Face3DGpu { if !show_2d_solid_fills && wire.fill_is_2d_solid { continue; } + if !planar_solid_faces_view(wire, view_dir) { + continue; + } // A real 3-D surface fill (PolyfaceMesh / PolygonMesh) carries a // double-single low residual paired with `fill_tris` — it lives at // true world coordinates and must keep its real depth. 2-D fills @@ -259,6 +305,7 @@ impl Face3DGpu { all_wires, keep_3d_mesh_fills, show_2d_solid_fills, + view_dir, depth_map, ); Self { @@ -275,6 +322,7 @@ fn upload_block_chunks( wires: &[WireModel], keep_3d_mesh_fills: bool, show_2d_solid_fills: bool, + view_dir: glam::Vec3, depth_map: &rustc_hash::FxHashMap, ) -> (Vec, Vec) { let mut slots = rustc_hash::FxHashMap::default(); @@ -285,6 +333,7 @@ fn upload_block_chunks( }; if wire.fill_tris.is_empty() || (!show_2d_solid_fills && wire.fill_is_2d_solid) + || !planar_solid_faces_view(wire, view_dir) || (wire.fill_is_3d && !keep_3d_mesh_fills) { continue; diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index a5954b35c..b6013f888 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -312,12 +312,8 @@ pub struct Pipeline { /// a pick bumps only `selection_generation`, refreshing the overlay without /// touching the main wire buffers. pub cached_selection: (u64, u64), - /// `(wire_content_id, face3d_fill_active, show_2d_solid_fills)` the Face3D - /// edge/fill buffers were uploaded for. A stable content id avoids retaining - /// the resident wire Arc: - /// that Arc must stay uniquely owned by Scene so a small edit can splice it - /// in place instead of rebuilding the whole drawing. - pub cached_face3d_key: (u64, bool, bool), + /// Face upload key; keeps the resident wire Arc uniquely owned by Scene. + pub cached_face3d_key: (u64, bool, bool, u64), /// Handle → indices into the resident wire set, built once per wire upload /// (when `cached_wire_id` changes). Lets the selection/hover xray overlay /// gather just the highlighted entity's wires (`O(highlighted)`) instead of @@ -2323,7 +2319,7 @@ impl Pipeline { cached_epoch: (u64::MAX, u64::MAX, u64::MAX), cached_wire_id: u64::MAX, cached_selection: (u64::MAX, u64::MAX), - cached_face3d_key: (u64::MAX, false, false), + cached_face3d_key: (u64::MAX, false, false, u64::MAX), wire_handle_index: std::sync::Arc::new(rustc_hash::FxHashMap::default()), render_sig: u64::MAX, skip_geometry: false, @@ -3023,6 +3019,7 @@ impl Pipeline { all_wires: &[WireModel], wireframe_only: bool, show_2d_solid_fills: bool, + view_dir: glam::Vec3, depth_map: &rustc_hash::FxHashMap, ) { let perf_started = crate::perf::enabled().then(iced::time::Instant::now); @@ -3056,6 +3053,7 @@ impl Pipeline { all_wires, keep_3d_mesh_fills, show_2d_solid_fills, + view_dir, depth_map, )); } diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 55424b5fe..cb96d6ea9 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -378,7 +378,7 @@ impl shader::Primitive for Primitive { inner.cached_wire_id = u64::MAX; inner.cached_selection = (u64::MAX, u64::MAX); inner.cached_mesh_content_id = u64::MAX; - inner.cached_face3d_key = (u64::MAX, false, false); + inner.cached_face3d_key = (u64::MAX, false, false, u64::MAX); inner.cached_hatch_source = None; inner.cached_preview_hatch_source = None; inner.cached_wipeout_source = None; @@ -484,6 +484,12 @@ impl shader::Primitive for Primitive { // the view toggle so 2D fills stay on even when the user picks // the Wireframe overlay style. let face3d_fill_active = fill_mode && !vp.view_wireframe; + let solid_fill_active = fill_mode && vp.show_2d_solid_fills; + let solid_visibility_key = + crate::scene::pipeline::face3d_gpu::planar_solid_visibility_key( + &vp_wires, + vp.view_dir, + ); let fill_changed = inner.cached_fill_mode != fill_mode; let hatch_changed = inner .cached_hatch_source @@ -574,14 +580,16 @@ impl shader::Primitive for Primitive { && !face_pass_unchanged); if face3d_changed || face3d_fill_active != inner.cached_face3d_key.1 - || vp.show_2d_solid_fills != inner.cached_face3d_key.2 + || solid_fill_active != inner.cached_face3d_key.2 + || solid_visibility_key != inner.cached_face3d_key.3 { inner.upload_face3d( device, &vp.face3d_wires[..], &vp_wires[..], !face3d_fill_active, - vp.show_2d_solid_fills, + solid_fill_active, + vp.view_dir, &draw_depths, ); inner.cached_face3d_source = Some(Arc::clone(&vp.face3d_wires)); @@ -590,7 +598,8 @@ impl shader::Primitive for Primitive { inner.cached_face3d_key = ( vp.wire_content_id, face3d_fill_active, - vp.show_2d_solid_fills, + solid_fill_active, + solid_visibility_key, ); // Wire buffers are world-space, so a camera move alone doesn't // change them — only the view_proj uniform (uploaded every frame).