fix(blocks): preserve UCS transformations
Bake BEDIT frames into block-local geometry while keeping model and viewport UCS isolated. Normalize legacy nonzero block origins during load.
This commit is contained in:
parent
02d15f5d23
commit
2c64888048
22 changed files with 693 additions and 41 deletions
|
|
@ -607,7 +607,8 @@ impl Scene {
|
|||
&mut self,
|
||||
handles: &[Handle],
|
||||
name: &str,
|
||||
base: glam::DVec3,
|
||||
world_to_block: &acadrust::types::Transform,
|
||||
block_to_world: &acadrust::types::Transform,
|
||||
) -> Result<Handle, String> {
|
||||
let name = name.trim();
|
||||
if name.is_empty() {
|
||||
|
|
@ -656,7 +657,7 @@ impl Scene {
|
|||
.add_entity(EntityType::BlockEnd(block_end))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let local = EntityTransform::Translate(-base);
|
||||
let local = EntityTransform::Affine(*world_to_block);
|
||||
for (old_handle, mut entity) in source_entities {
|
||||
view::dispatch::apply_transform(&mut entity, &local);
|
||||
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
||||
|
|
@ -668,10 +669,8 @@ impl Scene {
|
|||
self.erase_entities(&[old_handle]);
|
||||
}
|
||||
|
||||
let insert = DxfInsert::new(
|
||||
name,
|
||||
acadrust::types::Vector3::new(base.x, base.y, base.z),
|
||||
);
|
||||
let mut insert = DxfInsert::new(name, acadrust::types::Vector3::ZERO);
|
||||
acadrust::Entity::apply_transform(&mut insert, block_to_world);
|
||||
Ok(self.add_entity(EntityType::Insert(insert)))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2466,6 +2466,51 @@ impl Scene {
|
|||
self.push_geometry_delta(epoch, changes.to_vec(), false);
|
||||
}
|
||||
|
||||
/// True when any changed handle belongs to an ordinary block definition
|
||||
/// rather than model/paper space. History replay needs this distinction:
|
||||
/// restoring a block-local entity must invalidate the assembled definition
|
||||
/// and every INSERT that consumes it, not only that entity's direct wire.
|
||||
///
|
||||
/// Removed entities may no longer have a live slot, so fall back to the
|
||||
/// BlockRecord membership list, which deliberately retains their handles
|
||||
/// for symmetric undo/redo restoration.
|
||||
pub(crate) fn changes_touch_block_definition(
|
||||
&self,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
) -> bool {
|
||||
let layout_blocks: HashSet<Handle> = self
|
||||
.document
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|object| match object {
|
||||
ObjectType::Layout(layout) if !layout.block_record.is_null() => {
|
||||
Some(layout.block_record)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let is_definition = |record: &acadrust::tables::BlockRecord| {
|
||||
let name = record.name.to_ascii_uppercase();
|
||||
!layout_blocks.contains(&record.handle)
|
||||
&& !name.starts_with("*MODEL_SPACE")
|
||||
&& !name.starts_with("*PAPER_SPACE")
|
||||
};
|
||||
|
||||
changes.iter().any(|(handle, _)| {
|
||||
let owner = self
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.map(|entity| entity.common().owner_handle);
|
||||
self.document.block_records.iter().any(|record| {
|
||||
is_definition(record)
|
||||
&& (owner == Some(record.handle)
|
||||
|| record.block_entity_handle == *handle
|
||||
|| record.block_end_handle == *handle
|
||||
|| record.entity_handles.contains(handle))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bump_geometry(&mut self) {
|
||||
let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||
self.geometry_epoch = epoch;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,44 @@ struct TextOrient {
|
|||
x_scale: f64,
|
||||
}
|
||||
|
||||
fn inverse_affine(
|
||||
transform: &acadrust::types::Transform,
|
||||
) -> Option<acadrust::types::Transform> {
|
||||
use acadrust::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<TextOrient> {
|
||||
|
|
@ -229,6 +267,164 @@ impl Scene {
|
|||
self.bump_entities(&changes);
|
||||
}
|
||||
|
||||
/// 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<Handle> {
|
||||
let Some(record) = self
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == block_record)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let root_handles: HashSet<Handle> = 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: &acadrust::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<Handle> = 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<Handle> = handles.iter().copied().collect();
|
||||
let transform = EntityTransform::Affine(*local_from_old);
|
||||
let matrix = &local_from_old.matrix.m;
|
||||
let dependent_transform =
|
||||
EntityTransform::Affine(acadrust::types::Transform::from_matrix(
|
||||
acadrust::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.
|
||||
let references: Vec<(Handle, acadrust::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 = reference.get_transform();
|
||||
let inverse = inverse_affine(&insertion)?;
|
||||
Some((
|
||||
reference.common.handle,
|
||||
acadrust::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 {
|
||||
acadrust::Entity::apply_transform(attribute, &attribute_transform);
|
||||
}
|
||||
changed.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
for handle in changed.iter().copied() {
|
||||
let _ = crate::scene::annotative::sync_active_context_from_entity(
|
||||
&mut self.document,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -273,6 +273,55 @@ impl Camera {
|
|||
Mat4::from_quat(self.rotation.conjugate())
|
||||
}
|
||||
|
||||
/// Carry the camera through a rigid model-space transform. BEDIT uses this
|
||||
/// when a transient UCS is baked into block-local geometry: the contents
|
||||
/// retain their on-screen framing while their canonical coordinates change.
|
||||
pub fn apply_rigid_transform(&mut self, transform: &acadrust::types::Transform) {
|
||||
let point = acadrust::types::Vector3::new(
|
||||
self.target.x,
|
||||
self.target.y,
|
||||
self.target.z,
|
||||
);
|
||||
let point = transform.apply(point);
|
||||
self.target = DVec3::new(point.x, point.y, point.z);
|
||||
|
||||
let matrix = &transform.matrix.m;
|
||||
let rotation = glam::Mat3::from_cols(
|
||||
Vec3::new(matrix[0][0] as f32, matrix[1][0] as f32, matrix[2][0] as f32),
|
||||
Vec3::new(matrix[0][1] as f32, matrix[1][1] as f32, matrix[2][1] as f32),
|
||||
Vec3::new(matrix[0][2] as f32, matrix[1][2] as f32, matrix[2][2] as f32),
|
||||
);
|
||||
self.rotation = (Quat::from_mat3(&rotation) * self.rotation).normalize();
|
||||
self.sync_yaw_pitch();
|
||||
|
||||
if let Some((min, max)) = self.model_bounds {
|
||||
let corners = [
|
||||
Vec3::new(min.x, min.y, min.z),
|
||||
Vec3::new(max.x, min.y, min.z),
|
||||
Vec3::new(min.x, max.y, min.z),
|
||||
Vec3::new(max.x, max.y, min.z),
|
||||
Vec3::new(min.x, min.y, max.z),
|
||||
Vec3::new(max.x, min.y, max.z),
|
||||
Vec3::new(min.x, max.y, max.z),
|
||||
Vec3::new(max.x, max.y, max.z),
|
||||
];
|
||||
let mut new_min = Vec3::splat(f32::INFINITY);
|
||||
let mut new_max = Vec3::splat(f32::NEG_INFINITY);
|
||||
for corner in corners {
|
||||
let transformed = transform.apply(acadrust::types::Vector3::new(
|
||||
corner.x as f64,
|
||||
corner.y as f64,
|
||||
corner.z as f64,
|
||||
));
|
||||
let transformed =
|
||||
Vec3::new(transformed.x as f32, transformed.y as f32, transformed.z as f32);
|
||||
new_min = new_min.min(transformed);
|
||||
new_max = new_max.max(transformed);
|
||||
}
|
||||
self.model_bounds = Some((new_min, new_max));
|
||||
}
|
||||
}
|
||||
|
||||
/// The camera's roll — rotation about the view axis, in radians — the
|
||||
/// inverse of the `roll` argument to [`yaw_pitch_to_quat`]. Recovered by
|
||||
/// removing the yaw/pitch frame from the live rotation, so a saved view can
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ where
|
|||
}
|
||||
EntityTransform::Scale { center, factor } => apply_standard_scale(entity, *center, *factor),
|
||||
EntityTransform::Mirror { p1, p2 } => mirror(entity, *p1, *p2),
|
||||
EntityTransform::Affine(transform) => entity.apply_transform(transform),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue