diff --git a/Cargo.lock b/Cargo.lock index c25aab28c..13cff3b13 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=7e6a651c9493bd52f6abcdaccf508d346766cda5#7e6a651c9493bd52f6abcdaccf508d346766cda5" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=65aa99ef1ab2c5632d61e61baef7c56a27027d4b#65aa99ef1ab2c5632d61e61baef7c56a27027d4b" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 872492c17..f0ea2518e 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 = "393d6c0857447f9ca678bed97a612707f0a842c6", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "7e6a651c9493bd52f6abcdaccf508d346766cda5", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "65aa99ef1ab2c5632d61e61baef7c56a27027d4b", 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/display.rs b/src/app/commands/display.rs index 645e6753b..a27dfade3 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -796,7 +796,8 @@ impl OpenCADStudio { use crate::modules::insert::solid3d_cmds::RevolveCommand; let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); - let mut cmd = RevolveCommand::new(color); + let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize; + let mut cmd = RevolveCommand::new(color, isolines); if !selected.is_empty() { cmd.set_preselection( selected diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 43f174279..e353de6c8 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -36,7 +36,13 @@ impl super::OpenCADStudio { return Handle::NULL; }; self.tabs[i].scene.create_solid_history(handle, history); - self.tabs[i].scene.register_solid_model(handle, solid); + if !self.tabs[i].scene.register_solid_model(handle, solid) { + // A command result is not successful until it has renderable + // geometry. Roll the new entity back so DELOBJ cannot consume a + // visible source in exchange for an invisible result. + self.tabs[i].scene.rollback_new_entities(&[handle]); + return Handle::NULL; + } handle } @@ -63,7 +69,10 @@ impl super::OpenCADStudio { let Some(handle) = self.commit_entity_handle(entity) else { return Handle::NULL; }; - self.tabs[i].scene.register_solid_model(handle, surface); + if !self.tabs[i].scene.register_solid_model(handle, surface) { + self.tabs[i].scene.rollback_new_entities(&[handle]); + return Handle::NULL; + } handle } diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 25e7ffb77..63ea91bbe 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -156,22 +156,28 @@ impl ExtrudeCommand { self.taper_angle, ) }; - body.map(|body| preview_body_wires(&body, self.color)) + body.map(|body| preview_body_wires(&body, self.color, 0)) .unwrap_or_default() }) .collect() } } -fn preview_body_wires(body: &cadkernel::brep::Body, color: [f32; 4]) -> Vec { +fn preview_body_wires( + body: &cadkernel::brep::Body, + color: [f32; 4], + isolines: usize, +) -> Vec { let mesh = cadkernel::brep::mesh::tessellate( body, cadkernel::brep::mesh::TessellationTolerance::new( cadkernel::tessellation::DEFAULT_ANGLE, 1e-9, - ), + ) + .with_isolines(isolines), ); - mesh.edges + let mut wires = mesh + .edges .into_iter() .filter(|edge| edge.positions.len() >= 2) .map(|edge| { @@ -182,7 +188,21 @@ fn preview_body_wires(body: &cadkernel::brep::Body, color: [f32; 4]) -> Vec>(); + wires.extend( + mesh.isolines + .into_iter() + .filter(|line| line.positions.len() >= 2) + .map(|line| { + WireModel::solid_f64( + "REVOLVE-PREVIEW-ISOLINE".to_owned(), + line.positions, + color, + false, + ) + }), + ); + wires } impl CadCommand for ExtrudeCommand { @@ -626,6 +646,7 @@ pub struct RevolveCommand { angle: f64, start_angle: f64, reverse: bool, + isolines: usize, color: [f32; 4], } @@ -660,7 +681,7 @@ enum RevolveStep { } impl RevolveCommand { - pub fn new(color: [f32; 4]) -> Self { + pub fn new(color: [f32; 4], isolines: usize) -> Self { let defaults = *revolve_defaults().lock().unwrap_or_else(|error| error.into_inner()); Self { step: RevolveStep::Pick, @@ -675,6 +696,7 @@ impl RevolveCommand { angle: defaults.angle, start_angle: defaults.start_angle, reverse: false, + isolines, color, } } @@ -727,14 +749,29 @@ impl RevolveCommand { Some(self.axis_start + axis * (reference - self.axis_start).dot(axis)) } + fn profile_normal(&self) -> Option { + self.preview_profiles.iter().find_map(|(_, entity)| { + crate::scene::model::sweep_model::extrusion_profile_of(entity) + .and_then(|(profile, _)| profile.plane.normal()) + .map(DVec3::from_array) + .and_then(DVec3::try_normalize) + }) + } + fn cursor_angle(&self, cursor: DVec3, full_at_zero: bool) -> Option { - let axis = (self.axis_end - self.axis_start).try_normalize()?; let anchor = self.angle_anchor()?; let reference = (self.profile_reference()? - anchor).try_normalize()?; - let radial = (cursor - (self.axis_start - + axis * (cursor - self.axis_start).dot(axis))) - .try_normalize()?; - let mut angle = axis.dot(reference.cross(radial)).atan2(reference.dot(radial)); + // The picked cursor is projected onto the active/profile construction + // plane. Measuring it about the 3-D revolution axis collapses almost + // every ordinary mouse pick to 0 or 180 degrees, and 0 is the full-turn + // shortcut. Measure the scalar input in the profile plane instead; + // the resulting angle is still applied about the requested 3-D axis. + let normal = self.profile_normal().unwrap_or(self.working_plane.z); + let cursor_direction = cursor - anchor; + let radial = (cursor_direction - normal * cursor_direction.dot(normal)).try_normalize()?; + let mut angle = normal + .dot(reference.cross(radial)) + .atan2(reference.dot(radial)); if angle < 0.0 { angle += std::f64::consts::TAU; } @@ -792,7 +829,7 @@ impl RevolveCommand { start_angle, ) }; - body.map(|body| preview_body_wires(&body, self.color)) + body.map(|body| preview_body_wires(&body, self.color, self.isolines)) .unwrap_or_default() }) .collect() diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 89e035cf9..23c5bd1ee 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2723,14 +2723,28 @@ impl Scene { } /// Cache and display a Model-tab kernel solid. - pub fn register_solid_model(&mut self, handle: Handle, solid: cadkernel::brep::Body) { + pub fn register_solid_model( + &mut self, + handle: Handle, + solid: cadkernel::brep::Body, + ) -> bool { let color = self .document .get_entity(handle) .map(|e| self.render_style(e).0) .unwrap_or([0.8, 0.8, 0.85, 1.0]); - if let Some((mut set, wires, center)) = - crate::scene::model::solid_model::display_from_solid(&solid, color) + let facet_resolution = self.document.header.facet_resolution; + let chordal_deflection = + crate::entities::solid3d::display_deflection(&self.document.header, facet_resolution); + let isolines = self.document.header.isolines.max(0) as usize; + let displayed = if let Some((mut set, wires, center)) = + crate::scene::model::solid_model::display_from_solid( + &solid, + color, + facet_resolution, + chordal_deflection, + isolines, + ) { if let Some(entity) = self.document.get_entity_mut(handle) { let reference = acadrust::types::Vector3::new(center[0], center[1], center[2]); @@ -2772,15 +2786,18 @@ impl Scene { ); } self.meshes.insert(handle, set); + true } else { self.meshes.remove(&handle); - } + false + }; self.solid_models.insert(handle, solid); self.sync_solid_reference_point(handle); // Only this solid's mesh changed — report just its handle so the mesh / // wire caches patch it in rather than rebuilding the whole drawing (and // so a bulk register loop stays O(n), not O(n²)). self.bump_entities(&[(handle, ChangeKind::Modified)]); + displayed } /// `BACKGROUND` change picks up the new `adapt_to_bg` result without diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index a439e43cb..24969c146 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -18,6 +18,29 @@ fn tessellation(body: &Body) -> brep::mesh::BodyMesh { ) } +fn display_tessellation( + body: &Body, + facet_resolution: f64, + chordal_deflection: Option, + isolines: usize, +) -> brep::mesh::BodyMesh { + let resolution = if facet_resolution.is_finite() && facet_resolution > 0.0 { + facet_resolution.clamp(0.01, 10.0) + } else { + 1.0 + }; + let max_angle = chordal_deflection.map_or_else( + || cadkernel::tessellation::angle_for_resolution(resolution), + |_| cadkernel::tessellation::display_angle_for_resolution(resolution), + ); + let mut tolerance = brep::mesh::TessellationTolerance::new(max_angle, TOL) + .with_isolines(isolines); + if let Some(deflection) = chordal_deflection { + tolerance = tolerance.with_chordal_deflection(deflection); + } + brep::mesh::tessellate(body, tolerance) +} + /// Axis-aligned box from its center and full extents. pub fn box_solid(center: [f64; 3], length: f64, width: f64, height: f64) -> Option { brep::make::cuboid( @@ -397,8 +420,13 @@ fn mesh_from_tessellation( color, selected: false, }); - for edge in tessellation.edges { - for segment in edge.positions.windows(2) { + for positions in tessellation + .edges + .into_iter() + .map(|edge| edge.positions) + .chain(tessellation.isolines.into_iter().map(|line| line.positions)) + { + for segment in positions.windows(2) { for point in segment { let high = [point[0] as f32, point[1] as f32, point[2] as f32]; set.edge_verts.push(high); @@ -418,9 +446,17 @@ fn mesh_from_tessellation( pub fn display_from_solid( body: &Body, color: [f32; 4], + facet_resolution: f64, + chordal_deflection: Option, + isolines: usize, ) -> Option<(MeshLodSet, Vec, [f64; 3])> { use acadrust::types::Vector3; - let tessellation = tessellation(body); + let tessellation = display_tessellation( + body, + facet_resolution, + chordal_deflection, + isolines, + ); let center = mesh_center(&tessellation.mesh)?; let wires = tessellation .edges @@ -496,7 +532,7 @@ mod tests { use super::*; fn tri_count(body: &Body) -> usize { - display_from_solid(body, [0.7, 0.7, 0.7, 1.0]) + display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, 0) .map(|(m, _, _)| m.lods[0].indices.len() / 3) .unwrap_or(0) } diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 723a016d8..f2cc33654 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -781,7 +781,19 @@ impl Scene { // ── Erase ───────────────────────────────────────────────────────────── pub fn erase_entities(&mut self, handles: &[Handle]) { + self.remove_entities(handles, true); + } + /// Roll back entities created by the current command after a downstream + /// registration failure. These entities were never successful command + /// results, so locked-layer edit policy must not prevent their removal. + /// The shared removal path keeps first-touch undo state, history objects, + /// groups, selection, and derived caches coherent. + pub(crate) fn rollback_new_entities(&mut self, handles: &[Handle]) { + self.remove_entities(handles, false); + } + + fn remove_entities(&mut self, handles: &[Handle], respect_layer_locks: bool) { let erase_handles = self.handles_expanded_for_leader_annotations(handles); let mut handle_set: HashSet = HashSet::default(); @@ -791,7 +803,7 @@ impl Scene { for &h in &erase_handles { // Objects on a locked layer can't be erased. - if self.is_layer_locked(h) { + if respect_layer_locks && self.is_layer_locked(h) { continue; } // Delta-undo: capture the removed entity so an undo can re-insert it.