diff --git a/Cargo.lock b/Cargo.lock index 0e79115dc..f957a9daf 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=fb5a5c18f794df30e97c712fb5d4fe7c6629d1d5#fb5a5c18f794df30e97c712fb5d4fe7c6629d1d5" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=7d1fc67ec179ce9eac726efaedaa7c59d60e2416#7d1fc67ec179ce9eac726efaedaa7c59d60e2416" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index e7dbf0d58..3f9993cbf 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 = "fb5a5c18f794df30e97c712fb5d4fe7c6629d1d5", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "7d1fc67ec179ce9eac726efaedaa7c59d60e2416", 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/update/viewport.rs b/src/app/update/viewport.rs index c4023741f..368831900 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1685,14 +1685,21 @@ impl OpenCADStudio { } else { // Current deformed geometry. let mut preview = - self.tabs[i].scene.wire_models_for(&edited_handles); + self.tabs[i].scene.grip_wire_models_for(&edited_handles); // Also show the drag-start geometry as a faint ghost. // // This is display-only. Preview wires never participate in the resident // hit-test/snap set, so keeping the original shape visible does not // reintroduce self-snapping. - let mut reference = self.grip_reference_wires.clone(); + let keep_reference_overlay = edited_handles + .iter() + .any(|handle| self.tabs[i].scene.preview_hidden.contains(handle)); + let mut reference = if keep_reference_overlay { + self.grip_reference_wires.clone() + } else { + Vec::new() + }; for wire in &mut reference { wire.selected = false; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index b91d86657..89e035cf9 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1476,6 +1476,9 @@ pub struct Scene { pub selection_filter: HashSet, /// In-progress preview wires while a command is active (rubber-band + object ghosts). pub preview_wires: Vec, + /// Candidate wireframes produced by editable solid-history grips. The + /// resident body remains untouched until the grip is committed. + solid_history_preview_wires: HashMap>, /// Live hatch fill used by interactive edits such as dragging the pattern /// origin grip. Kept separate from the resident hatch set so moving one /// pattern never re-uploads every hatch in a large drawing. @@ -1883,6 +1886,7 @@ impl Scene { model_lineweight_scale: 1.0, selection_filter: HashSet::default(), preview_wires: vec![], + solid_history_preview_wires: HashMap::default(), preview_hatches: Arc::new(Vec::new()), preview_text: vec![], interim_wire: None, diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index c32d63dd1..31d75a372 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -2314,7 +2314,7 @@ fn apply_extrusion_draft_grip(value: &mut SolidHistorySweep, apply: GripApply) - let GripApply::Absolute(world) = apply else { return false; }; - let Some((_, base_top, radial, profile_radius, height)) = extrusion_draft_grip(value) else { + let Some((_, base_top, radial, _, height)) = extrusion_draft_grip(value) else { return false; }; let top_offset = (world - base_top).dot(radial); @@ -2322,10 +2322,27 @@ fn apply_extrusion_draft_grip(value: &mut SolidHistorySweep, apply: GripApply) - return false; } let limit = 89.0_f64.to_radians(); - let inward_limit = ((profile_radius - 1e-6).max(1e-6) / height) - .atan() - .min(limit); - let angle = (-top_offset).atan2(height).clamp(-limit, inward_limit); + let desired = (-top_offset).atan2(height).clamp(-limit, limit); + let angle = if desired > 0.0 && !extrusion_draft_rebuilds(value, desired) { + // The maximum inset is profile-dependent. A corner radius is too + // restrictive for many outlines, while allowing the pointer through + // a collapsed outline makes the top grow again. Search the actual + // rebuild boundary so every supported profile reaches its smallest + // valid top without crossing into an inverted result. + let mut valid = 0.0; + let mut invalid = limit; + for _ in 0..16 { + let candidate = (valid + invalid) * 0.5; + if extrusion_draft_rebuilds(value, candidate) { + valid = candidate; + } else { + invalid = candidate; + } + } + valid + } else { + desired + }; if !angle.is_finite() || (angle - value.draft_angle).abs() <= 1e-12 { return false; } @@ -2333,6 +2350,12 @@ fn apply_extrusion_draft_grip(value: &mut SolidHistorySweep, apply: GripApply) - true } +fn extrusion_draft_rebuilds(value: &SolidHistorySweep, angle: f64) -> bool { + let mut candidate = value.clone(); + candidate.draft_angle = angle; + cadkernel::acis::rebuild_body(&SolidHistoryOperation::Extrusion(candidate)).is_ok() +} + fn grip( id: usize, world: glam::DVec3, diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index 582037219..a439e43cb 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -3,6 +3,7 @@ use cadkernel::brep::{self, Body, Curve3, EdgeKey, FaceKey, Surface}; use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; +use crate::scene::model::wire_model::WireModel; /// What counts as the same point when the kernel checks a body over. const TOL: f64 = 1e-9; @@ -243,6 +244,27 @@ pub fn edge_wires(body: &Body) -> Vec { .collect() } +/// White wireframe used while a solid-history grip is hot. +/// +/// This deliberately does not touch the resident solid mesh or its entity +/// wires: the selected source stays visible in blue while the candidate body +/// is presented as a separate, non-pickable outline until placement. +pub fn grip_preview_wires(body: &Body, handle: acadrust::Handle) -> Vec { + tessellation(body) + .edges + .into_iter() + .filter(|edge| edge.positions.len() >= 2) + .map(|edge| { + WireModel::solid_f64( + format!("{}-GRIP-PREVIEW", handle.value()), + edge.positions, + WireModel::WHITE, + false, + ) + }) + .collect() +} + /// B-rep edge nearest a world-space surface pick. pub fn nearest_edge(body: &Body, pick: [f64; 3]) -> Option { let pick = cadkernel::space::Vec3::from(pick); diff --git a/src/scene/modify.rs b/src/scene/modify.rs index e3cc388d4..6c43923a9 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -841,6 +841,7 @@ impl Scene { handle: Handle, operation: acadrust::objects::SolidHistoryOperation, ) -> bool { + self.solid_history_preview_wires.remove(&handle); let Ok(body) = cadkernel::acis::rebuild_body(&operation) else { return false; }; @@ -879,6 +880,7 @@ impl Scene { }; entity.set_sat_document(&document); self.sync_solid_reference_point(handle); + self.solid_history_preview_wires.remove(&handle); self.register_solid_model(handle, body); true } @@ -901,7 +903,10 @@ impl Scene { let Some(EntityType::Solid3D(_)) = self.document.get_entity(handle) else { return false; }; - self.register_solid_model(handle, body); + self.solid_history_preview_wires.insert( + handle, + crate::scene::model::solid_model::grip_preview_wires(&body, handle), + ); true } diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 10abfa5ad..93b91c9cf 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -272,6 +272,7 @@ impl Scene { // flips the wire content id back to the base tessellation id, which // re-uploads the base wires (without the preview) on the next frame. self.preview_wires = vec![]; + self.solid_history_preview_wires.clear(); if !self.preview_hatches.is_empty() { self.preview_hatches = std::sync::Arc::new(Vec::new()); } @@ -302,6 +303,21 @@ impl Scene { .collect() } + /// Current candidate geometry for a hot grip. Solid-history candidates + /// live outside the document until placement; ordinary entities continue + /// to use their current tessellation. + pub fn grip_wire_models_for(&self, handles: &[acadrust::Handle]) -> Vec { + handles + .iter() + .flat_map(|handle| { + self.solid_history_preview_wires + .get(handle) + .cloned() + .unwrap_or_else(|| self.wire_models_for(std::slice::from_ref(handle))) + }) + .collect() + } + /// Split a MIRROR selection into plain ghost wires (reflected wholesale) and /// text ghosts paired with their bounding-box centre. Lets the preview match /// the commit for TEXT: MIRRTEXT on → true glyph mirror (full reflection, same