From a24d1e610887fe64a5be47c312891b10d43367c4 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:53:48 +0300 Subject: [PATCH] Fix live extrusion surface grip editing --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/ocs_doc_api/Cargo.toml | 2 +- src/app/update/viewport.rs | 53 +++++++++++++++++++++++++------ src/modules/model/edge_cmd.rs | 2 +- src/scene/convert/acis_kernel.rs | 4 ++- src/scene/convert/solid3d_tess.rs | 8 +++++ src/scene/mod.rs | 5 +++ src/scene/model/solid_history.rs | 19 ++++++++--- src/scene/model/solid_model.rs | 41 +++++++++++++++++++----- src/scene/modify.rs | 10 +++++- 11 files changed, 121 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10aaeed9d..d77046e7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=dfd0de176a878c20a1aeb8d23b0b8265197b5f5c#dfd0de176a878c20a1aeb8d23b0b8265197b5f5c" +source = "git+https://github.com/ramox81/cadkernel.git?rev=c584d19dc45b3bef5146d6cf2e3dae79b72e9468#c584d19dc45b3bef5146d6cf2e3dae79b72e9468" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 49870b58e..f7a8295a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "c584d19dc45b3bef5146d6cf2e3dae79b72e9468", 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_doc_api/Cargo.toml b/crates/ocs_doc_api/Cargo.toml index 592effe98..4c902ed91 100644 --- a/crates/ocs_doc_api/Cargo.toml +++ b/crates/ocs_doc_api/Cargo.toml @@ -15,7 +15,7 @@ thiserror = "1" # Host adapters convert ObjectId to acadrust handles; kernel handles stay local. acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"], optional = true } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", features = ["acis", "offset"], optional = true } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "c584d19dc45b3bef5146d6cf2e3dae79b72e9468", features = ["acis", "offset"], optional = true } ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] } [build-dependencies] diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index df4c7a11d..96eeff041 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1535,15 +1535,30 @@ impl OpenCADStudio { } if let Some(axis) = grip.axis { - snapped = cursor_on_projected_axis( - p, - bounds, - view_rot, - eye, - grip.origin_world, - axis, - ) - .unwrap_or(snapped); + let acquired_point = otrack_hit.is_some() + || snap_hit.is_some_and(|hit| { + hit.snap_type != crate::snap::SnapType::Grid + }); + if acquired_point { + if let Some(axis) = axis.try_normalize() { + // An axis grip still moves on its construction axis, but + // an acquired object point supplies the exact coordinate + // along that axis. This lets height and draft arrows seat + // back onto existing surface/solid geometry. + snapped = grip.origin_world + + axis * (snapped - grip.origin_world).dot(axis); + } + } else { + snapped = cursor_on_projected_axis( + p, + bounds, + view_rot, + eye, + grip.origin_world, + axis, + ) + .unwrap_or(snapped); + } } let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0; @@ -1751,6 +1766,26 @@ impl OpenCADStudio { let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0; let geometry_ms = grip_started.elapsed().as_secs_f64() * 1000.0; self.refresh_selected_grips(); + // A history rebuild can clamp a requested value or move a derived + // construction handle non-linearly. Rebase each active target onto + // the freshly rebuilt grip so the hot arrow remains attached to the + // live surface and reverses immediately from a geometric limit. + let refreshed_targets: Vec<_> = self.tabs[i] + .selected_grip_handles + .iter() + .copied() + .zip(self.tabs[i].selected_grips.iter()) + .map(|(handle, grip)| ((handle, grip.id), grip.world)) + .collect(); + if let Some(active) = self.tabs[i].active_grip.as_mut() { + for target in &mut active.targets { + if let Some((_, world)) = refreshed_targets.iter().find(|((handle, id), _)| { + *handle == target.handle && *id == target.grip_id + }) { + target.last_world = *world; + } + } + } let grips_ms = grip_started.elapsed().as_secs_f64() * 1000.0 - geometry_ms; // Properties are refreshed when the grip is committed or cancelled. // Rebuilding the inspector on every pointer event adds no drawing diff --git a/src/modules/model/edge_cmd.rs b/src/modules/model/edge_cmd.rs index ae7cb1303..59a8a9c4b 100644 --- a/src/modules/model/edge_cmd.rs +++ b/src/modules/model/edge_cmd.rs @@ -210,7 +210,7 @@ impl SolidEdgeCommand { ) .ok()?, }; - let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle); + let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle, 0); for wire in &mut wires { wire.color = self.preview_color; wire.name = format!("{}-{}-PREVIEW", handle.value(), self.name()); diff --git a/src/scene/convert/acis_kernel.rs b/src/scene/convert/acis_kernel.rs index c21c69364..4edc63268 100644 --- a/src/scene/convert/acis_kernel.rs +++ b/src/scene/convert/acis_kernel.rs @@ -25,6 +25,7 @@ pub fn tessellate_sat( facet_res: f64, chordal_deflection: Option, isolines: usize, + planar_isolines: bool, ) -> Option { let (bodies, loss) = lift(document); if bodies.is_empty() { @@ -114,7 +115,8 @@ pub fn tessellate_sat( max_angle, source_fit * placement_scale, ) - .with_isolines(isolines); + .with_isolines(isolines) + .with_planar_isolines(planar_isolines); if let Some(deflection) = chordal_deflection { tolerance = tolerance.with_chordal_deflection(deflection); } diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 598627e8c..3015b4a7f 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -11,6 +11,7 @@ fn tessellate_acis( facet_res: f64, chordal_deflection: Option, isolines: usize, + planar_isolines: bool, ) -> Option { crate::scene::convert::acis_kernel::tessellate_sat( sat, @@ -19,6 +20,7 @@ fn tessellate_acis( facet_res, chordal_deflection, isolines, + planar_isolines, ) } @@ -219,6 +221,7 @@ fn finish( facet_res: f64, chordal_deflection: Option, isolines: usize, + planar_isolines: bool, acis: &acadrust::entities::AcisData, ) -> Option { let mut set = tessellate_acis( @@ -228,6 +231,7 @@ fn finish( facet_res, chordal_deflection, isolines, + planar_isolines, )?; remap_acis_material_bindings(&mut set, acis); Some(set) @@ -252,6 +256,7 @@ pub fn tessellate_region( facet_res, chordal_deflection, isolines, + false, ®ion.acis_data, ) } @@ -275,6 +280,7 @@ pub fn tessellate_body( facet_res, chordal_deflection, isolines, + false, &body.acis_data, ) } @@ -298,6 +304,7 @@ pub fn tessellate_surface( facet_res, chordal_deflection, isolines, + true, &surface.acis_data, ) } @@ -321,6 +328,7 @@ pub fn tessellate_solid3d( facet_res, chordal_deflection, isolines, + false, &solid.acis_data, ) } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f136d16e3..bd2cf4fb0 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3083,6 +3083,10 @@ impl Scene { let chordal_deflection = crate::entities::solid3d::display_deflection(&self.document.header, facet_resolution); let isolines = self.document.header.isolines.max(0) as usize; + let planar_isolines = matches!( + self.document.get_entity(handle), + Some(EntityType::Surface(_)) + ); let (mut set, wires, center) = crate::scene::model::solid_model::display_from_solid( solid, @@ -3090,6 +3094,7 @@ impl Scene { facet_resolution, chordal_deflection, isolines, + planar_isolines, )?; let name = handle.value().to_string(); for mesh in &mut set.lods { diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index a11274ea4..ba6027255 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -3537,21 +3537,32 @@ pub fn primitive_grips( if let (Some(center), Some(base)) = (profile_center, matrix(value.base.transform)) { let direction = base.transform_vector3(glam::DVec3::from_array(direction)); if direction.length_squared() > 1e-12 { - grips.push(grip( + let mut height_grip = grip( GRIP_HEIGHT, center + direction, GripShape::Triangle, Some(direction.normalize()), - )); + ); + if is_surface { + // Surface construction arrows keep one stable screen + // orientation while their world-space axis continues to + // constrain the edit. + height_grip.dir = None; + } + grips.push(height_grip); } } if let Some((world, _, radial, _, _)) = extrusion_draft_grip(value) { - grips.push(grip( + let mut draft_grip = grip( GRIP_DRAFT, world, GripShape::Triangle, Some(radial), - )); + ); + if is_surface { + draft_grip.dir = None; + } + grips.push(draft_grip); } } } diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index fedae9365..fadc09389 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -23,6 +23,7 @@ fn display_tessellation( facet_resolution: f64, chordal_deflection: Option, isolines: usize, + planar_isolines: bool, ) -> brep::mesh::BodyMesh { let resolution = if facet_resolution.is_finite() && facet_resolution > 0.0 { facet_resolution.clamp(0.01, 10.0) @@ -34,7 +35,8 @@ fn display_tessellation( |_| cadkernel::tessellation::display_angle_for_resolution(resolution), ); let mut tolerance = brep::mesh::TessellationTolerance::new(max_angle, TOL) - .with_isolines(isolines); + .with_isolines(isolines) + .with_planar_isolines(planar_isolines); if let Some(deflection) = chordal_deflection { tolerance = tolerance.with_chordal_deflection(deflection); } @@ -267,20 +269,41 @@ pub fn edge_wires(body: &Body) -> Vec { .collect() } -/// White wireframe used while a solid-history grip is hot. +/// White wireframe used while a solid-history grip is hot. Surface previews +/// include the requested construction isolines so their cage follows the edit. /// /// 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) +pub fn grip_preview_wires( + body: &Body, + handle: acadrust::Handle, + isolines: usize, +) -> Vec { + let tessellation = brep::mesh::tessellate( + body, + brep::mesh::TessellationTolerance::new( + cadkernel::tessellation::DEFAULT_ANGLE, + TOL, + ) + .with_isolines(isolines) + .with_planar_isolines(isolines > 0), + ); + tessellation .edges .into_iter() - .filter(|edge| edge.positions.len() >= 2) - .map(|edge| { + .map(|edge| edge.positions) + .chain( + tessellation + .isolines + .into_iter() + .map(|isoline| isoline.positions), + ) + .filter(|positions| positions.len() >= 2) + .map(|positions| { WireModel::solid_f64( format!("{}-GRIP-PREVIEW", handle.value()), - edge.positions, + positions, WireModel::WHITE, false, ) @@ -454,6 +477,7 @@ pub fn display_from_solid( facet_resolution: f64, chordal_deflection: Option, isolines: usize, + planar_isolines: bool, ) -> Option<(MeshLodSet, Vec, [f64; 3])> { use acadrust::types::Vector3; let tessellation = display_tessellation( @@ -461,6 +485,7 @@ pub fn display_from_solid( facet_resolution, chordal_deflection, isolines, + planar_isolines, ); let center = mesh_center(&tessellation.mesh)?; let wires = tessellation @@ -541,7 +566,7 @@ mod tests { use super::*; fn tri_count(body: &Body) -> usize { - display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, 0) + display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, 0, false) .map(|(m, _, _)| m.lods[0].indices.len() / 3) .unwrap_or(0) } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 0b9fdc6a3..edd7195d5 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1043,7 +1043,15 @@ impl Scene { } self.solid_history_preview_wires.insert( handle, - crate::scene::model::solid_model::grip_preview_wires(&body, handle), + crate::scene::model::solid_model::grip_preview_wires( + &body, + handle, + if matches!(self.document.get_entity(handle), Some(EntityType::Surface(_))) { + self.document.header.isolines.max(0) as usize + } else { + 0 + }, + ), ); true }