diff --git a/Cargo.lock b/Cargo.lock index a65881e4..ee4656dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ dependencies = [ [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=1ece83a#1ece83af08a93bf84cac16f919a4b274298c8c59" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=74c5fe9#74c5fe9faafd3d573745a7e9559072dfb66adb62" dependencies = [ "ahash 0.8.12", "anyhow", @@ -890,7 +890,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=bbd6688#bbd6688f4cdffae3e69787ab0a8e5a2b9aed60ee" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=f85efb6#f85efb659741f8d19d6bf750f3234aa333826926" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 16487934..3af5aad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,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 = "1ece83a", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "bbd6688", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "74c5fe9", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "f85efb6", features = ["acis", "offset"] } acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index d91406bb..3fea1363 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -9,7 +9,7 @@ license = "GPL-3.0-only" # 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 = "1ece83a", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "74c5fe9", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 0fbdefd9..7916d995 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 = "1ece83a", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "74c5fe9", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b78c5db6..025581bc 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -107,11 +107,25 @@ impl OpenCADStudio { /// Roll a hot grip back to its pre-drag image and remove every grip-owned /// overlay. Shared by Escape and drawing-space transitions. + pub(super) fn capture_grip_history_originals(&mut self, i: usize, handles: &[Handle]) { + if !self.grip_history_originals.is_empty() { + return; + } + self.grip_history_originals = handles + .iter() + .filter_map(|&handle| { + let objects = self.tabs[i].scene.solid_history_objects(handle); + (!objects.is_empty()).then_some((handle, objects)) + }) + .collect(); + } + pub(super) fn cancel_active_grip_edit(&mut self) -> bool { let i = self.active_tab; let had_grip = self.tabs[i].active_grip.take().is_some() || self.grip_add_provisional.is_some() - || !self.grip_preview_handles.is_empty(); + || !self.grip_preview_handles.is_empty() + || !self.grip_history_originals.is_empty(); if !had_grip { return false; } @@ -132,31 +146,62 @@ impl OpenCADStudio { let handles = std::mem::take(&mut self.grip_preview_handles); let originals = std::mem::take(&mut self.grip_originals); + let history_originals = std::mem::take(&mut self.grip_history_originals); + let history_handles: rustc_hash::FxHashSet<_> = history_originals + .iter() + .map(|(handle, _)| *handle) + .collect(); + for (_, objects) in history_originals { + for (object_handle, object) in objects { + self.tabs[i] + .scene + .document + .objects + .insert(object_handle, object); + } + } let mut changed_handles: rustc_hash::FxHashSet<_> = handles.iter().copied().collect(); for (handle, original) in originals { changed_handles.insert(handle); - let current = self.tabs[i] - .scene - .document - .get_entity(handle) - .and_then(crate::entities::solid3d::point_of_reference) - .map(|point| [point.x, point.y, point.z]); - let target = crate::entities::solid3d::point_of_reference(&original) - .map(|point| [point.x, point.y, point.z]); - if let (Some(current), Some(target)) = (current, target) { - self.tabs[i].scene.translate_solid_geometry( - handle, - [ - target[0] - current[0], - target[1] - current[1], - target[2] - current[2], - ], - ); + if !history_handles.contains(&handle) { + let current = self.tabs[i] + .scene + .document + .get_entity(handle) + .and_then(crate::entities::solid3d::point_of_reference) + .map(|point| [point.x, point.y, point.z]); + let target = crate::entities::solid3d::point_of_reference(&original) + .map(|point| [point.x, point.y, point.z]); + if let (Some(current), Some(target)) = (current, target) { + self.tabs[i].scene.translate_solid_geometry( + handle, + [ + target[0] - current[0], + target[1] - current[1], + target[2] - current[2], + ], + ); + } } if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { *entity = original; } } + for handle in history_handles { + let restored = self.tabs[i] + .scene + .document + .solid_history_operation(handle) + .cloned() + .is_some_and(|operation| { + self.tabs[i] + .scene + .rebuild_solid_history(handle, operation) + }); + if !restored { + self.tabs[i].scene.reseed_derived_caches(handle); + } + } for &handle in &handles { self.tabs[i].scene.preview_hidden.remove(&handle); } @@ -962,13 +1007,6 @@ impl OpenCADStudio { // transform_entities — always delta-safe. let pending = self.begin_undo(i, label, handles.len(), true); self.tabs[i].scene.transform_entities(&handles, &transform); - // ACIS solids render from a cached mesh, so a move/rotate/ - // scale/mirror needs the mesh re-tessellated from the now-moved - // body — wire re-tessellation alone leaves the solid drawn at - // its old spot. (#135) - if self.tabs[i].scene.any_solid(&handles) { - self.tabs[i].scene.refresh_meshes_for_handles(&handles); - } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; @@ -986,9 +1024,6 @@ impl OpenCADStudio { let delta_safe = self.delta_copy_safe(i, &handles); let pending = self.begin_undo(i, label, handles.len(), delta_safe); let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform); - if self.tabs[i].scene.any_solid(&new_handles) { - self.tabs[i].scene.refresh_meshes_for_handles(&new_handles); - } self.tabs[i].dirty = true; self.tabs[i].scene.deselect_all(); for h in new_handles { @@ -2689,7 +2724,6 @@ impl OpenCADStudio { let new_handle = self.tabs[i].scene.add_entity(s3d); self.tabs[i] .scene - .document .create_solid_history(new_handle, history); self.tabs[i].scene.register_solid_model(new_handle, solid); let _ = mesh; @@ -2752,7 +2786,6 @@ impl OpenCADStudio { let new_handle = self.tabs[i].scene.add_entity(s3d); self.tabs[i] .scene - .document .create_solid_history(new_handle, history); self.tabs[i].scene.register_solid_model(new_handle, solid); let _ = mesh; @@ -2813,7 +2846,6 @@ impl OpenCADStudio { if let Some(history) = history { self.tabs[i] .scene - .document .create_solid_history(new_handle, history); } for mesh in &mut set.lods { @@ -2856,7 +2888,6 @@ impl OpenCADStudio { let new_handle = self.tabs[i].scene.add_entity(entity); self.tabs[i] .scene - .document .create_solid_history(new_handle, history); for mesh in &mut set.lods { mesh.name = format!("{}", new_handle.value()); diff --git a/src/app/history.rs b/src/app/history.rs index 2a06a1a6..342f7604 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -189,6 +189,7 @@ impl OpenCADStudio { i: usize, label: impl Into, before: Vec<(Handle, Arc)>, + object_before: Vec<(Handle, acadrust::objects::ObjectType)>, dirty_before: bool, ) { self.finish_pending_history(i); @@ -199,7 +200,18 @@ impl OpenCADStudio { Some((handle, Some(original), Some(after))) }) .collect(); - if entities.is_empty() { + let objects: Vec<_> = object_before + .into_iter() + .filter_map(|(handle, before)| { + let after = self.tabs[i].scene.document.objects.get(&handle).cloned(); + (Some(before.clone()) != after).then_some(ObjectEntryDelta { + handle, + before: Some(before), + after, + }) + }) + .collect(); + if entities.is_empty() && objects.is_empty() { return; } let selected: Vec = self.tabs[i].scene.selected.iter().copied().collect(); @@ -211,7 +223,7 @@ impl OpenCADStudio { selected_after: selected, dirty_before, dirty_after: true, - structure: None, + structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)), label: label.into(), }; self.push_undo_entry(i, HistorySnapshot::Delta(delta)); diff --git a/src/app/mod.rs b/src/app/mod.rs index 5b699c98..1d8cea4e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -543,6 +543,11 @@ pub(super) struct OpenCADStudio { /// Snapshots of edited entities taken at the start of a grip drag. The drag /// mutates the document live, so Escape restores this group atomically. grip_originals: Vec<(acadrust::Handle, acadrust::EntityType)>, + /// Solid-history objects paired with their owning entity before a grip drag. + grip_history_originals: Vec<( + acadrust::Handle, + Vec<(acadrust::Handle, acadrust::objects::ObjectType)>, + )>, /// Document dirty state before the live grip mutation began. grip_dirty_before: Option, /// Frozen wire geometry of the entities being grip-edited. @@ -3136,6 +3141,7 @@ impl OpenCADStudio { grip_preview_handles: Vec::new(), hover_dwell: None, grip_originals: Vec::new(), + grip_history_originals: Vec::new(), grip_dirty_before: None, grip_snap_wires: Vec::new(), grip_text_verts: Vec::new(), diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 90e2a6a6..53efe862 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -31,10 +31,7 @@ impl super::OpenCADStudio { let Some(handle) = self.commit_entity_handle(entity) else { return Handle::NULL; }; - self.tabs[i] - .scene - .document - .create_solid_history(handle, history); + self.tabs[i].scene.create_solid_history(handle, history); self.tabs[i].scene.register_solid_model(handle, solid); handle } diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 10acd718..acf512be 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -369,6 +369,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .collect(); } + self.capture_grip_history_originals(i, &edited_handles); + self.grip_preview_handles = edited_handles; } let delta = target - grip.last_world; diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index b49efe2c..bffa44b5 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1070,6 +1070,7 @@ impl OpenCADStudio { }) .collect(); } + self.capture_grip_history_originals(i, &edited_handles); for &handle in &edited_handles { if !self.tabs[i].scene.meshes.contains_key(&handle) { self.tabs[i].scene.preview_hidden.insert(handle); @@ -2733,6 +2734,7 @@ impl OpenCADStudio { // edited entity back into the resident tessellation. let handles = std::mem::take(&mut self.grip_preview_handles); let originals = std::mem::take(&mut self.grip_originals); + let history_originals = std::mem::take(&mut self.grip_history_originals); let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty); if !handles.is_empty() { if !originals.is_empty() { @@ -2743,6 +2745,10 @@ impl OpenCADStudio { .into_iter() .map(|(handle, entity)| (handle, std::sync::Arc::new(entity))) .collect(), + history_originals + .into_iter() + .flat_map(|(_, objects)| objects) + .collect(), dirty_before, ); self.tabs[i].dirty = true; diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 634ece4f..e87c66a1 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -475,6 +475,11 @@ impl Scene { self.images.insert(handle, model); } self.refresh_meshes_for_handles(&[handle]); + if let Some(operation) = self.document.solid_history_operation(handle).cloned() { + if let Ok(body) = cadkernel::acis::rebuild_body(&operation) { + self.solid_models.insert(handle, body); + } + } } /// Re-tessellate only the named ACIS entities. The former edit path diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f6ef8219..7fbdc0c9 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -7626,24 +7626,6 @@ impl Scene { handles } - /// True when any handle resolves to an ACIS volume entity (3D solid / - /// region / body / surface) — i.e. one whose render geometry is a cached - /// mesh that must be re-tessellated after an edit. - pub fn any_solid(&self, handles: &[Handle]) -> bool { - handles.iter().any(|&h| { - matches!( - self.document.get_entity(h), - Some(EntityType::Solid3D(_)) - | Some(EntityType::Region(_)) - | Some(EntityType::Body(_)) - | Some(EntityType::Surface(_)) - | Some(EntityType::Mesh(_)) - | Some(EntityType::PolygonMesh(_)) - | Some(EntityType::PolyfaceMesh(_)) - ) - }) - } - /// Top-level solid handles caught by a rectangular selection box. pub fn mesh_box_hit( &self, diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 831873bd..6b72397d 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -9,6 +9,7 @@ use acadrust::EntityType; use cadkernel::brep::Body; use crate::scene::model::object::{GripApply, GripDef, GripShape}; +use crate::command::EntityTransform; pub const GRIP_LENGTH: usize = 10_001; pub const GRIP_WIDTH: usize = 10_002; @@ -23,6 +24,71 @@ fn matrix(transform: [f64; 16]) -> Option { (matrix.is_finite() && matrix.determinant().abs() > 1e-12).then_some(matrix) } +fn codec_matrix(transform: &acadrust::types::Transform) -> glam::DMat4 { + let matrix = transform.matrix.m; + glam::DMat4::from_cols_array(&[ + matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0], + matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1], + matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2], + matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3], + ]) +} + +fn transform_matrix(transform: &EntityTransform) -> Option { + Some(match transform { + EntityTransform::Translate(delta) => glam::DMat4::from_translation(*delta), + EntityTransform::Rotate { + center, + axis, + angle_rad, + } => { + let axis = axis.normalize_or_zero(); + if axis.length_squared() <= 1e-12 { + return None; + } + glam::DMat4::from_translation(*center) + * glam::DMat4::from_axis_angle(axis, *angle_rad) + * glam::DMat4::from_translation(-*center) + } + EntityTransform::Scale { center, factor } => { + glam::DMat4::from_translation(*center) + * glam::DMat4::from_scale(glam::DVec3::splat(*factor)) + * glam::DMat4::from_translation(-*center) + } + EntityTransform::Mirror { + p1, + p2, + working_normal, + } => codec_matrix(&crate::scene::view::transform::reflection_about_working_line( + *p1, + *p2, + *working_normal, + )), + EntityTransform::Affine(value) => codec_matrix(value), + }) +} + +pub fn transform_operation( + operation: &mut SolidHistoryOperation, + transform: &EntityTransform, +) -> bool { + let Some(base) = operation.base_mut() else { + return false; + }; + let Some(current) = matrix(base.transform) else { + return false; + }; + let Some(by) = transform_matrix(transform) else { + return false; + }; + let transformed = by * current; + if !transformed.is_finite() || transformed.determinant().abs() <= 1e-12 { + return false; + } + base.transform = transformed.to_cols_array(); + true +} + fn world_point(transform: [f64; 16], point: [f64; 3]) -> Option { Some(matrix(transform)?.transform_point3(glam::DVec3::from_array(point))) } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index fb76d34f..158f2b64 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -214,6 +214,7 @@ impl Scene { }) .flatten() .collect(); + let mut refresh_solid_handles = Vec::new(); for &h in handles { // Delta-undo: capture the pre-transform image before mutating. if self.is_recording_undo() { @@ -226,6 +227,20 @@ impl Scene { mirror_true_text_flags(entity); } } + let rebuilt_history = self.transform_solid_history(h, t); + if !rebuilt_history + && self.document.get_entity(h).is_some_and(|entity| { + matches!( + entity, + EntityType::Solid3D(_) + | EntityType::Region(_) + | EntityType::Body(_) + | EntityType::Surface(_) + ) + }) + { + refresh_solid_handles.push(h); + } if self.hatches.contains_key(&h) { let existing_color = self.hatches[&h].color; let new_model = match self.document.get_entity(h) { @@ -271,6 +286,7 @@ impl Scene { let changes: Vec<(Handle, ChangeKind)> = handles.iter().map(|&h| (h, ChangeKind::Modified)).collect(); self.bump_entities(&changes); + self.refresh_meshes_for_handles(&refresh_solid_handles); } /// Entities in anonymous dimension blocks that visually belong to @@ -550,6 +566,7 @@ impl Scene { matches!(t, EntityTransform::Mirror { .. }) && self.document.header.mirror_text; let mut new_handles = Vec::with_capacity(clones.len()); let mut handle_map = rustc_hash::FxHashMap::default(); + let mut refresh_solid_handles = Vec::new(); for (src_handle, mut entity) in clones { let text_orient = if preserve_text_orientation { capture_text_orient(&entity) @@ -602,6 +619,21 @@ impl Scene { if let Some(model) = new_model { self.hatches.insert(h, model); } + let rebuilt_history = self.copy_solid_history(src_handle, h) + && self.transform_solid_history(h, t); + if !rebuilt_history + && self.document.get_entity(h).is_some_and(|entity| { + matches!( + entity, + EntityType::Solid3D(_) + | EntityType::Region(_) + | EntityType::Body(_) + | EntityType::Surface(_) + ) + }) + { + refresh_solid_handles.push(h); + } } new_handles.push(h); if !h.is_null() { @@ -620,11 +652,71 @@ impl Scene { .map(|&h| (h, ChangeKind::Added)) .collect(); self.bump_entities(&changes); + self.refresh_meshes_for_handles(&refresh_solid_handles); new_handles } // ── Grip editing ────────────────────────────────────────────────────── + pub(crate) fn solid_history_objects( + &self, + handle: Handle, + ) -> Vec<(Handle, acadrust::objects::ObjectType)> { + let Some(graph) = self.document.solid_history_graph(handle) else { + return Vec::new(); + }; + std::iter::once(graph.root) + .chain(graph.nodes) + .filter_map(|object_handle| { + self.document + .objects + .get(&object_handle) + .cloned() + .map(|object| (object_handle, object)) + }) + .collect() + } + + fn record_solid_history_before(&mut self, handle: Handle) { + if !self.is_recording_undo() { + return; + } + for (object_handle, object) in self.solid_history_objects(handle) { + self.record_undo_object_before(object_handle, Some(object)); + } + } + + pub fn create_solid_history( + &mut self, + handle: Handle, + operation: acadrust::objects::SolidHistoryOperation, + ) -> bool { + let Some(graph) = self.document.create_solid_history(handle, operation) else { + return false; + }; + self.record_undo_object_before(graph.root, None); + for node in graph.nodes { + self.record_undo_object_before(node, None); + } + true + } + + fn copy_solid_history(&mut self, source: Handle, target: Handle) -> bool { + let Some(graph) = self.document.copy_solid_history(source, target) else { + return false; + }; + self.record_undo_object_before(graph.root, None); + for node in graph.nodes { + self.record_undo_object_before(node, None); + } + true + } + + pub(crate) fn delete_solid_history(&mut self, handle: Handle) { + self.record_solid_history_before(handle); + self.document.delete_solid_history(handle); + } + pub fn rebuild_solid_history( &mut self, handle: Handle, @@ -637,6 +729,7 @@ impl Scene { return false; }; let wires = crate::scene::model::solid_model::edge_wires(&body); + self.record_solid_history_before(handle); if self .document .update_solid_history(handle, operation) @@ -654,6 +747,28 @@ impl Scene { true } + fn transform_solid_history( + &mut self, + handle: Handle, + transform: &EntityTransform, + ) -> bool { + let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { + return false; + }; + if !crate::scene::model::solid_history::transform_operation( + &mut operation, + transform, + ) { + return false; + } + if self.rebuild_solid_history(handle, operation.clone()) { + return true; + } + self.record_solid_history_before(handle); + let _ = self.document.update_solid_history(handle, operation); + false + } + fn apply_solid_history_grip( &mut self, handle: Handle, @@ -764,7 +879,13 @@ impl Scene { .map(|p| [p.x, p.y, p.z]); if let Some(new) = new_por { let delta = [new[0] - old[0], new[1] - old[1], new[2] - old[2]]; - self.translate_solid_geometry(handle, delta); + let moved_history = self.transform_solid_history( + handle, + &EntityTransform::Translate(glam::DVec3::from_array(delta)), + ); + if !moved_history { + self.translate_solid_geometry(handle, delta); + } } } diff --git a/src/scene/selection.rs b/src/scene/selection.rs index ced46be3..55728cd5 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -631,6 +631,7 @@ impl Scene { let before = self.document.get_entity_arc(h); self.record_undo_before(h, before); } + self.delete_solid_history(h); self.remember_removed_cache_categories(h); self.document.remove_entity_arc(h); highlight_changed |= self.selected.remove(&h);