// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged. use super::*; /// Orientation fields of a text-like entity, captured so a mirror can keep the /// glyphs right-reading when MIRRTEXT (`header.mirror_text`) is off. struct TextOrient { rotation: f64, oblique: f64, x_scale: f64, } fn inverse_affine(transform: &codec::types::Transform) -> Option { use codec::types::{Matrix3, Matrix4, Transform, Vector3}; let matrix = &transform.matrix.m; let linear = Matrix3::from_rows( [matrix[0][0], matrix[0][1], matrix[0][2]], [matrix[1][0], matrix[1][1], matrix[1][2]], [matrix[2][0], matrix[2][1], matrix[2][2]], ); let inverse = linear.inverse()?; let translation = Vector3::new(matrix[0][3], matrix[1][3], matrix[2][3]); let inverse_translation = inverse * (translation * -1.0); Some(Transform::from_matrix(Matrix4 { m: [ [ inverse.m[0][0], inverse.m[0][1], inverse.m[0][2], inverse_translation.x, ], [ inverse.m[1][0], inverse.m[1][1], inverse.m[1][2], inverse_translation.y, ], [ inverse.m[2][0], inverse.m[2][1], inverse.m[2][2], inverse_translation.z, ], [0.0, 0.0, 0.0, 1.0], ], })) } /// Snapshot the orientation of a text / mtext / shape entity, or `None` for any /// other kind. fn capture_text_orient(e: &EntityType) -> Option { match e { EntityType::Text(t) => Some(TextOrient { rotation: t.rotation, oblique: t.oblique_angle, x_scale: 0.0, }), EntityType::MText(m) => Some(TextOrient { rotation: m.rotation, oblique: 0.0, x_scale: 0.0, }), EntityType::Shape(s) => Some(TextOrient { rotation: s.rotation, oblique: s.oblique_angle, x_scale: s.relative_x_scale, }), _ => None, } } /// Restores readable text orientation after a mirror. fn restore_text_orient(e: &mut EntityType, o: &TextOrient) { match e { EntityType::Text(t) => { t.rotation = o.rotation; t.oblique_angle = o.oblique; use codec::entities::TextHorizontalAlignment as HA; t.horizontal_alignment = match t.horizontal_alignment { HA::Left => HA::Right, HA::Right => HA::Left, other => other, }; crate::entities::text::sync_text_alignment_point(t); } EntityType::MText(m) => { m.rotation = o.rotation; } EntityType::Shape(s) => { s.rotation = o.rotation; s.oblique_angle = o.oblique; s.relative_x_scale = o.x_scale; } _ => {} } } /// MIRRTEXT on: a real MIRROR makes text a true glyph mirror. `apply_transform` /// already reflects the baseline rotation; toggling *both* group-71 bits /// (backward `0x2` + upside-down `0x4`) then renders the exact geometric /// reflection (the renderer XORs these flags into the effective width-factor /// sign / 180° flip). Toggling — not setting — keeps a double mirror an /// involution. Only single-line TEXT carries group 71; MTEXT / Shape keep the /// reflected rotation (readable-but-rotated), which is standard-conforming. fn mirror_true_text_flags(e: &mut EntityType) { if let EntityType::Text(t) = e { t.generation_flags ^= 0x2 | 0x4; } } /// Transform one entity of a dimension's `*D` block. Its text reads the /// right way after a mirror, whatever MIRRTEXT says. fn transform_dimension_part(entity: &mut EntityType, t: &EntityTransform) { let orient = matches!(t, EntityTransform::Mirror { .. }) .then(|| capture_text_orient(entity)) .flatten(); view::dispatch::apply_transform(entity, t); if let Some(orient) = orient { restore_text_orient(entity, &orient); } } impl Scene { pub(crate) fn sync_displayed_annotation_context(&mut self, handle: Handle) -> bool { let scale = self.displayed_annotation_scale_handle(); crate::scene::annotative::sync_annotation_context_from_entity( &mut self.document, handle, scale, ) } /// Invalidate a dimension's baked block while capturing every removed /// sub-entity for an active history transaction. pub fn invalidate_dim_block_recorded(&mut self, handle: Handle) { if self.is_recording_undo() { let owned: Vec = self .document .get_entity(handle) .and_then(|entity| match entity { EntityType::Dimension(d) => { let name = d.base().block_name.clone(); self.document.block_records.get(&name).map(|record| { let mut handles = record.entity_handles.clone(); handles.push(record.block_entity_handle); handles.push(record.block_end_handle); handles }) } _ => None, }) .unwrap_or_default(); if let Some(before) = self.document.get_entity_arc(handle) { self.record_undo_before(handle, Some(before)); } for owned_handle in owned { if let Some(before) = self.document.get_entity_arc(owned_handle) { self.record_undo_before(owned_handle, Some(before)); } } self.poison_undo_recording(); } crate::modules::draw::modify::explode::invalidate_dim_block(&mut self.document, handle); } // ── Modify (transform / copy) ───────────────────────────────────────── pub fn transform_entities(&mut self, handles: &[Handle], t: &EntityTransform) { // Never transform objects on a locked layer (defense-in-depth: the pick // path already excludes them, but programmatic callers may not). let handles: Vec = handles .iter() .copied() .filter(|&h| !self.is_layer_locked(h)) .collect(); let handles = &handles[..]; let retained_originals: Vec<(Handle, EntityType)> = handles .iter() .filter_map(|handle| { self.document .get_entity(*handle) .cloned() .map(|entity| (*handle, entity)) }) .collect(); let transformed_refs: Vec<_> = handles .iter() .copied() .map(parametric_constraints::ParametricRef::whole) .collect(); let mut driven_refs = Vec::new(); if !matches!(t, EntityTransform::Translate(_)) { for set in &self.parametric_constraints { for constraint in &set.constraints { if !constraint.enabled || !matches!( constraint.kind, parametric_constraints::ConstraintKind::Parallel | parametric_constraints::ConstraintKind::Perpendicular | parametric_constraints::ConstraintKind::Colinear ) { continue; } let selected_refs = constraint .refs .iter() .filter(|reference| handles.contains(&reference.entity)) .count(); if selected_refs == 0 || selected_refs == constraint.refs.len() { continue; } for reference in constraint .refs .iter() .copied() .filter(|reference| !handles.contains(&reference.entity)) { let anchor = if let Some(index) = reference.segment_index() { parametric_constraints::ParametricRef::point( reference.entity, index as i32, ) } else if reference.directional_axis().is_some() && matches!( self.document.get_entity(reference.entity), Some(EntityType::Ellipse(_)) ) { parametric_constraints::ParametricRef::center(reference.entity) } else { parametric_constraints::ParametricRef::point(reference.entity, 0) }; let follows_selected_point = set.constraints.iter().any(|link| { link.enabled && link.kind == parametric_constraints::ConstraintKind::Coincident && link.refs.contains(&anchor) && link.refs.iter().any(|item| handles.contains(&item.entity)) }); if !follows_selected_point && !driven_refs.contains(&anchor) { driven_refs.push(anchor); } } } } } // Preserve readable text orientation when MIRRTEXT is disabled. let preserve_text_orientation = matches!(t, EntityTransform::Mirror { .. }) && !self.document.header.mirror_text; // Toggle group-71 flags when glyph mirroring is enabled. let mirror_true = matches!(t, EntityTransform::Mirror { .. }) && self.document.header.mirror_text; let mut text_orient_backup: Vec<(Handle, TextOrient)> = Vec::new(); if preserve_text_orientation { for &h in handles { if let Some(entity) = self.document.get_entity(h) { if let Some(o) = capture_text_orient(entity) { text_orient_backup.push((h, o)); } } } } // A dimension's final geometry is baked into a per-instance `*D` // block, and the render draws those sub-entities directly (not the // definition points). Transform them with the dimension, or it would // stay drawn in place while only its def points move. let dim_block_subs: Vec<(Handle, Option)> = handles .iter() .filter_map(|&h| match self.document.get_entity(h) { Some(entity @ EntityType::Dimension(d)) => { let bn = d.base().block_name.clone(); if bn.trim().is_empty() { None } else { Some((bn, render_graph::dim_picture_placement(entity))) } } _ => None, }) .filter_map(|(bn, placement)| { self.document .block_records .iter() .find(|br| br.name.eq_ignore_ascii_case(&bn)) .map(|br| { br.entity_handles .iter() .map(|&sh| (sh, placement.clone())) .collect::>() }) }) .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() { let before = self.document.get_entity_arc(h); self.record_undo_before(h, before); } if let Some(entity) = self.document.get_entity_mut(h) { view::dispatch::apply_transform(entity, t); render_graph::clear_dim_picture_placement(entity); if mirror_true { 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) { Some(EntityType::Hatch(dxf)) => Self::hatch_model_from_dxf(dxf, existing_color), // A DXF SOLID renders as a solid-fill hatch; rebuild it from // the moved corners so the fill follows the transform. Some(EntityType::Solid(s)) => Some(Self::solid_hatch_model(s, existing_color)), _ => None, }; if let Some(model) = new_model { self.hatches.insert(h, model); } } let image_bearing = self.document.get_entity(h).is_some_and(|entity| { matches!( entity, EntityType::RasterImage(_) | EntityType::Ole2Frame(_) | EntityType::Underlay(_) ) }); if image_bearing { let model = self .document .get_entity(h) .and_then(|entity| self.image_seed_for(entity)); self.images.remove(&h); if let Some(model) = model { self.images.insert(h, model); } } } if preserve_text_orientation { for (h, o) in text_orient_backup { if let Some(entity) = self.document.get_entity_mut(h) { restore_text_orient(entity, &o); } } } for &h in handles { let others = crate::scene::annotative::transform_annotation_contexts(&mut self.document, h, t); if self.sync_displayed_annotation_context(h) || others { self.poison_undo_recording(); } } // Move the baked dimension-block sub-entities too (collected above). // These are entities not named in the reported change set, so a delta // must capture their before-images here or a dimension move won't undo. for (h, placement) in &dim_block_subs { if self.is_recording_undo() { let before = self.document.get_entity_arc(*h); self.record_undo_before(*h, before); } if let Some(entity) = self.document.get_entity_mut(*h) { if let Some(placement) = placement { view::dispatch::apply_transform(entity, placement); } transform_dimension_part(entity, t); } } // The moved `*D` contents are a block definition the block cache holds; // without a fresh block epoch a reopened drawing's dimensions stay // drawn where they were until REGEN. (#1342) if !dim_block_subs.is_empty() { self.block_epoch = super::GEOMETRY_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed); } // Only the transformed entities changed (a top-level move/rotate/scale/ // mirror never edits a block definition) — report just those so the // resident set re-tessellates only them and every derived cache patches // per-handle, keeping the block cache + all other memoized wires. let changes: Vec<(Handle, ChangeKind)> = handles.iter().map(|&h| (h, ChangeKind::Modified)).collect(); self.bump_entities_with_parametric_transform_policy( &changes, &driven_refs, &transformed_refs, &retained_originals, ); self.refresh_meshes_for_handles(&refresh_solid_handles); } /// Entities in anonymous dimension blocks that visually belong to /// dimensions inside `block_record`. They must follow a block-coordinate /// reframe and be included in BEDIT's discard snapshot. pub(crate) fn block_definition_dependent_handles(&self, block_record: Handle) -> Vec { let Some(record) = self .document .block_records .iter() .find(|record| record.handle == block_record) else { return Vec::new(); }; let root_handles: HashSet = record.entity_handles.iter().copied().collect(); let mut result = Vec::new(); let mut seen = HashSet::default(); for handle in &record.entity_handles { let Some(EntityType::Dimension(dimension)) = self.document.get_entity(*handle) else { continue; }; let name = dimension.base().block_name.trim(); if name.is_empty() { continue; } let Some(dependent) = self .document .block_records .iter() .find(|candidate| candidate.name.eq_ignore_ascii_case(name)) else { continue; }; for dependent_handle in &dependent.entity_handles { if !root_handles.contains(dependent_handle) && seen.insert(*dependent_handle) { result.push(*dependent_handle); } } } result } /// Bake a transient BEDIT UCS into one block definition. /// /// `local_from_old` maps the currently stored block coordinates into the /// newly selected UCS frame. No UCS record is attached to the block: the /// content is transformed and remains canonical around identity/zero. pub fn reframe_block_definition( &mut self, block_record: Handle, local_from_old: &codec::types::Transform, ) -> usize { let Some(record) = self .document .block_records .iter() .find(|record| record.handle == block_record) else { return 0; }; let block_name = record.name.clone(); let root_handles: HashSet = record.entity_handles.iter().copied().collect(); let dependent_handles = self.block_definition_dependent_handles(block_record); let mut handles = record.entity_handles.clone(); handles.extend(dependent_handles.iter().copied()); let owned: HashSet = handles.iter().copied().collect(); let transform = EntityTransform::Affine(*local_from_old); let matrix = &local_from_old.matrix.m; let dependent_transform = EntityTransform::Affine(codec::types::Transform::from_matrix( codec::types::Matrix4 { m: [ [matrix[0][0], matrix[0][1], matrix[0][2], 0.0], [matrix[1][0], matrix[1][1], matrix[1][2], 0.0], [matrix[2][0], matrix[2][1], matrix[2][2], 0.0], [0.0, 0.0, 0.0, 1.0], ], }, )); let mut changed = Vec::new(); for handle in handles { let is_structural = self.document.get_entity(handle).is_some_and(|entity| { matches!(entity, EntityType::Block(_) | EntityType::BlockEnd(_)) }); if is_structural { continue; } if self.is_recording_undo() { let before = self.document.get_entity_arc(handle); self.record_undo_before(handle, before); } if let Some(entity) = self.document.get_entity_mut(handle) { // A nested dimension's anonymous `*D` picture is placed by // adding the dimension insertion point. That point already // receives the affine translation with the root dimension, so // its picture receives only the new basis here; applying the // translation twice would shift dimensions on origin changes. let entity_transform = if root_handles.contains(&handle) { &transform } else { &dependent_transform }; view::dispatch::apply_transform(entity, entity_transform); changed.push(handle); } } // ATTRIBs live inline on each INSERT in that reference's owner space. // Conjugate the block-local reframe through the INSERT transform: // owner' = M · local_from_old · M⁻¹ · owner. M is the block-local → // world map the render path uses (`insert_transform`), because that is // the map the attribute positions on the INSERT were placed through; // the codec's base-ignoring transform would conjugate them through a // displaced frame whenever the definition has a non-zero base point. let references: Vec<(Handle, codec::types::Transform)> = self .document .entities() .filter_map(|entity| match entity { EntityType::Insert(reference) if reference.block_name.eq_ignore_ascii_case(&block_name) && !reference.attributes.is_empty() && !owned.contains(&reference.common.handle) => { let insertion = crate::scene::render_graph::insert_transform(&self.document, reference); let inverse = inverse_affine(&insertion)?; Some(( reference.common.handle, codec::types::Transform::from_matrix( insertion.matrix * local_from_old.matrix * inverse.matrix, ), )) } _ => None, }) .collect(); for (handle, attribute_transform) in references { if self.is_recording_undo() { let before = self.document.get_entity_arc(handle); self.record_undo_before(handle, before); } if let Some(EntityType::Insert(reference)) = self.document.get_entity_mut(handle) { for attribute in &mut reference.attributes { codec::Entity::apply_transform(attribute, &attribute_transform); } changed.push(handle); } } for handle in changed.iter().copied() { let _ = self.sync_displayed_annotation_context(handle); } if !changed.is_empty() { self.rebuild_derived_caches(); } changed.len() } /// Give a freshly-cloned entity brand-new handles for every *inline* /// sub-entity that stores one (INSERT attributes, 3D-polyline vertices). /// `document.add_entity` only assigns the top-level handle, so without this /// a copy keeps its source's sub-handles — duplicate handles that corrupt /// the saved DWG (file won't reopen in other CAD apps). Vertices that don't /// store a handle (LwPolyline / heavy 2D polyline) get one from the writer, /// so they need no fix-up here. (#129) pub(super) fn reset_clone_subhandles(doc: &mut codec::CadDocument, entity: &mut EntityType) { match entity { EntityType::Insert(ins) => { for att in ins.attributes.iter_mut() { att.common.handle = doc.allocate_handle(); } } EntityType::Polyline3D(p) => { for v in p.vertices.iter_mut() { v.handle = doc.allocate_handle(); } } _ => {} } } /// Add a clone to the active space with fresh top-level and inline handles. /// Source owner handles are document-local and must not cross drawings. /// (#129, #934) pub fn add_entity_clone(&mut self, mut entity: EntityType) -> Handle { Self::reset_clone_subhandles(&mut self.document, &mut entity); let common = entity.common_mut(); common.handle = Handle::NULL; common.owner_handle = Handle::NULL; common.entity_mode = Some( if self.current_layout != "Model" && self.active_viewport.is_none() { 1 } else { 2 }, ); self.add_entity(entity) } /// Duplicate the anonymous block `src_name`, transforming every sub-entity /// by `t`, and return the new block's name. A dimension's drawn geometry /// lives in such a baked `*D` block, so a copied dimension needs its own /// transformed block — otherwise it still references the source block and /// renders on top of the original instead of at the copy. Returns None when /// the source block is missing or empty. (#161) fn clone_transformed_block( &mut self, src_name: &str, placement: Option<&EntityTransform>, t: &EntityTransform, ) -> Option { let sub_handles = self .document .block_records .iter() .find(|br| br.name.eq_ignore_ascii_case(src_name)) .map(|br| br.entity_handles.clone())?; let subs: Vec = sub_handles .iter() .filter_map(|&sh| self.document.get_entity(sh).cloned()) .map(|mut sub| { if let Some(placement) = placement { view::dispatch::apply_transform(&mut sub, placement); } sub }) .collect(); self.define_transformed_block(&subs, t) } /// Build a fresh anonymous `*D` block from `subs` — a source block's /// sub-entities in its own (WCS-baked) coordinates — transforming each by /// `t`, and return the new block's name. Shared by the in-drawing copy /// (source block still lives in this document, via `clone_transformed_block`) /// and clipboard paste (source block snapshotted into the clipboard, so a /// pasted dimension gets its own transformed block cross-drawing too — see /// `finalize_paste`, #290). Returns None when `subs` is empty. pub(crate) fn define_transformed_block( &mut self, subs: &[EntityType], t: &EntityTransform, ) -> Option { if subs.is_empty() { return None; } // Smallest free `*D` anonymous name. let mut n = 0u64; let new_name = loop { let cand = format!("*D{n}"); if self.document.block_records.get(&cand).is_none() { break cand; } n += 1; }; let next = self.document.next_handle(); let br_handle = Handle::new(next); let block_handle = Handle::new(next + 1); let end_handle = Handle::new(next + 2); let mut br = codec::tables::BlockRecord::new(&new_name); br.handle = br_handle; br.block_entity_handle = block_handle; br.block_end_handle = end_handle; self.document.block_records.add(br).ok()?; let mut block = Block::new(&new_name, codec::types::Vector3::ZERO); block.common.handle = block_handle; block.common.owner_handle = br_handle; self.document.add_entity(EntityType::Block(block)).ok()?; let mut block_end = BlockEnd::new(); block_end.common.handle = end_handle; block_end.common.owner_handle = br_handle; self.document .add_entity(EntityType::BlockEnd(block_end)) .ok()?; for sub in subs { let mut sub = sub.clone(); transform_dimension_part(&mut sub, t); Self::reset_clone_subhandles(&mut self.document, &mut sub); sub.common_mut().handle = Handle::NULL; sub.common_mut().owner_handle = br_handle; // An extension dictionary is exclusively owned; the clone must // not claim the source's, or file validation reports // "Extension dictionary Cannot access". sub.common_mut().xdictionary_handle = None; let _ = self.document.add_entity(sub); } Some(new_name) } pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec { let copy_handles = self.handles_expanded_for_leader_annotations(handles); // Objects on a locked layer can be selected but not copied. let clones: Vec<(Handle, EntityType, Vec)> = copy_handles .iter() .filter(|&&h| !self.is_layer_locked(h)) .filter_map(|&h| { let entity = self.document.get_entity(h)?.clone(); // Any annotative entity (not only a LEADER/MTEXT pair) needs // its own context tree; a shared one ties the copy to the // source. (#700) let annotation_scales = crate::scene::annotative::annotation_scale_handles_for_entity( &self.document, h, ); Some((h, entity, annotation_scales)) }) .collect(); // MIRRTEXT also governs the copy path (default MIRROR keeps the source // and adds a mirrored copy): keep the copied text right-reading when the // header flag is off. let preserve_text_orientation = matches!(t, EntityTransform::Mirror { .. }) && !self.document.header.mirror_text; let mirror_true = 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, annotation_scales) in clones { let text_orient = if preserve_text_orientation { capture_text_orient(&entity) } else { None }; let picture_placement = render_graph::dim_picture_placement(&entity); view::dispatch::apply_transform(&mut entity, t); render_graph::clear_dim_picture_placement(&mut entity); if let Some(o) = &text_orient { restore_text_orient(&mut entity, o); } if mirror_true { mirror_true_text_flags(&mut entity); } // A dimension draws from its baked `*D` block; give the copy its own // transformed block so it lands at the copy position rather than // rendering on top of the source. (#161) if let EntityType::Dimension(d) = &entity { let bn = d.base().block_name.clone(); if !bn.trim().is_empty() { let placement = picture_placement.as_ref(); if let Some(new_bn) = self.clone_transformed_block(&bn, placement, t) { // Delta-undo: a copied dimension adds a fresh anonymous // *D block record — non-entity state a pure-entity delta // can't restore. Poison so the app keeps a full snapshot. self.poison_undo_recording(); if let EntityType::Dimension(d) = &mut entity { d.base_mut().block_name = new_bn; } } } } Self::reset_clone_subhandles(&mut self.document, &mut entity); // An annotative LEADER/MTEXT pair must receive a fresh extension dictionary. // Keeping this handle would make the copy share the source's context tree. if !annotation_scales.is_empty() { entity.common_mut().xdictionary_handle = None; } entity.common_mut().handle = Handle::NULL; let h = self.document.add_entity(entity).unwrap_or(Handle::NULL); if !h.is_null() { if !annotation_scales.is_empty() { for scale_handle in annotation_scales { crate::scene::annotative::create_annotation_context( &mut self.document, h, scale_handle, ); } // Annotation contexts add dictionary/object records outside the entity // delta itself, so keep undo on the safe full-snapshot path. if self.is_recording_undo() { self.poison_undo_recording(); } } // Delta-undo: a copy's before-image is "nothing" (undo erases it). if self.is_recording_undo() { self.record_undo_before(h, None); } let new_model = match self.document.get_entity(h) { Some(EntityType::Hatch(dxf)) => { let color = convert::tess_util::aci_to_rgba(&dxf.common.color); Self::hatch_model_from_dxf(dxf, color) } Some(EntityType::Solid(s)) => { let color = convert::tess_util::aci_to_rgba(&s.common.color); Some(Self::solid_hatch_model(s, color)) } _ => None, }; if let Some(model) = new_model { self.hatches.insert(h, model); } // Images draw from a derived model, not the wire frame; without // one the copy shows only its frame. (#829) let image = self.document.get_entity(h).and_then(|entity| { matches!( entity, EntityType::RasterImage(_) | EntityType::Ole2Frame(_) | EntityType::Underlay(_) ) .then(|| self.image_seed_for(entity)) .flatten() }); if let Some(model) = image { self.images.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() { handle_map.insert(src_handle, h); } } // A copied LEADER must reference the copied annotation, never the // source annotation. Both entities now exist, so remap the stored handle. let leader_links: Vec<(Handle, Handle)> = handle_map .iter() .filter_map(|(&source_handle, &copied_handle)| { let EntityType::Leader(source_leader) = self.document.get_entity(source_handle)? else { return None; }; let copied_annotation = handle_map .get(&source_leader.annotation_handle) .copied() .unwrap_or(Handle::NULL); Some((copied_handle, copied_annotation)) }) .collect(); for (leader_handle, annotation_handle) in leader_links { if let Some(EntityType::Leader(leader)) = self.document.get_entity_mut(leader_handle) { leader.annotation_handle = annotation_handle; } let _ = self.sync_displayed_annotation_context(leader_handle); } remap_copied_reactors(&mut self.document, &handle_map); // Complete group copies record their new Group objects and dictionary // entry as targeted object deltas inside copy_complete_groups. self.copy_complete_groups(&handle_map); self.copy_dimension_associations(&handle_map); self.copy_hatch_associations(&handle_map); // The copies are new handles (natural memo misses, tessellated fresh) // and reference only already-cached blocks — no block defn changes. // Report them as additions so derived caches patch in exactly the copies. let changes: Vec<(Handle, ChangeKind)> = new_handles .iter() .map(|&h| (h, ChangeKind::Added)) .collect(); self.bump_entities(&changes); // Constraints wholly within the duplicated selection follow it. self.duplicate_parametric_constraints_for(&handle_map); self.refresh_meshes_for_handles(&refresh_solid_handles); new_handles } // ── Grip editing ────────────────────────────────────────────────────── pub(crate) fn solid_history_objects( &self, handle: Handle, ) -> Vec<(Handle, codec::objects::ObjectType)> { let Some(graph) = self.document.solid_history_graph(handle) else { return Vec::new(); }; std::iter::once(graph.root) .chain(graph.nodes) .chain(graph.evaluation_graph) .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: codec::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.into_iter().chain(graph.evaluation_graph) { self.record_undo_object_before(node, None); } self.sync_solid_reference_point(handle); true } pub fn append_solid_history( &mut self, handle: Handle, operation: codec::objects::SolidHistoryOperation, ) -> bool { let previous: Vec = self .document .solid_history_graph(handle) .map(|graph| { graph .nodes .into_iter() .chain(graph.evaluation_graph) .collect() }) .unwrap_or_default(); self.record_solid_history_before(handle); let Some(graph) = self.document.append_solid_history(handle, operation) else { return false; }; // A history saved without an evaluation graph gets one on append. for node in graph.nodes.into_iter().chain(graph.evaluation_graph) { if !previous.contains(&node) { self.record_undo_object_before(node, None); } } self.sync_solid_reference_point(handle); true } /// Make `target`'s history the boolean steps that built it: starting from /// `first`'s history, each of `steps` joins a solid's history into the /// result so far. When `first` is `target` its own history is kept; a /// solid with no history starts from its body in `bodies`. /// /// False, with `target` left without a history, when one of the solids /// cannot carry one. pub fn join_boolean_histories( &mut self, target: Handle, first: Handle, steps: &[(Handle, u8)], bodies: &std::collections::HashMap, ) -> bool { let solids = std::iter::once(target) .chain(std::iter::once(first)) .chain(steps.iter().map(|(handle, _)| *handle)) .collect::>(); let solid = |handle: &Handle| { matches!( self.document.get_entity(*handle), Some(EntityType::Solid3D(_)) ) }; if !solids.iter().all(solid) { return false; } for handle in solids.iter().skip(1) { if self.document.solid_history_graph(*handle).is_some() { continue; } let Some(body) = bodies.get(handle) else { return false; }; if !self .create_solid_history(*handle, crate::scene::model::solid_history::brep_op(body)) { return false; } } if first != target { self.delete_solid_history(target); if self.is_recording_undo() { let before = self.document.get_entity_arc(target); self.record_undo_before(target, before); } if !self.copy_solid_history(first, target) { return false; } } for &(tool, operation) in steps { if !self.merge_solid_history_boolean(target, tool, operation) { self.delete_solid_history(target); return false; } } true } fn merge_solid_history_boolean(&mut self, target: Handle, tool: Handle, operation: u8) -> bool { let previous: Vec = [target, tool] .iter() .filter_map(|handle| self.document.solid_history_graph(*handle)) .flat_map(|graph| graph.nodes.into_iter().chain(graph.evaluation_graph)) .collect(); self.record_solid_history_before(target); self.record_solid_history_before(tool); if self.is_recording_undo() { // The tool loses its history link; undo must give it back. let before = self.document.get_entity_arc(tool); self.record_undo_before(tool, before); } let Some(graph) = self .document .merge_solid_history_boolean(target, tool, operation) else { return false; }; for node in graph.nodes.into_iter().chain(graph.evaluation_graph) { if !previous.contains(&node) { self.record_undo_object_before(node, None); } } true } pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) { let reference = self .document .solid_history_operations(handle) .and_then(|operations| { operations .iter() .rev() .find_map(crate::scene::model::solid_history::reference_point) }); let Some(reference) = reference else { return; }; let point = codec::types::Vector3::new(reference.x, reference.y, reference.z); match self.document.get_entity_mut(handle) { Some(EntityType::Solid3D(entity)) => entity.point_of_reference = point, Some(EntityType::Surface(entity)) => entity.point_of_reference = point, _ => {} } } 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) { if self.is_recording_undo() { // The codec also clears the entity's history handle. Capture that // link before deletion so undo reconnects the restored graph. let before = self.document.get_entity_arc(handle); self.record_undo_before(handle, before); } self.record_solid_history_before(handle); self.document.delete_solid_history(handle); } fn rebuild_history_body( &self, handle: Handle, operation: &codec::objects::SolidHistoryOperation, ) -> Option { use codec::objects::SolidHistoryOperation; match (self.document.get_entity(handle)?, operation) { (EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => { kernel::acis::rebuild_extrusion_with_mode(value, true).ok() } (EntityType::Surface(_), SolidHistoryOperation::Loft(_)) => { kernel::acis::rebuild_body(operation).ok() } (EntityType::Solid3D(_), _) => { let mut tree = self.document.solid_history_tree(handle)?; tree.find_mut(operation.base()?.node_id())?.operation = operation.clone(); kernel::acis::rebuild_history_tree(&tree).ok() } _ => None, } } fn history_surface_data( &self, handle: Handle, operation: &codec::objects::SolidHistoryOperation, ) -> Option> { use codec::objects::SolidHistoryOperation; match (self.document.get_entity(handle)?, operation) { (EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => { crate::scene::model::solid_history::extrusion_surface_data(value).map(Some) } (EntityType::Surface(_), SolidHistoryOperation::Loft(value)) => { let EntityType::Surface(surface) = crate::scene::model::loft_command_model::surface_entity(value) else { return None; }; Some(Some(surface.surface_data)) } (EntityType::Solid3D(_), _) => Some(None), _ => None, } } /// Swept surfaces linked to scale/twist expressions follow the named /// parameters: re-evaluated and rebuilt when a value changes, as the /// reference keeps them associative. Returns the surfaces rebuilt. pub fn refresh_expression_sweeps(&mut self) -> Vec { use crate::scene::model::sweep_model; let linked = self .document .entities() .filter_map(|entity| { Some(( entity.common().handle, sweep_model::sweep_expressions(entity)?, )) }) .collect::>(); let mut rebuilt = Vec::new(); for (handle, [scale, twist]) in linked { let Some(entity) = self.document.get_entity(handle) else { continue; }; let Some(mut record) = sweep_model::surface_sweep_record(entity) else { continue; }; let evaluate = |expression: &str| { let mut table = self.named_parameters().clone(); table.set("sweepExpression", expression).ok()?; table .resolve("sweepExpression") .ok() .filter(|value| value.is_finite()) }; let mut changed = false; if let Some(value) = scale.as_deref().and_then(evaluate) { changed |= (record.scale_factor - value).abs() > 1e-12; record.scale_factor = value; } if let Some(value) = twist.as_deref().and_then(evaluate).map(f64::to_radians) { changed |= (record.twist_angle - value).abs() > 1e-12; record.twist_angle = value; } if !changed { continue; } let Ok(body) = kernel::acis::rebuild_sweep_with_mode(&record, true) else { continue; }; let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { continue; }; let Some(display) = self.prepare_solid_model_display(handle, &body) else { continue; }; let before = self .document .get_entity(handle) .cloned() .map(std::sync::Arc::new); self.record_undo_before(handle, before); if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle) { surface.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string()); if let codec::entities::SurfaceData::Swept { options, .. } = &mut surface.surface_data { options.scale_factor = record.scale_factor; options.twist_angle = record.twist_angle; } } self.register_prepared_solid_model(handle, body, display); rebuilt.push(handle); } rebuilt } /// Associative swept surfaces follow edits of their profile or path: /// swept again from them with the options they were made with. A /// surface edited along with its sources keeps its own edit. Returns the /// surfaces rebuilt. pub(crate) fn refresh_associative_sweeps( &mut self, changes: &[(Handle, crate::scene::ChangeKind)], ) -> Vec<(Handle, crate::scene::ChangeKind)> { use crate::scene::model::sweep_model; let changed = changes .iter() .filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed)) .map(|(handle, _)| *handle) .collect::>(); // Every edit passes through here: a drawing with no associative // swept surface (no application registered for them) skips the scan. if changed.is_empty() || !self .document .app_ids .contains(sweep_model::SWEEP_SOURCES_APP) { return Vec::new(); } let linked = self .document .entities() .filter(|entity| matches!(entity, EntityType::Surface(_))) .filter_map(|entity| { let handle = entity.common().handle; let (profile, path) = sweep_model::sweep_sources(entity)?; (!changed.contains(&handle) && (changed.contains(&profile) || changed.contains(&path))) .then_some((handle, profile, path)) }) .collect::>(); let mut rebuilt = Vec::new(); for (handle, profile, path) in linked { let (Some(surface), Some(profile), Some(path)) = ( self.document.get_entity(handle), self.document.get_entity(profile), self.document.get_entity(path), ) else { continue; }; let Some((record, body)) = sweep_model::resweep_from_sources(surface, profile, path) else { continue; }; let EntityType::Surface(fresh) = sweep_model::swept_surface_entity(&record) else { continue; }; let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { continue; }; let Some(display) = self.prepare_solid_model_display(handle, &body) else { continue; }; let before = self .document .get_entity(handle) .cloned() .map(std::sync::Arc::new); self.record_undo_before(handle, before); if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle) { surface.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string()); surface.surface_data = fresh.surface_data; surface.point_of_reference = fresh.point_of_reference; } self.register_prepared_solid_model(handle, body, display); rebuilt.push((handle, crate::scene::ChangeKind::Modified)); } rebuilt } /// Associative extruded, revolved and lofted surfaces follow edits of /// their sources: made again from them with the values they were made /// with. A surface edited along with its sources keeps its own edit. /// Returns the surfaces rebuilt. pub(crate) fn refresh_associative_surfaces(&mut self, changes: &[(Handle, crate::scene::ChangeKind)]) -> Vec<(Handle, crate::scene::ChangeKind)> { use crate::scene::model::surface_sources::{axis_points, surface_sources, SurfaceSources, SURFACE_SOURCES_APP}; use crate::scene::model::sweep_model; use codec::objects::SolidHistoryOperation; let changed = changes.iter() .filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed)) .map(|(handle, _)| *handle) .collect::>(); if changed.is_empty() || !self.document.app_ids.contains(SURFACE_SOURCES_APP) { return Vec::new(); } let linked = self.document.entities() .filter(|entity| matches!(entity, EntityType::Surface(_))) .filter_map(|entity| { let handle = entity.common().handle; let sources = surface_sources(entity)?; (!changed.contains(&handle) && sources.handles().iter().any(|source| changed.contains(source))) .then_some((handle, sources)) }) .collect::>(); let mut rebuilt = Vec::new(); for (handle, sources) in linked { let source = |handle: Handle| self.document.get_entity(handle).cloned(); // The surface made again: a history step, or a body with its data. let made: Option> = match &sources { SurfaceSources::Extrude { profile, height, direction, taper } => (|| { let entity = source(*profile)?; let (shape, _) = sweep_model::extrusion_profile_of(&entity)?; let direction = match direction { Some(direction) => *direction, None => glam::DVec3::from_array(shape.plane.normal()?) * *height, }; if self.document.solid_history_operations(handle).is_some() { return sweep_model::extrusion_history(&entity, None, direction.to_array(), *taper, shape.plane.origin).map(Ok); } let body = sweep_model::extruded_surface(&entity, direction.to_array(), *taper)?; let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_extruded_surface(direction, *taper) else { return None }; Some(Err((body, fresh.surface_data))) })(), SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => (|| { let entity = source(*profile)?; let (start, end) = axis .and_then(|axis| self.document.get_entity(axis).and_then(axis_points)) .unwrap_or((*start, *end)); let body = sweep_model::revolved_surface(&entity, start.to_array(), end.to_array(), *angle, *start_angle)?; let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_revolved_surface(&entity, start, end, *angle, *start_angle) else { return None }; Some(Err((body, fresh.surface_data))) })(), SurfaceSources::Loft { sections } => (|| { let operations = self.document.solid_history_operations(handle)?; let mut record = operations.into_iter().find_map(|operation| match operation { SolidHistoryOperation::Loft(record) => Some(record), _ => None, })?; if record.cross_sections.len() != sections.len() { return None; } record.cross_sections = sections.iter() .map(|section| source(*section).as_ref().and_then(crate::scene::model::loft_command_model::embedded_section)) .collect::>>()?; Some(Ok(SolidHistoryOperation::Loft(record))) })(), }; let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new); match made { Some(Ok(operation)) => { self.record_undo_before(handle, before); if self.rebuild_solid_history(handle, operation) { rebuilt.push((handle, crate::scene::ChangeKind::Modified)); } } Some(Err((body, data))) => { let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { continue }; let Some(display) = self.prepare_solid_model_display(handle, &body) else { continue }; self.record_undo_before(handle, before); if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle) { surface.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string()); surface.surface_data = data; } self.register_prepared_solid_model(handle, body, display); rebuilt.push((handle, crate::scene::ChangeKind::Modified)); } None => {} } } rebuilt } /// Swept surfaces the reference keeps associative to a scale or twist /// expression (an associative swept surface action whose value parameter /// depends on an expression variable) are linked the same way here. pub(crate) fn import_sweep_expression_links(&mut self) { use codec::objects::{AssocSurfaceActionKind, AssociativeData, ObjectType}; let object = |handle: Handle| match self.document.objects.get(&handle) { Some(ObjectType::Associative(object)) => Some(&object.data), _ => None, }; let mut links = Vec::new(); let mut sources = Vec::new(); let mut others = Vec::new(); for item in self.document.objects.values() { let ObjectType::Associative(action) = item else { continue }; let AssociativeData::Action(action) = &action.data else { continue }; let Some(AssociativeData::SurfaceActionBody(body)) = object(action.action_body) else { continue }; let Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency) else { continue }; // The entities a named path parameter reads (an edge of a region // reads through a geometry dependency). let read = |name: &str| -> Vec { action.owned_parameters.iter().filter_map(|parameter| { let Some(AssociativeData::PathActionParam(path)) = object(*parameter) else { return None }; if path.compound.action_param.name != name { return None; } let Some(AssociativeData::EdgeActionParam(edge)) = object(*path.compound.parameters.first()?) else { return None }; match object(edge.single_dependency.dependency) { Some(AssociativeData::Dependency(dependency)) => Some(dependency.dependent_on), Some(AssociativeData::GeomDependency(dependency)) => Some(dependency.dependency.dependent_on), _ => None, } }).collect() }; let real = |name: &str| action.values.iter().find(|value| value.name == name) .and_then(|value| value.variables.first()) .and_then(|variable| match variable.value.value { codec::objects::AssocEvalValue::Real(value) => Some(value), _ => None, }); match body.kind { AssocSurfaceActionKind::Extruded => { if let (Some(profile), Some(height), Some(taper)) = (read("ExtrudeProfile").first().copied(), real("ExtrusionHeight"), real("ExtrusionTaperAngle")) { others.push((written.dependent_on, (body.kind, vec![profile], None, height, taper))); } continue; } AssocSurfaceActionKind::Revolved => { if let (Some(profile), Some(angle)) = (read("RevolveProfile").first().copied(), real("RevolveAngle")) { others.push((written.dependent_on, (body.kind, vec![profile], read("RevolvePath").first().copied(), angle, 0.0))); } continue; } AssocSurfaceActionKind::Lofted => { let sections = read("CrossSection"); if sections.len() >= 2 { others.push((written.dependent_on, (body.kind, sections, None, 0.0, 0.0))); } continue; } AssocSurfaceActionKind::Swept => {} _ => continue, } let mut expressions: [Option; 2] = [None, None]; for value in &action.values { let slot = match value.name.as_str() { "ScaleFactor" => 0, "TwistAngle" => 1, _ => continue, }; for variable in &value.variables { let Some(AssociativeData::ValueDependency(dependency)) = object(variable.handle) else { continue; }; let Some(AssociativeData::Variable(source)) = object(dependency.dependency.dependent_on) else { continue; }; if !source.expression.trim().is_empty() && source.expression.trim().parse::().is_err() { expressions[slot] = Some(source.expression.trim().to_string()); } } } if expressions.iter().any(Option::is_some) { links.push((written.dependent_on, expressions)); } // The profile and path the action reads. if let (Some(profile), Some(path)) = (read("SweepProfile").first().copied(), read("SweepPath").first().copied()) { sources.push((written.dependent_on, profile, path)); } } for (surface, profile, path) in sources { let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue; }; if crate::scene::model::sweep_model::sweep_sources(entity).is_some() { continue; } let codec::entities::SurfaceData::Swept { options, .. } = &value.surface_data else { continue; }; let linked = crate::command::SweepOptions { align: options.sweep_alignment_flags == 1, bank: options.bank, ..crate::command::SweepOptions::default() }; crate::scene::model::sweep_model::link_sweep_sources( &mut self.document, surface, profile, path, &linked, ); } for (handle, expressions) in links { let Some(entity) = self.document.get_entity(handle) else { continue; }; if crate::scene::model::sweep_model::sweep_expressions(entity).is_some() { continue; } crate::scene::model::sweep_model::link_sweep_expressions( &mut self.document, handle, &expressions, ); } // Associative extruded, revolved and lofted surfaces follow their // sources here too. use crate::scene::model::surface_sources::{link_surface_sources, surface_sources, SurfaceSources}; for (surface, (kind, handles, axis, amount, taper)) in others { let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue }; if surface_sources(entity).is_some() { continue; } let linked = match (kind, &value.surface_data) { (AssocSurfaceActionKind::Extruded, codec::entities::SurfaceData::Extruded { sweep_vector, .. }) => { let profile = handles[0]; let along = glam::DVec3::new(sweep_vector.x, sweep_vector.y, sweep_vector.z); // A direction other than the profile's normal is kept. let normal = self.document.get_entity(profile) .and_then(|entity| crate::scene::model::sweep_model::extrusion_profile_of(entity)) .and_then(|(shape, _)| shape.plane.normal()) .map(glam::DVec3::from_array); let direction = match normal { Some(normal) if (normal * amount - along).length() <= 1e-9 * amount.abs().max(1.0) => None, _ if along.length() > 1e-12 => Some(along), _ => None, }; SurfaceSources::Extrude { profile, height: amount, direction, taper } } (AssocSurfaceActionKind::Revolved, codec::entities::SurfaceData::Revolved { axis_point, axis_vector, start_angle, .. }) => { let start = glam::DVec3::new(axis_point.x, axis_point.y, axis_point.z); SurfaceSources::Revolve { profile: handles[0], axis, start, end: start + glam::DVec3::new(axis_vector.x, axis_vector.y, axis_vector.z), angle: amount, start_angle: *start_angle, } } (AssocSurfaceActionKind::Lofted, _) => SurfaceSources::Loft { sections: handles }, _ => continue, }; link_surface_sources(&mut self.document, surface, &linked); } } pub fn rebuild_solid_history( &mut self, handle: Handle, operation: codec::objects::SolidHistoryOperation, ) -> bool { let Some(body) = self.rebuild_history_body(handle, &operation) else { return false; }; let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { return false; }; let Some(surface_data) = self.history_surface_data(handle, &operation) else { return false; }; let Some(display) = self.prepare_solid_model_display(handle, &body) else { return false; }; if matches!( &operation, codec::objects::SolidHistoryOperation::Loft(_) | codec::objects::SolidHistoryOperation::Extrusion(_) ) && !display.0.complete { return false; } self.record_solid_history_before(handle); if self .document .update_solid_history_step(handle, operation) .is_none() { return false; } match self.document.get_entity_mut(handle) { Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document), Some(EntityType::Surface(entity)) => { entity.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string()); if let Some(data) = surface_data { if matches!(&data, codec::entities::SurfaceData::Extruded { .. }) { entity.kind = codec::entities::SurfaceKind::Extruded; } entity.surface_data = data; } } _ => return false, } self.register_prepared_solid_model(handle, body, display); self.solid_history_preview_wires.remove(&handle); true } pub fn finalize_solid_history(&mut self, handle: Handle) -> bool { let Some(operation) = self.document.solid_history_operation(handle).cloned() else { return false; }; let Some(body) = self.rebuild_history_body(handle, &operation) else { return false; }; let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { return false; }; let Some(surface_data) = self.history_surface_data(handle, &operation) else { return false; }; let Some(display) = self.prepare_solid_model_display(handle, &body) else { return false; }; if matches!( &operation, codec::objects::SolidHistoryOperation::Loft(_) | codec::objects::SolidHistoryOperation::Extrusion(_) ) && !display.0.complete { return false; } match self.document.get_entity_mut(handle) { Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document), Some(EntityType::Surface(entity)) => { entity.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string()); if let Some(data) = surface_data { if matches!(&data, codec::entities::SurfaceData::Extruded { .. }) { entity.kind = codec::entities::SurfaceKind::Extruded; } entity.surface_data = data; } } _ => return false, } self.register_prepared_solid_model(handle, body, display); self.solid_history_preview_wires.remove(&handle); true } fn preview_solid_history( &mut self, handle: Handle, operation: codec::objects::SolidHistoryOperation, ) -> bool { let Some(body) = self.rebuild_history_body(handle, &operation) else { return false; }; if self.history_surface_data(handle, &operation).is_none() { return false; } // An operand inside a composite is a step below the active one. let active = self .document .solid_history_operation(handle) .and_then(|active| active.base()) .map(|base| base.node_id()); let updated = if operation.base().map(|base| base.node_id()) == active { self.document .update_solid_history(handle, operation) .is_some() } else { self.document .update_solid_history_step(handle, operation) .is_some() }; if !updated { return false; } let (isoline_counts, planar_isolines) = match self.document.get_entity(handle) { Some(EntityType::Surface(surface)) => ( crate::entities::solid3d::surface_isoline_counts(surface), crate::entities::solid3d::surface_property_state(surface).isolines, ), _ => ([0; 2], false), }; self.solid_history_preview_wires.insert( handle, crate::scene::model::solid_model::grip_preview_wires( &body, handle, isoline_counts, planar_isolines, ), ); 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 let EntityTransform::Translate(delta) = transform { let surface_data = match (&operation, self.document.get_entity(handle)) { ( codec::objects::SolidHistoryOperation::Extrusion(value), Some(EntityType::Surface(_)), ) => { let Some(data) = crate::scene::model::solid_history::extrusion_surface_data(value) else { return false; }; Some(data) } _ => None, }; if self .document .update_solid_history(handle, operation) .is_none() { return false; } if let (Some(data), Some(EntityType::Surface(entity))) = (surface_data, self.document.get_entity_mut(handle)) { entity.surface_data = data; } self.translate_solid_geometry(handle, delta.to_array()); return true; } self.rebuild_solid_history(handle, operation) } pub fn apply_solid_history_choice(&mut self, handle: Handle, field: &str, value: &str) -> bool { self.record_solid_history_before(handle); let mut created = false; if self.document.solid_history_graph(handle).is_none() { let create = match field { crate::scene::model::solid_history::PROP_HISTORY => { if value.eq_ignore_ascii_case("Record") { true } else if value.eq_ignore_ascii_case("None") { return false; } else { return false; } } crate::scene::model::solid_history::PROP_SHOW_HISTORY if self.document.header.show_solid_history.clamp(0, 2) == 1 => { if value.eq_ignore_ascii_case("Yes") { true } else if value.eq_ignore_ascii_case("No") { return false; } else { return false; } } _ => return false, }; if create { if !matches!( self.document.get_entity(handle), Some(EntityType::Solid3D(_)) ) { return false; } self.restore_solid_models(&[handle]); let Some(body) = self.solid_models.get(&handle).cloned() else { return false; }; if self.is_recording_undo() { let before = self.document.get_entity_arc(handle); self.record_undo_before(handle, before); } if !self.create_solid_history( handle, crate::scene::model::solid_history::brep_op(&body), ) { return false; } // A solid without a graph was displayed as History=None. // Preserve that object state before applying the requested // choice, independent of the drawing-wide SOLIDHIST setting. crate::scene::model::solid_history::apply_history_choice( &mut self.document, handle, crate::scene::model::solid_history::PROP_HISTORY, "None", ); created = true; } } let applied = crate::scene::model::solid_history::apply_history_choice( &mut self.document, handle, field, value, ); if created || applied { self.bump_entities(&[(handle, ChangeKind::Modified)]); } created || applied } fn apply_solid_history_grip( &mut self, handle: Handle, grip_id: usize, apply: GripApply, ) -> bool { let Some(mut operation) = crate::scene::model::solid_history::edit_operand(&self.document, handle) .or_else(|| self.document.solid_history_operation(handle).cloned()) else { return false; }; if grip_id == crate::scene::model::solid_history::GRIP_FILLET_RADIUS { let codec::objects::SolidHistoryOperation::Fillet(value) = &mut operation else { return false; }; let Some(radius) = value.radii.first().copied() else { return false; }; self.restore_solid_models(&[handle]); let Some(definition) = self.solid_models.get(&handle).and_then(|body| { crate::scene::model::solid_history::fillet_radius_grip(body, radius) }) else { return false; }; let Some(axis) = definition.axis else { return false; }; let change = match apply { GripApply::Absolute(world) => (world - definition.world).dot(axis), GripApply::Translate(delta) => delta.dot(axis), }; let radius = radius + change; if !radius.is_finite() || radius <= 1.0e-6 { return false; } value.radii.clear(); value.radii.push(radius); return self.preview_solid_history(handle, operation); } if matches!( grip_id, crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1 | crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE2 ) { let definitions = { let codec::objects::SolidHistoryOperation::Chamfer(value) = &operation else { return false; }; crate::scene::model::solid_history::chamfer_distance_grips( &self.document, handle, value, ) }; let Some(definition) = definitions.into_iter().find(|grip| grip.id == grip_id) else { return false; }; let Some(axis) = definition.axis else { return false; }; let change = match apply { GripApply::Absolute(world) => (world - definition.world).dot(axis), GripApply::Translate(delta) => delta.dot(axis), }; let codec::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else { return false; }; let distance = if grip_id == crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1 { &mut value.base_distance } else { &mut value.other_distance }; *distance += change; if !distance.is_finite() || *distance <= 1.0e-6 { return false; } return self.preview_solid_history(handle, operation); } if !crate::scene::model::solid_history::apply_primitive_grip(&mut operation, grip_id, apply) { return false; } let Some(operation) = crate::scene::model::solid_history::operand_to_history( &self.document, handle, operation, ) else { return false; }; self.preview_solid_history(handle, operation) } pub fn apply_solid_history_property( &mut self, handle: Handle, field: &str, value: &str, ) -> bool { let Some(mut operation) = crate::scene::model::solid_history::primitive_property_operation( &self.document, handle, ) else { return false; }; if !crate::scene::model::solid_history::apply_primitive_property( &mut operation, field, value, ) { return false; } let Some(operation) = crate::scene::model::solid_history::operand_to_history( &self.document, handle, operation, ) else { return false; }; self.rebuild_solid_history(handle, operation) } /// The modeler error code the reference reports when it refuses a sweep /// property edit (twist or scale along a path with a corner). pub fn solid_history_property_refusal( &self, handle: Handle, field: &str, value: &str, ) -> Option { let mut operation = crate::scene::model::solid_history::primitive_property_operation( &self.document, handle, )?; if !crate::scene::model::solid_history::apply_primitive_property( &mut operation, field, value, ) { return None; } let codec::objects::SolidHistoryOperation::Sweep(record) = &operation else { return None; }; kernel::acis::sweep_history_refusal(record) .map(crate::scene::model::sweep_model::sweep_refusal_code) } pub fn apply_solid_position_property( &mut self, handle: Handle, field: &str, value: &str, plane: crate::command::WorkingPlane, ) -> Option { let axis = match field { "s3d_px" | "rgn_px" | "bdy_px" | "srf_px" => 0, "s3d_py" | "rgn_py" | "bdy_py" | "srf_py" => 1, "s3d_pz" | "rgn_pz" | "bdy_pz" | "srf_pz" => 2, _ => return None, }; let Ok(target) = value.trim().parse::() else { return Some(false); }; if !target.is_finite() { return Some(false); } let Some(point) = self .document .get_entity(handle) .and_then(crate::entities::solid3d::point_of_reference) else { return Some(false); }; let world = glam::DVec3::new(point.x, point.y, point.z); let mut local = plane.to_local(world); local[axis] = target; let delta = plane.to_world(local) - world; if delta.length_squared() <= f64::EPSILON { return Some(true); } self.transform_entities(&[handle], &EntityTransform::Translate(delta)); Some(true) } pub(crate) fn translate_solid_geometry(&mut self, handle: Handle, delta: [f64; 3]) { if delta.iter().all(|value| value.abs() <= f64::EPSILON) { return; } let placement = kernel::brep::Placement::at(delta); if let Some(body) = self.solid_models.get(&handle).cloned() { if let Some(moved) = kernel::brep::transform(&body, &placement) { self.solid_models.insert(handle, moved); } } let Some(set) = self.meshes.get_mut(&handle) else { return; }; let translate_split = |high: &mut [f32; 3], low: &mut [f32; 3]| { let absolute = [ high[0] as f64 + low[0] as f64 + delta[0], high[1] as f64 + low[1] as f64 + delta[1], high[2] as f64 + low[2] as f64 + delta[2], ]; *high = [absolute[0] as f32, absolute[1] as f32, absolute[2] as f32]; *low = [ (absolute[0] - high[0] as f64) as f32, (absolute[1] - high[1] as f64) as f32, (absolute[2] - high[2] as f64) as f32, ]; }; for lod in &mut set.lods { if lod.verts_low.len() != lod.verts.len() { lod.verts_low = vec![[0.0; 3]; lod.verts.len()]; } for (high, low) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) { translate_split(high, low); } } if set.edge_verts_low.len() != set.edge_verts.len() { set.edge_verts_low = vec![[0.0; 3]; set.edge_verts.len()]; } for (high, low) in set.edge_verts.iter_mut().zip(set.edge_verts_low.iter_mut()) { translate_split(high, low); } for generator in &mut set.curved_gens { if let Some(source) = kernel::brep::mesh::transform_silhouette(&generator.source, &placement) { generator.source = source; } } set.metrics.translate(delta); set.recompute_aabb(); } pub fn apply_grip(&mut self, handle: Handle, grip_id: usize, apply: GripApply) { // Objects on a locked layer can't be grip-edited. if self.is_layer_locked(handle) { return; } // An attribute grip sits on the displayed (annotation-scaled) // attribute; the stored one is unscaled. let apply = match self.document.get_entity(handle) { Some(EntityType::Insert(insert)) => crate::entities::insert::unscale_attribute_grip( insert, self.annotation_scale, grip_id, apply, ), _ => apply, }; if self.apply_solid_history_grip(handle, grip_id, apply.clone()) { return; } // For Solid3D / Region / Body, record the old point_of_reference so we // can translate the pre-tessellated MeshModel by the same delta after // the grip is applied (the ACIS data itself is not modified). let old_por: Option<[f64; 3]> = self .document .get_entity(handle) .and_then(crate::entities::solid3d::point_of_reference) .map(|p| [p.x, p.y, p.z]); // A LEADER's final vertex is the end of its horizontal landing. // Remember its old position and linked MTEXT so the annotation can follow // when that grip stretches the landing. let leader_landing_before = self.document.get_entity(handle).and_then(|entity| { let EntityType::Leader(leader) = entity else { return None; }; let n = leader.vertices.len(); if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null() { return None; } let point = leader.vertices.last()?; Some(( leader.annotation_handle, glam::DVec3::new(point.x, point.y, point.z), )) }); if let Some(entity) = self.document.get_entity_mut(handle) { view::dispatch::apply_grip(entity, grip_id, apply); } self.sync_diameter_association_angle(handle); if let Some((annotation_handle, old_landing)) = leader_landing_before { let new_landing = self.document.get_entity(handle).and_then(|entity| { let EntityType::Leader(leader) = entity else { return None; }; let point = leader.vertices.last()?; Some(glam::DVec3::new(point.x, point.y, point.z)) }); if let Some(new_landing) = new_landing { let delta = new_landing - old_landing; if delta.length_squared() > 1.0e-20 { if self.is_recording_undo() { if let Some(before) = self.document.get_entity_arc(annotation_handle) { self.record_undo_before(annotation_handle, Some(before)); } } if let Some(annotation) = self.document.get_entity_mut(annotation_handle) { view::dispatch::apply_transform( annotation, &crate::command::EntityTransform::Translate(delta), ); } if self.sync_displayed_annotation_context(annotation_handle) { self.poison_undo_recording(); } self.bump_entities(&[(annotation_handle, crate::scene::ChangeKind::Modified)]); } } } if self.sync_displayed_annotation_context(handle) { self.poison_undo_recording(); } // A dimension loaded from a file renders through its baked *D block; // the grip only moved a definition point, so the baked graphics are // stale — drop them so tessellation falls back to the live geometry // and the next save re-bakes (no-op for non-dimensions). (#398) self.invalidate_dim_block_recorded(handle); // Translate MeshModel vertices by the same delta the grip applied. if let Some(old) = old_por { let new_por: Option<[f64; 3]> = self .document .get_entity(handle) .and_then(crate::entities::solid3d::point_of_reference) .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]]; let moved_history = self.transform_solid_history( handle, &EntityTransform::Translate(glam::DVec3::from_array(delta)), ); if !moved_history { self.translate_solid_geometry(handle, delta); } } } // Rebuild GPU hatch/solid model when a boundary vertex or corner moves. match self.document.get_entity(handle) { Some(EntityType::Hatch(dxf)) => { let color = convert::tess_util::aci_to_rgba(&dxf.common.color); if let Some(model) = Self::hatch_model_from_dxf(dxf, color) { self.hatches.insert(handle, model); } else { self.hatches.remove(&handle); } } Some(EntityType::Solid(solid)) => { let color = convert::tess_util::aci_to_rgba(&solid.common.color); self.hatches .insert(handle, Self::solid_hatch_model(solid, color)); } _ => {} } // The grip-drag caller refreshes changed resident meshes per move. // Raster images, OLE frames and PDF underlays keep a derived ImageModel // with their textured quad geometry. A grip edit mutates the entity directly, // so rebuild that cache as well or only the wire frame follows the grip. let image_bearing = self.document.get_entity(handle).is_some_and(|entity| { matches!( entity, EntityType::RasterImage(_) | EntityType::Ole2Frame(_) | EntityType::Underlay(_) ) }); if image_bearing { let model = self .document .get_entity(handle) .and_then(|entity| self.image_seed_for(entity)); self.images.remove(&handle); if let Some(model) = model { self.images.insert(handle, model); } } // A grip edit can move geometry used as the boundary of an associative hatch. // The source entity has already been mutated above, so rebuild any dependent // hatch from the live document geometry immediately. let source_change = [(handle, ChangeKind::Modified)]; let hatch_changes = self.refresh_associative_hatches(&source_change); if !hatch_changes.is_empty() { self.bump_entities(&hatch_changes); } } } /// A copy reacts only to what was copied with it: each reactor of the source /// that maps to a copy follows it there, and the rest (the source's dimension /// association, group, associative hatch) are dropped. The association and /// group copies add their own reactors afterwards. pub(crate) fn remap_copied_reactors( document: &mut codec::CadDocument, handle_map: &rustc_hash::FxHashMap, ) { for © in handle_map.values() { let Some(entity) = document.get_entity_mut(copy) else { continue; }; let reactors = &mut entity.common_mut().reactors; *reactors = reactors .iter() .filter_map(|reactor| handle_map.get(reactor).copied()) .collect(); } } #[cfg(test)] mod define_transformed_block_xdict_tests { use super::*; /// No extension dictionary may be referenced by more than one entity. fn assert_no_shared_xdicts(doc: &codec::CadDocument) { use std::collections::HashMap; let mut owners: HashMap = HashMap::new(); for entity in doc.entities() { if let Some(xd) = entity.common().xdictionary_handle { if !xd.is_null() { *owners.entry(xd).or_default() += 1; } } } let shared: Vec<_> = owners .iter() .filter(|(_, &n)| n > 1) .map(|(h, n)| format!("{h:?} x{n}")) .collect(); assert!( shared.is_empty(), "shared extension dictionaries: {}", shared.join(", ") ); } /// Cloned sub-entities must not share the source's extension dictionary. /// `define_transformed_block` clones each sub wholesale; without clearing /// `xdictionary_handle` the clone claims the source's dictionary and /// file validation reports "Extension dictionary Cannot access". #[test] fn cloned_subs_do_not_share_source_xdict() { let mut scene = Scene::new(); let dict_handle = scene.document.allocate_handle(); let mut dict = codec::objects::Dictionary::new(); dict.handle = dict_handle; scene .document .objects .insert(dict_handle, codec::objects::ObjectType::Dictionary(dict)); let mut br = codec::tables::BlockRecord::new("SRC"); br.handle = scene.document.allocate_handle(); scene.document.block_records.add(br).unwrap(); let mut line = codec::entities::Line::new(); line.common.xdictionary_handle = Some(dict_handle); let src_handle = scene .document .add_entity(codec::EntityType::Line(line)) .unwrap(); scene .document .block_records .get_mut("SRC") .unwrap() .entity_handles .push(src_handle); let subs = vec![scene.document.get_entity(src_handle).unwrap().clone()]; let name = scene .define_transformed_block( &subs, &crate::command::EntityTransform::Translate(glam::DVec3::ZERO), ) .expect("clone block must be created"); assert!( name.starts_with("*D"), "anonymous block expected, got {name}" ); assert_no_shared_xdicts(&scene.document); // Source keeps its dictionary. match scene.document.get_entity(src_handle) { Some(e) => assert_eq!( e.common().xdictionary_handle, Some(dict_handle), "the source must keep its own dictionary" ), None => panic!("source entity missing"), } // Clones carry none. let owned = scene .document .block_records .get(&name) .unwrap() .entity_handles .clone(); assert!(!owned.is_empty(), "clone block should own sub-entities"); for h in &owned { match scene.document.get_entity(*h) { Some(e) => assert!( e.common().xdictionary_handle.is_none(), "cloned child {h:?} must not carry an extension dictionary" ), None => panic!("cloned child {h:?} missing"), } } } }