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
|
|
@ -932,9 +932,17 @@ impl OpenCADStudio {
|
||||||
base,
|
base,
|
||||||
} => {
|
} => {
|
||||||
self.push_undo_snapshot(i, "BLOCK");
|
self.push_undo_snapshot(i, "BLOCK");
|
||||||
|
let ucs = self.tabs[i].ucs_xform();
|
||||||
|
let world_to_block = ucs.to_ucs_transform_at(base);
|
||||||
|
let block_to_world = ucs.to_wcs_transform_at(base);
|
||||||
match self.tabs[i]
|
match self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.create_block_from_entities(&handles, &name, base)
|
.create_block_from_entities(
|
||||||
|
&handles,
|
||||||
|
&name,
|
||||||
|
&world_to_block,
|
||||||
|
&block_to_world,
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Ok(insert_handle) => {
|
Ok(insert_handle) => {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
|
|
||||||
|
|
@ -406,8 +406,8 @@ impl OpenCADStudio {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot the block's block-local entities so Discard can restore
|
// Snapshot the complete block definition so Discard can restore
|
||||||
// them (skip structural Block/BlockEnd/AttDef, mirroring REFEDIT).
|
// structural markers, ATTDEFs and geometry with exact handles.
|
||||||
let snapshot: Vec<_> = {
|
let snapshot: Vec<_> = {
|
||||||
let br = self.tabs[i]
|
let br = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -418,26 +418,45 @@ impl OpenCADStudio {
|
||||||
br.entity_handles
|
br.entity_handles
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||||
.filter(|e| {
|
|
||||||
!matches!(
|
|
||||||
e,
|
|
||||||
acadrust::EntityType::Block(_)
|
|
||||||
| acadrust::EntityType::BlockEnd(_)
|
|
||||||
| acadrust::EntityType::AttributeDefinition(_)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
let dependent_snapshot: Vec<_> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.block_definition_dependent_handles(br_handle)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|handle| {
|
||||||
|
self.tabs[i].scene.document.get_entity_arc(handle)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let reference_attributes: Vec<_> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| match entity {
|
||||||
|
acadrust::EntityType::Insert(reference)
|
||||||
|
if reference.block_name.eq_ignore_ascii_case(&insert.block_name)
|
||||||
|
&& !reference.attributes.is_empty() =>
|
||||||
|
{
|
||||||
|
Some((
|
||||||
|
reference.common.handle,
|
||||||
|
reference.attributes.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
self.push_undo_snapshot(i, "BEDIT");
|
self.push_undo_snapshot(i, "BEDIT");
|
||||||
|
|
||||||
// Capture the camera before the editor reframes it, so leaving
|
// Capture the camera before the editor reframes it, so leaving
|
||||||
// the block editor returns the view exactly where it was (#425).
|
// the block editor returns the view exactly where it was (#425).
|
||||||
let current_camera = self.tabs[i].scene.camera.borrow().clone();
|
let current_camera = self.tabs[i].scene.camera.borrow().clone();
|
||||||
|
let current_ucs = self.tabs[i].active_ucs.clone();
|
||||||
let (return_layout, return_block, return_camera) =
|
let (return_layout, return_block, return_camera) =
|
||||||
if let Some(parent_index) = self.tabs[i].active_block_edit {
|
if let Some(parent_index) = self.tabs[i].active_block_edit {
|
||||||
let parent = &mut self.tabs[i].block_edits[parent_index];
|
let parent = &mut self.tabs[i].block_edits[parent_index];
|
||||||
parent.editor_camera = current_camera.clone();
|
parent.editor_camera = current_camera.clone();
|
||||||
|
parent.editor_ucs = current_ucs;
|
||||||
(
|
(
|
||||||
parent.return_layout.clone(),
|
parent.return_layout.clone(),
|
||||||
Some(parent.block_name.clone()),
|
Some(parent.block_name.clone()),
|
||||||
|
|
@ -463,10 +482,16 @@ impl OpenCADStudio {
|
||||||
return_layout,
|
return_layout,
|
||||||
return_block,
|
return_block,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
dependent_snapshot,
|
||||||
|
reference_attributes,
|
||||||
return_camera,
|
return_camera,
|
||||||
editor_camera: current_camera,
|
editor_camera: current_camera,
|
||||||
|
editor_ucs: None,
|
||||||
});
|
});
|
||||||
self.tabs[i].active_block_edit = Some(self.tabs[i].block_edits.len() - 1);
|
self.tabs[i].active_block_edit = Some(self.tabs[i].block_edits.len() - 1);
|
||||||
|
// Every BEDIT tab owns an isolated local UCS. Never inherit or
|
||||||
|
// overwrite the drawing's model-space UCS.
|
||||||
|
self.tabs[i].refresh_active_ucs();
|
||||||
|
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
// The first click of a double-click selects the model-space
|
// The first click of a double-click selects the model-space
|
||||||
|
|
@ -574,10 +599,25 @@ impl OpenCADStudio {
|
||||||
let return_layout = session.return_layout.clone();
|
let return_layout = session.return_layout.clone();
|
||||||
let return_camera = session.return_camera.clone();
|
let return_camera = session.return_camera.clone();
|
||||||
for mut entity in session.snapshot {
|
for mut entity in session.snapshot {
|
||||||
entity.common_mut().handle = acadrust::Handle::NULL;
|
// Preserve the original handle so ATTDEF references and
|
||||||
|
// other handle-based relationships survive Discard.
|
||||||
entity.common_mut().owner_handle = session.br_handle;
|
entity.common_mut().owner_handle = session.br_handle;
|
||||||
let _ = self.tabs[i].scene.document.add_entity(entity);
|
let _ = self.tabs[i].scene.document.add_entity(entity);
|
||||||
}
|
}
|
||||||
|
for (handle, attributes) in session.reference_attributes {
|
||||||
|
if let Some(acadrust::EntityType::Insert(reference)) =
|
||||||
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
|
{
|
||||||
|
reference.attributes = attributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for entity in session.dependent_snapshot {
|
||||||
|
let handle = entity.common().handle;
|
||||||
|
let _ = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.replace_entity_arc(handle, entity);
|
||||||
|
}
|
||||||
self.restore_after_block_edit_close(
|
self.restore_after_block_edit_close(
|
||||||
i,
|
i,
|
||||||
return_block.as_deref(),
|
return_block.as_deref(),
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ impl OpenCADStudio {
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
let parts: Vec<&str> = cmd.splitn(4, ' ').collect();
|
let parts: Vec<&str> = cmd.splitn(4, ' ').collect();
|
||||||
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
||||||
|
let mut active_changed = false;
|
||||||
match sub.as_str() {
|
match sub.as_str() {
|
||||||
"" | "LIST" | "?" => {
|
"" | "LIST" | "?" => {
|
||||||
let active_name = self.tabs[i]
|
let active_name = self.tabs[i]
|
||||||
|
|
@ -248,6 +249,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
"W" | "WORLD" => {
|
"W" | "WORLD" => {
|
||||||
self.tabs[i].active_ucs = None;
|
self.tabs[i].active_ucs = None;
|
||||||
|
active_changed = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output("UCS reset to World Coordinate System.");
|
.push_output("UCS reset to World Coordinate System.");
|
||||||
}
|
}
|
||||||
|
|
@ -271,6 +273,7 @@ impl OpenCADStudio {
|
||||||
wcs_origin.y as f64,
|
wcs_origin.y as f64,
|
||||||
wcs_origin.z as f64,
|
wcs_origin.z as f64,
|
||||||
);
|
);
|
||||||
|
active_changed = true;
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"UCS origin set to ({:.4}, {:.4}, {:.4}).",
|
"UCS origin set to ({:.4}, {:.4}, {:.4}).",
|
||||||
wcs_origin.x, wcs_origin.y, wcs_origin.z
|
wcs_origin.x, wcs_origin.y, wcs_origin.z
|
||||||
|
|
@ -313,6 +316,7 @@ impl OpenCADStudio {
|
||||||
Vector3::new(ny.x as f64, ny.y as f64, ny.z as f64);
|
Vector3::new(ny.x as f64, ny.y as f64, ny.z as f64);
|
||||||
}
|
}
|
||||||
self.tabs[i].active_ucs = Some(new_ucs);
|
self.tabs[i].active_ucs = Some(new_ucs);
|
||||||
|
active_changed = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("UCS rotated {:.2}° around Z.", angle_deg));
|
.push_output(&format!("UCS rotated {:.2}° around Z.", angle_deg));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -340,6 +344,7 @@ impl OpenCADStudio {
|
||||||
let rot = glam::Quat::from_axis_angle(x_ax, rad);
|
let rot = glam::Quat::from_axis_angle(x_ax, rad);
|
||||||
let ny = rot * old_y;
|
let ny = rot * old_y;
|
||||||
ucs.y_axis = Vector3::new(ny.x as f64, ny.y as f64, ny.z as f64);
|
ucs.y_axis = Vector3::new(ny.x as f64, ny.y as f64, ny.z as f64);
|
||||||
|
active_changed = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("UCS rotated {:.2}° around X.", angle_deg));
|
.push_output(&format!("UCS rotated {:.2}° around X.", angle_deg));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -367,6 +372,7 @@ impl OpenCADStudio {
|
||||||
let rot = glam::Quat::from_axis_angle(y_ax, rad);
|
let rot = glam::Quat::from_axis_angle(y_ax, rad);
|
||||||
let nx = rot * old_x;
|
let nx = rot * old_x;
|
||||||
ucs.x_axis = Vector3::new(nx.x as f64, nx.y as f64, nx.z as f64);
|
ucs.x_axis = Vector3::new(nx.x as f64, nx.y as f64, nx.z as f64);
|
||||||
|
active_changed = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("UCS rotated {:.2}° around Y.", angle_deg));
|
.push_output(&format!("UCS rotated {:.2}° around Y.", angle_deg));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -378,6 +384,7 @@ impl OpenCADStudio {
|
||||||
let name = sub.clone();
|
let name = sub.clone();
|
||||||
if let Some(named) = self.tabs[i].scene.document.ucss.get(&name).cloned() {
|
if let Some(named) = self.tabs[i].scene.document.ucss.get(&name).cloned() {
|
||||||
self.tabs[i].active_ucs = Some(named);
|
self.tabs[i].active_ucs = Some(named);
|
||||||
|
active_changed = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("UCS '{}' activated.", name));
|
.push_output(&format!("UCS '{}' activated.", name));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -388,11 +395,9 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Keep the scene's ViewCube UCS in lock-step with active_ucs and
|
if active_changed {
|
||||||
// persist it to the active pane (per-viewport UCS inside a
|
self.commit_active_ucs_change(i, "UCS");
|
||||||
// viewport, header model UCS in the Model tab) so it round-trips.
|
}
|
||||||
self.tabs[i].sync_ucs_to_scene();
|
|
||||||
self.tabs[i].persist_active_ucs();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Named Views (VIEW command) ────────────────────────────────
|
// ── Named Views (VIEW command) ────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ impl DocumentTab {
|
||||||
|
|
||||||
/// The document's saved model-space UCS (header), as a `Ucs`. `None` when it
|
/// The document's saved model-space UCS (header), as a `Ucs`. `None` when it
|
||||||
/// is identity (plain WCS).
|
/// is identity (plain WCS).
|
||||||
fn model_ucs_from_header(&self) -> Option<Ucs> {
|
pub(super) fn model_ucs_from_header(&self) -> Option<Ucs> {
|
||||||
let h = &self.scene.document.header;
|
let h = &self.scene.document.header;
|
||||||
let mut u = Ucs::new(h.model_space_ucs_name.clone());
|
let mut u = Ucs::new(h.model_space_ucs_name.clone());
|
||||||
u.origin = h.model_space_ucs_origin;
|
u.origin = h.model_space_ucs_origin;
|
||||||
|
|
@ -281,7 +281,9 @@ impl DocumentTab {
|
||||||
/// change (enter/exit viewport, layout / tab switch, load) so one field
|
/// change (enter/exit viewport, layout / tab switch, load) so one field
|
||||||
/// drives all UCS-aware systems regardless of where editing happens.
|
/// drives all UCS-aware systems regardless of where editing happens.
|
||||||
pub(super) fn refresh_active_ucs(&mut self) {
|
pub(super) fn refresh_active_ucs(&mut self) {
|
||||||
self.active_ucs = if let Some(h) = self.scene.active_viewport {
|
self.active_ucs = if let Some(session) = self.active_block_edit_session() {
|
||||||
|
session.editor_ucs.clone()
|
||||||
|
} else if let Some(h) = self.scene.active_viewport {
|
||||||
self.ucs_from_viewport(h)
|
self.ucs_from_viewport(h)
|
||||||
} else if self.scene.current_layout == "Model" {
|
} else if self.scene.current_layout == "Model" {
|
||||||
self.model_ucs_from_header()
|
self.model_ucs_from_header()
|
||||||
|
|
@ -297,16 +299,28 @@ impl DocumentTab {
|
||||||
/// any UCS change.
|
/// any UCS change.
|
||||||
pub(super) fn persist_active_ucs(&mut self) {
|
pub(super) fn persist_active_ucs(&mut self) {
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
if let Some(h) = self.scene.active_viewport {
|
if let Some(index) = self.active_block_edit {
|
||||||
let (o, x, y, per) = match &self.active_ucs {
|
let active_ucs = self.active_ucs.clone();
|
||||||
Some(u) => (u.origin, u.x_axis, u.y_axis, true),
|
if let Some(session) = self.block_edits.get_mut(index) {
|
||||||
None => (Vector3::ZERO, Vector3::UNIT_X, Vector3::UNIT_Y, false),
|
session.editor_ucs = active_ucs;
|
||||||
|
}
|
||||||
|
} else if let Some(h) = self.scene.active_viewport {
|
||||||
|
let (o, x, y, handle, per) = match &self.active_ucs {
|
||||||
|
Some(u) => (u.origin, u.x_axis, u.y_axis, u.handle, true),
|
||||||
|
None => (
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::UNIT_X,
|
||||||
|
Vector3::UNIT_Y,
|
||||||
|
Handle::NULL,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
if let Some(acadrust::EntityType::Viewport(vp)) = self.scene.document.get_entity_mut(h)
|
if let Some(acadrust::EntityType::Viewport(vp)) = self.scene.document.get_entity_mut(h)
|
||||||
{
|
{
|
||||||
vp.ucs_origin = o;
|
vp.ucs_origin = o;
|
||||||
vp.ucs_x_axis = x;
|
vp.ucs_x_axis = x;
|
||||||
vp.ucs_y_axis = y;
|
vp.ucs_y_axis = y;
|
||||||
|
vp.ucs_handle = handle;
|
||||||
vp.ucs_per_viewport = per;
|
vp.ucs_per_viewport = per;
|
||||||
}
|
}
|
||||||
} else if self.scene.current_layout == "Model" {
|
} else if self.scene.current_layout == "Model" {
|
||||||
|
|
@ -316,11 +330,13 @@ impl DocumentTab {
|
||||||
h.model_space_ucs_origin = u.origin;
|
h.model_space_ucs_origin = u.origin;
|
||||||
h.model_space_ucs_x_axis = u.x_axis;
|
h.model_space_ucs_x_axis = u.x_axis;
|
||||||
h.model_space_ucs_y_axis = u.y_axis;
|
h.model_space_ucs_y_axis = u.y_axis;
|
||||||
|
h.model_space_ucs_name = u.name.clone();
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
h.model_space_ucs_origin = Vector3::ZERO;
|
h.model_space_ucs_origin = Vector3::ZERO;
|
||||||
h.model_space_ucs_x_axis = Vector3::UNIT_X;
|
h.model_space_ucs_x_axis = Vector3::UNIT_X;
|
||||||
h.model_space_ucs_y_axis = Vector3::UNIT_Y;
|
h.model_space_ucs_y_axis = Vector3::UNIT_Y;
|
||||||
|
h.model_space_ucs_name.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,14 @@ impl UcsXform {
|
||||||
pub(super) fn from_ucs(ucs: &Ucs) -> Self {
|
pub(super) fn from_ucs(ucs: &Ucs) -> Self {
|
||||||
let v = |a: acadrust::types::Vector3| glam::DVec3::new(a.x, a.y, a.z);
|
let v = |a: acadrust::types::Vector3| glam::DVec3::new(a.x, a.y, a.z);
|
||||||
let x = v(ucs.x_axis).normalize_or(glam::DVec3::X);
|
let x = v(ucs.x_axis).normalize_or(glam::DVec3::X);
|
||||||
let y = v(ucs.y_axis).normalize_or(glam::DVec3::Y);
|
let raw_y = v(ucs.y_axis).normalize_or(glam::DVec3::Y);
|
||||||
let z = x.cross(y).normalize_or(glam::DVec3::Z);
|
let fallback_z = if x.dot(glam::DVec3::Z).abs() < 0.999 {
|
||||||
|
glam::DVec3::Z
|
||||||
|
} else {
|
||||||
|
glam::DVec3::Y
|
||||||
|
};
|
||||||
|
let z = x.cross(raw_y).normalize_or(x.cross(fallback_z).normalize());
|
||||||
|
let y = z.cross(x).normalize();
|
||||||
Self { origin: v(ucs.origin), x, y, z }
|
Self { origin: v(ucs.origin), x, y, z }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,6 +133,44 @@ impl UcsXform {
|
||||||
glam::Vec4::W,
|
glam::Vec4::W,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full UCS-local → WCS transform, using `origin` as local zero while
|
||||||
|
/// retaining this UCS's orthonormal axes.
|
||||||
|
pub(super) fn to_wcs_transform_at(
|
||||||
|
&self,
|
||||||
|
origin: glam::DVec3,
|
||||||
|
) -> acadrust::types::Transform {
|
||||||
|
use acadrust::types::{Matrix4, Transform};
|
||||||
|
Transform::from_matrix(Matrix4 {
|
||||||
|
m: [
|
||||||
|
[self.x.x, self.y.x, self.z.x, origin.x],
|
||||||
|
[self.x.y, self.y.y, self.z.y, origin.y],
|
||||||
|
[self.x.z, self.y.z, self.z.z, origin.z],
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full WCS → UCS-local transform, using `origin` as the local zero.
|
||||||
|
pub(super) fn to_ucs_transform_at(
|
||||||
|
&self,
|
||||||
|
origin: glam::DVec3,
|
||||||
|
) -> acadrust::types::Transform {
|
||||||
|
use acadrust::types::{Matrix4, Transform};
|
||||||
|
Transform::from_matrix(Matrix4 {
|
||||||
|
m: [
|
||||||
|
[self.x.x, self.x.y, self.x.z, -origin.dot(self.x)],
|
||||||
|
[self.y.x, self.y.y, self.y.z, -origin.dot(self.y)],
|
||||||
|
[self.z.x, self.z.y, self.z.z, -origin.dot(self.z)],
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert from the represented UCS into its canonical local frame.
|
||||||
|
pub(super) fn to_ucs_transform(&self) -> acadrust::types::Transform {
|
||||||
|
self.to_ucs_transform_at(self.origin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ────────────────────
|
// ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ────────────────────
|
||||||
|
|
|
||||||
|
|
@ -969,11 +969,23 @@ impl OpenCADStudio {
|
||||||
scene.reseed_derived_caches(handle);
|
scene.reseed_derived_caches(handle);
|
||||||
}
|
}
|
||||||
if !final_changes.is_empty() {
|
if !final_changes.is_empty() {
|
||||||
|
if scene.changes_touch_block_definition(&final_changes) {
|
||||||
|
// A block-local delta changes the assembled definition
|
||||||
|
// consumed by every INSERT (including nested blocks).
|
||||||
|
// Replaying only the child handle leaves those cached
|
||||||
|
// references visually stale after BEDIT closes.
|
||||||
|
scene.bump_geometry();
|
||||||
|
} else {
|
||||||
scene.bump_entities(&final_changes);
|
scene.bump_entities(&final_changes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
scene.clear_preview_wire();
|
scene.clear_preview_wire();
|
||||||
}
|
}
|
||||||
|
// UCS state is cached separately from its persisted header/viewport
|
||||||
|
// fields. History may just have restored those fields, so adopt them
|
||||||
|
// before drawing the icon/grid or accepting the next coordinate.
|
||||||
|
self.tabs[i].refresh_active_ucs();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].active_grip = None;
|
self.tabs[i].active_grip = None;
|
||||||
|
|
|
||||||
|
|
@ -493,7 +493,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
&& (self.ucs_grip_drag.is_some() || self.ucs_icon_selected)
|
&& (self.ucs_grip_drag.is_some() || self.ucs_icon_selected)
|
||||||
{
|
{
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.ucs_grip_drag = None;
|
// A UCS grip drag is live-only until mouse release. Reload
|
||||||
|
// the persisted pane UCS so Escape really cancels it.
|
||||||
|
if self.ucs_grip_drag.take().is_some() {
|
||||||
|
self.tabs[i].refresh_active_ucs();
|
||||||
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
|
}
|
||||||
self.ucs_icon_selected = false;
|
self.ucs_icon_selected = false;
|
||||||
self.ucs_icon_hover = false;
|
self.ucs_icon_hover = false;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|
@ -1319,6 +1324,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
vp.ucs_origin = ucs.origin.clone();
|
vp.ucs_origin = ucs.origin.clone();
|
||||||
vp.ucs_x_axis = ucs.x_axis.clone();
|
vp.ucs_x_axis = ucs.x_axis.clone();
|
||||||
vp.ucs_y_axis = ucs.y_axis.clone();
|
vp.ucs_y_axis = ucs.y_axis.clone();
|
||||||
|
vp.ucs_per_viewport = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2344,16 +2344,20 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::SetViewcubeUcs(name) => {
|
Message::SetViewcubeUcs(name) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let mut changed = false;
|
||||||
if name.is_empty() || name == "WCS" {
|
if name.is_empty() || name == "WCS" {
|
||||||
self.tabs[i].active_ucs = None;
|
self.tabs[i].active_ucs = None;
|
||||||
self.command_line.push_output("UCS: World");
|
self.command_line.push_output("UCS: World");
|
||||||
|
changed = true;
|
||||||
} else if let Some(named) = self.tabs[i].scene.document.ucss.get(&name).cloned() {
|
} else if let Some(named) = self.tabs[i].scene.document.ucss.get(&name).cloned() {
|
||||||
self.tabs[i].active_ucs = Some(named);
|
self.tabs[i].active_ucs = Some(named);
|
||||||
self.command_line.push_output(&format!("UCS: {}", name));
|
self.command_line.push_output(&format!("UCS: {}", name));
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
self.tabs[i].sync_ucs_to_scene();
|
if changed {
|
||||||
|
self.commit_active_ucs_change(i, "UCS");
|
||||||
self.tabs[i].scene.camera_generation += 1;
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
self.tabs[i].dirty = true;
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1752,10 +1752,81 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].sync_ucs_to_scene();
|
self.tabs[i].sync_ucs_to_scene();
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.tabs[i].scene.camera_generation += 1;
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Commit the live UCS to its owning space. Model/viewport panes persist
|
||||||
|
/// their normal UCS fields. A BEDIT pane has no persistent UCS of its own:
|
||||||
|
/// its transient origin and axes are baked into the block definition, then
|
||||||
|
/// the editor returns to canonical local coordinates.
|
||||||
|
pub(in crate::app) fn commit_active_ucs_change(&mut self, i: usize, label: &'static str) {
|
||||||
|
if let Some((session_index, block_record)) = self.tabs[i]
|
||||||
|
.active_block_edit
|
||||||
|
.and_then(|index| {
|
||||||
|
self.tabs[i]
|
||||||
|
.block_edits
|
||||||
|
.get(index)
|
||||||
|
.map(|session| (index, session.br_handle))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
let frame = self.tabs[i].ucs_xform();
|
||||||
|
if !frame.is_identity() {
|
||||||
|
self.push_undo_snapshot(i, label);
|
||||||
|
let local_from_old = frame.to_ucs_transform();
|
||||||
|
let changed = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.reframe_block_definition(block_record, &local_from_old);
|
||||||
|
if changed > 0 {
|
||||||
|
// Carry the editor camera through the same rigid transform
|
||||||
|
// so changing the block coordinate frame does not make its
|
||||||
|
// contents jump on screen.
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.camera
|
||||||
|
.borrow_mut()
|
||||||
|
.apply_rigid_transform(&local_from_old);
|
||||||
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks do not own a saved UCS. The chosen frame has now become
|
||||||
|
// their identity coordinates.
|
||||||
|
self.tabs[i].active_ucs = None;
|
||||||
|
let editor_camera = self.tabs[i].scene.camera.borrow().clone();
|
||||||
|
if let Some(session) = self.tabs[i].block_edits.get_mut(session_index) {
|
||||||
|
session.editor_ucs = None;
|
||||||
|
session.editor_camera = editor_camera;
|
||||||
|
}
|
||||||
|
self.tabs[i].sync_ucs_to_scene();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let persisted = if let Some(handle) = self.tabs[i].scene.active_viewport {
|
||||||
|
self.tabs[i].ucs_from_viewport(handle)
|
||||||
|
} else if self.tabs[i].scene.current_layout == "Model" {
|
||||||
|
self.tabs[i].model_ucs_from_header()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let same_basis = |a: Option<&acadrust::tables::Ucs>,
|
||||||
|
b: Option<&acadrust::tables::Ucs>| {
|
||||||
|
match (a, b) {
|
||||||
|
(None, None) => true,
|
||||||
|
(Some(a), Some(b)) => {
|
||||||
|
a.origin == b.origin && a.x_axis == b.x_axis && a.y_axis == b.y_axis
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !same_basis(self.tabs[i].active_ucs.as_ref(), persisted.as_ref()) {
|
||||||
|
self.push_undo_snapshot(i, label);
|
||||||
|
self.tabs[i].persist_active_ucs();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
self.tabs[i].sync_ucs_to_scene();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Per-pane Model viewport (pane_grid) ───────────────────────────────
|
// ── Per-pane Model viewport (pane_grid) ───────────────────────────────
|
||||||
|
|
||||||
/// Focus Model pane `idx` (cursor entered it): swap in its camera, sync the
|
/// Focus Model pane `idx` (cursor entered it): swap in its camera, sync the
|
||||||
|
|
@ -2033,9 +2104,10 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit a UCS icon grip drag: persist the new UCS so it
|
// Commit a UCS icon grip drag: persist the new UCS so it
|
||||||
// round-trips, and clear the lingering press state.
|
// round-trips, and clear the lingering press state. In BEDIT this
|
||||||
|
// rebases only the block; elsewhere it writes the model/viewport UCS.
|
||||||
if self.ucs_grip_drag.take().is_some() {
|
if self.ucs_grip_drag.take().is_some() {
|
||||||
self.tabs[i].persist_active_ucs();
|
self.commit_active_ucs_change(i, "UCS");
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.snapper.from_point = None;
|
self.snapper.from_point = None;
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
|
|
@ -3819,9 +3891,11 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
let cancel_task = self.cancel_active_command_for_space_change();
|
let cancel_task = self.cancel_active_command_for_space_change();
|
||||||
let current_camera = self.tabs[i].scene.camera.borrow().clone();
|
let current_camera = self.tabs[i].scene.camera.borrow().clone();
|
||||||
|
let current_ucs = self.tabs[i].active_ucs.clone();
|
||||||
if let Some(active_index) = self.tabs[i].active_block_edit {
|
if let Some(active_index) = self.tabs[i].active_block_edit {
|
||||||
if let Some(session) = self.tabs[i].block_edits.get_mut(active_index) {
|
if let Some(session) = self.tabs[i].block_edits.get_mut(active_index) {
|
||||||
session.editor_camera = current_camera;
|
session.editor_camera = current_camera;
|
||||||
|
session.editor_ucs = current_ucs;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].scene.sync_camera_to_document();
|
self.tabs[i].scene.sync_camera_to_document();
|
||||||
|
|
@ -3902,9 +3976,11 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
if let Some(active_index) = self.tabs[i].active_block_edit.take() {
|
if let Some(active_index) = self.tabs[i].active_block_edit.take() {
|
||||||
let camera = self.tabs[i].scene.camera.borrow().clone();
|
let camera = self.tabs[i].scene.camera.borrow().clone();
|
||||||
|
let editor_ucs = self.tabs[i].active_ucs.clone();
|
||||||
let (return_layout, return_camera) = {
|
let (return_layout, return_camera) = {
|
||||||
let session = &mut self.tabs[i].block_edits[active_index];
|
let session = &mut self.tabs[i].block_edits[active_index];
|
||||||
session.editor_camera = camera;
|
session.editor_camera = camera;
|
||||||
|
session.editor_ucs = editor_ucs;
|
||||||
(session.return_layout.clone(), session.return_camera.clone())
|
(session.return_layout.clone(), session.return_camera.clone())
|
||||||
};
|
};
|
||||||
let return_layout = if self.tabs[i].scene.layout_names().contains(&return_layout) {
|
let return_layout = if self.tabs[i].scene.layout_names().contains(&return_layout) {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ pub enum EntityTransform {
|
||||||
Scale { center: DVec3, factor: f64 },
|
Scale { center: DVec3, factor: f64 },
|
||||||
/// Mirror across the line through `p1`→`p2` in the world XY plane.
|
/// Mirror across the line through `p1`→`p2` in the world XY plane.
|
||||||
Mirror { p1: DVec3, p2: DVec3 },
|
Mirror { p1: DVec3, p2: DVec3 },
|
||||||
|
/// General affine transform. Used when a complete UCS basis must be baked
|
||||||
|
/// into block-local geometry instead of stored as drawing UCS state.
|
||||||
|
Affine(acadrust::types::Transform),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tangent object ─────────────────────────────────────────────────────────
|
// ── Tangent object ─────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -636,6 +636,47 @@ fn apply_transform(dim: &mut Dimension, t: &EntityTransform) {
|
||||||
EntityTransform::Mirror { p1, p2 } => {
|
EntityTransform::Mirror { p1, p2 } => {
|
||||||
transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2))
|
transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2))
|
||||||
}
|
}
|
||||||
|
EntityTransform::Affine(transform) => {
|
||||||
|
let old_normal = dim.base().normal;
|
||||||
|
let text_rotation =
|
||||||
|
transformed_dimension_angle(old_normal, dim.base().text_rotation, transform);
|
||||||
|
let horizontal_direction = transformed_dimension_angle(
|
||||||
|
old_normal,
|
||||||
|
dim.base().horizontal_direction,
|
||||||
|
transform,
|
||||||
|
);
|
||||||
|
let insertion_rotation = transformed_dimension_angle(
|
||||||
|
old_normal,
|
||||||
|
dim.base().insertion_rotation,
|
||||||
|
transform,
|
||||||
|
);
|
||||||
|
let linear_angles = match dim {
|
||||||
|
Dimension::Linear(value) => Some((
|
||||||
|
transformed_dimension_angle(old_normal, value.rotation, transform),
|
||||||
|
transformed_dimension_angle(old_normal, value.ext_line_rotation, transform),
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
transform_dimension_points(dim, |point| {
|
||||||
|
*point = transform.apply(*point);
|
||||||
|
});
|
||||||
|
let transformed_normal = transform.apply_rotation(old_normal);
|
||||||
|
let base = dim.base_mut();
|
||||||
|
base.normal = if transformed_normal.length() > 1e-12 {
|
||||||
|
transformed_normal.normalize()
|
||||||
|
} else {
|
||||||
|
old_normal
|
||||||
|
};
|
||||||
|
base.text_rotation = text_rotation;
|
||||||
|
base.horizontal_direction = horizontal_direction;
|
||||||
|
base.insertion_rotation = insertion_rotation;
|
||||||
|
if let Some((rotation, ext_line_rotation)) = linear_angles {
|
||||||
|
if let Dimension::Linear(value) = dim {
|
||||||
|
value.rotation = rotation;
|
||||||
|
value.ext_line_rotation = ext_line_rotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dim.base_mut().actual_measurement = dim.measurement();
|
dim.base_mut().actual_measurement = dim.measurement();
|
||||||
}
|
}
|
||||||
|
|
@ -644,6 +685,7 @@ fn transform_dimension_points<F>(dim: &mut Dimension, mut f: F)
|
||||||
where
|
where
|
||||||
F: FnMut(&mut acadrust::types::Vector3),
|
F: FnMut(&mut acadrust::types::Vector3),
|
||||||
{
|
{
|
||||||
|
f(&mut dim.base_mut().definition_point);
|
||||||
f(&mut dim.base_mut().text_middle_point);
|
f(&mut dim.base_mut().text_middle_point);
|
||||||
f(&mut dim.base_mut().insertion_point);
|
f(&mut dim.base_mut().insertion_point);
|
||||||
match dim {
|
match dim {
|
||||||
|
|
@ -702,6 +744,32 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn transformed_dimension_angle(
|
||||||
|
normal: acadrust::types::Vector3,
|
||||||
|
angle: f64,
|
||||||
|
transform: &acadrust::types::Transform,
|
||||||
|
) -> f64 {
|
||||||
|
let ((xx, xy, xz), (yx, yy, yz)) =
|
||||||
|
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||||
|
let direction = acadrust::types::Vector3::new(
|
||||||
|
xx * angle.cos() + yx * angle.sin(),
|
||||||
|
xy * angle.cos() + yy * angle.sin(),
|
||||||
|
xz * angle.cos() + yz * angle.sin(),
|
||||||
|
);
|
||||||
|
let transformed_normal = transform.apply_rotation(normal).normalize();
|
||||||
|
let transformed_direction = transform.apply_rotation(direction).normalize();
|
||||||
|
let ((nxx, nxy, nxz), (nyx, nyy, nyz)) = crate::scene::view::transform::ocs_axes((
|
||||||
|
transformed_normal.x,
|
||||||
|
transformed_normal.y,
|
||||||
|
transformed_normal.z,
|
||||||
|
));
|
||||||
|
let new_x = acadrust::types::Vector3::new(nxx, nxy, nxz);
|
||||||
|
let new_y = acadrust::types::Vector3::new(nyx, nyy, nyz);
|
||||||
|
transformed_direction
|
||||||
|
.dot(&new_y)
|
||||||
|
.atan2(transformed_direction.dot(&new_x))
|
||||||
|
}
|
||||||
|
|
||||||
fn rotate_point(p: &mut acadrust::types::Vector3, center: DVec3, angle_rad: f64) {
|
fn rotate_point(p: &mut acadrust::types::Vector3, center: DVec3, angle_rad: f64) {
|
||||||
let dx = p.x - center.x;
|
let dx = p.x - center.x;
|
||||||
let dy = p.y - center.y;
|
let dy = p.y - center.y;
|
||||||
|
|
|
||||||
|
|
@ -1383,6 +1383,7 @@ fn entity_transform(transform: &EntityTransform) -> Transform {
|
||||||
EntityTransform::Mirror { p1, p2 } => {
|
EntityTransform::Mirror { p1, p2 } => {
|
||||||
crate::scene::view::transform::reflection_about_xy_line(*p1, *p2)
|
crate::scene::view::transform::reflection_about_xy_line(*p1, *p2)
|
||||||
}
|
}
|
||||||
|
EntityTransform::Affine(transform) => *transform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,9 @@ fn apply_transform(line: &mut Line, t: &EntityTransform) {
|
||||||
EntityTransform::Mirror { p1, p2 } => {
|
EntityTransform::Mirror { p1, p2 } => {
|
||||||
crate::scene::view::transform::mirror_xy_line(line, *p1, *p2);
|
crate::scene::view::transform::mirror_xy_line(line, *p1, *p2);
|
||||||
}
|
}
|
||||||
|
EntityTransform::Affine(transform) => {
|
||||||
|
acadrust::Entity::apply_transform(line, transform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,9 @@ fn apply_transform(ole: &mut Ole2Frame, t: &EntityTransform) {
|
||||||
ole.lower_right_corner.x = scale(ole.lower_right_corner.x, center.x as f64);
|
ole.lower_right_corner.x = scale(ole.lower_right_corner.x, center.x as f64);
|
||||||
ole.lower_right_corner.y = scale(ole.lower_right_corner.y, center.y as f64);
|
ole.lower_right_corner.y = scale(ole.lower_right_corner.y, center.y as f64);
|
||||||
}
|
}
|
||||||
|
EntityTransform::Affine(transform) => {
|
||||||
|
acadrust::Entity::apply_transform(ole, transform);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -419,6 +419,9 @@ impl Transformable for Underlay {
|
||||||
self.insertion_point.y = by + dx * sin_a + dy * cos_a;
|
self.insertion_point.y = by + dx * sin_a + dy * cos_a;
|
||||||
self.rotation += a;
|
self.rotation += a;
|
||||||
}
|
}
|
||||||
|
EntityTransform::Affine(transform) => {
|
||||||
|
acadrust::Entity::apply_transform(self, transform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@ pub(crate) fn load_file_with_progress(
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.read()
|
.read()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
normalize_block_origins(&mut doc);
|
||||||
fix_viewport_status_flags(&mut doc);
|
fix_viewport_status_flags(&mut doc);
|
||||||
fix_current_style_names(&mut doc);
|
fix_current_style_names(&mut doc);
|
||||||
resolve_raster_image_paths(&mut doc, path.parent());
|
resolve_raster_image_paths(&mut doc, path.parent());
|
||||||
|
|
@ -382,6 +383,7 @@ pub(crate) fn load_file_with_progress(
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.read()
|
.read()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
normalize_block_origins(&mut doc);
|
||||||
fix_dxf_dimension_rotations(&mut doc);
|
fix_dxf_dimension_rotations(&mut doc);
|
||||||
fix_dxf_layout_plot_settings(&mut doc);
|
fix_dxf_layout_plot_settings(&mut doc);
|
||||||
fix_viewport_status_flags(&mut doc);
|
fix_viewport_status_flags(&mut doc);
|
||||||
|
|
@ -394,6 +396,62 @@ pub(crate) fn load_file_with_progress(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Canonicalise legacy block-table origins to the post-R10 representation:
|
||||||
|
/// block contents are local around zero and INSERT carries placement.
|
||||||
|
///
|
||||||
|
/// DWG exposes the legacy point on `BlockRecord::base_point`; DXF exposes it
|
||||||
|
/// on the structural BLOCK entity. Rendering, picking, exploding and nested
|
||||||
|
/// insertion can then share the ordinary zero-origin transform without each
|
||||||
|
/// path having to reinterpret this compatibility field.
|
||||||
|
fn normalize_block_origins(doc: &mut CadDocument) {
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
use acadrust::EntityType;
|
||||||
|
|
||||||
|
let blocks: Vec<_> = doc
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.filter_map(|record| {
|
||||||
|
let marker_point = match doc.get_entity(record.block_entity_handle) {
|
||||||
|
Some(EntityType::Block(block)) => block.base_point,
|
||||||
|
_ => Vector3::ZERO,
|
||||||
|
};
|
||||||
|
let base_point = if marker_point.length() > 1e-12 {
|
||||||
|
marker_point
|
||||||
|
} else {
|
||||||
|
record.base_point
|
||||||
|
};
|
||||||
|
(base_point.length() > 1e-12).then(|| {
|
||||||
|
(
|
||||||
|
record.name.clone(),
|
||||||
|
record.block_entity_handle,
|
||||||
|
record.entity_handles.clone(),
|
||||||
|
base_point,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (name, marker_handle, handles, base_point) in blocks {
|
||||||
|
let offset = base_point * -1.0;
|
||||||
|
for handle in handles {
|
||||||
|
let Some(entity) = doc.get_entity_mut(handle) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match entity {
|
||||||
|
EntityType::Block(block) => block.base_point = Vector3::ZERO,
|
||||||
|
EntityType::BlockEnd(_) => {}
|
||||||
|
_ => entity.translate(offset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(EntityType::Block(block)) = doc.get_entity_mut(marker_handle) {
|
||||||
|
block.base_point = Vector3::ZERO;
|
||||||
|
}
|
||||||
|
if let Some(record) = doc.block_records.get_mut(&name) {
|
||||||
|
record.base_point = Vector3::ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A RasterImage entity stores its file path on the linked ImageDefinition,
|
/// A RasterImage entity stores its file path on the linked ImageDefinition,
|
||||||
/// often as the original author's absolute path (e.g. a Windows / VMware share)
|
/// often as the original author's absolute path (e.g. a Windows / VMware share)
|
||||||
/// that doesn't exist on this machine. Resolve each image to a usable path:
|
/// that doesn't exist on this machine. Resolve each image to a usable path:
|
||||||
|
|
|
||||||
|
|
@ -55,15 +55,27 @@ pub struct BlockEditSession {
|
||||||
/// Parent block tab to return to for a nested BEDIT session. `None` returns
|
/// Parent block tab to return to for a nested BEDIT session. `None` returns
|
||||||
/// to `return_layout`.
|
/// to `return_layout`.
|
||||||
pub return_block: Option<String>,
|
pub return_block: Option<String>,
|
||||||
/// The block's block-local entities captured on entry, so Discard can
|
/// The complete block definition captured on entry, so Discard restores
|
||||||
/// restore the definition (structural Block/BlockEnd/AttDef excluded).
|
/// structural markers and content with their original handles.
|
||||||
pub snapshot: Vec<EntityType>,
|
pub snapshot: Vec<EntityType>,
|
||||||
|
/// Baked anonymous dimension-block entities transformed alongside
|
||||||
|
/// dimensions in this definition. They remain in place, so Discard can
|
||||||
|
/// restore their shared before-images directly.
|
||||||
|
pub dependent_snapshot: Vec<std::sync::Arc<EntityType>>,
|
||||||
|
/// Inline ATTRIB values/positions on every existing INSERT of this block.
|
||||||
|
/// Rebasing the origin moves them with the definition; Discard restores
|
||||||
|
/// their exact on-entry state.
|
||||||
|
pub reference_attributes:
|
||||||
|
Vec<(Handle, Vec<acadrust::entities::AttributeEntity>)>,
|
||||||
/// Camera state of the space that was active when BEDIT began, restored
|
/// Camera state of the space that was active when BEDIT began, restored
|
||||||
/// on Save/Discard so the view returns exactly where it was (#425).
|
/// on Save/Discard so the view returns exactly where it was (#425).
|
||||||
pub return_camera: crate::scene::view::camera::Camera,
|
pub return_camera: crate::scene::view::camera::Camera,
|
||||||
/// Camera owned by this block tab. Updated whenever another space tab is
|
/// Camera owned by this block tab. Updated whenever another space tab is
|
||||||
/// activated, then restored when the user returns to this block.
|
/// activated, then restored when the user returns to this block.
|
||||||
pub editor_camera: crate::scene::view::camera::Camera,
|
pub editor_camera: crate::scene::view::camera::Camera,
|
||||||
|
/// Transient UCS being manipulated in this block tab. On commit its full
|
||||||
|
/// frame is baked into the definition and this returns to `None`.
|
||||||
|
pub editor_ucs: Option<acadrust::tables::Ucs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BEDIT pick command ─────────────────────────────────────────────────────
|
// ── BEDIT pick command ─────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -607,7 +607,8 @@ impl Scene {
|
||||||
&mut self,
|
&mut self,
|
||||||
handles: &[Handle],
|
handles: &[Handle],
|
||||||
name: &str,
|
name: &str,
|
||||||
base: glam::DVec3,
|
world_to_block: &acadrust::types::Transform,
|
||||||
|
block_to_world: &acadrust::types::Transform,
|
||||||
) -> Result<Handle, String> {
|
) -> Result<Handle, String> {
|
||||||
let name = name.trim();
|
let name = name.trim();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
|
|
@ -656,7 +657,7 @@ impl Scene {
|
||||||
.add_entity(EntityType::BlockEnd(block_end))
|
.add_entity(EntityType::BlockEnd(block_end))
|
||||||
.map_err(|e| e.to_string())?;
|
.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 {
|
for (old_handle, mut entity) in source_entities {
|
||||||
view::dispatch::apply_transform(&mut entity, &local);
|
view::dispatch::apply_transform(&mut entity, &local);
|
||||||
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
||||||
|
|
@ -668,10 +669,8 @@ impl Scene {
|
||||||
self.erase_entities(&[old_handle]);
|
self.erase_entities(&[old_handle]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let insert = DxfInsert::new(
|
let mut insert = DxfInsert::new(name, acadrust::types::Vector3::ZERO);
|
||||||
name,
|
acadrust::Entity::apply_transform(&mut insert, block_to_world);
|
||||||
acadrust::types::Vector3::new(base.x, base.y, base.z),
|
|
||||||
);
|
|
||||||
Ok(self.add_entity(EntityType::Insert(insert)))
|
Ok(self.add_entity(EntityType::Insert(insert)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2466,6 +2466,51 @@ impl Scene {
|
||||||
self.push_geometry_delta(epoch, changes.to_vec(), false);
|
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) {
|
pub fn bump_geometry(&mut self) {
|
||||||
let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||||
self.geometry_epoch = epoch;
|
self.geometry_epoch = epoch;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,44 @@ struct TextOrient {
|
||||||
x_scale: f64,
|
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
|
/// Snapshot the orientation of a text / mtext / shape entity, or `None` for any
|
||||||
/// other kind.
|
/// other kind.
|
||||||
fn capture_text_orient(e: &EntityType) -> Option<TextOrient> {
|
fn capture_text_orient(e: &EntityType) -> Option<TextOrient> {
|
||||||
|
|
@ -229,6 +267,164 @@ impl Scene {
|
||||||
self.bump_entities(&changes);
|
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*
|
/// Give a freshly-cloned entity brand-new handles for every *inline*
|
||||||
/// sub-entity that stores one (INSERT attributes, 3D-polyline vertices).
|
/// sub-entity that stores one (INSERT attributes, 3D-polyline vertices).
|
||||||
/// `document.add_entity` only assigns the top-level handle, so without this
|
/// `document.add_entity` only assigns the top-level handle, so without this
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,55 @@ impl Camera {
|
||||||
Mat4::from_quat(self.rotation.conjugate())
|
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
|
/// The camera's roll — rotation about the view axis, in radians — the
|
||||||
/// inverse of the `roll` argument to [`yaw_pitch_to_quat`]. Recovered by
|
/// 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
|
/// 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::Scale { center, factor } => apply_standard_scale(entity, *center, *factor),
|
||||||
EntityTransform::Mirror { p1, p2 } => mirror(entity, *p1, *p2),
|
EntityTransform::Mirror { p1, p2 } => mirror(entity, *p1, *p2),
|
||||||
|
EntityTransform::Affine(transform) => entity.apply_transform(transform),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue