forked from 1b-agent/opencadstudio-upstream
feat(blocks): persist independent Move and Scale reference grips
Selected drawing references now expose action parameter grips outside Test Block. Point input evaluates immutable definition geometry into an independent anonymous representation and stores per-INSERT values in native AppDataCache XRecords, preserving definition graphs and sibling references. Reuse visibility representation isolation, including shared COPY blocks, and retain cache metadata. Record geometry, INSERT links, new block records and parameter values in one undo step. Support transformed uniform references and Point, Linear, Polar and XY endpoints for the existing Move/Scale actions. Validate synthetic DWG/DXF persistence, re-edit/resave, COPY isolation, XY/Polar cache layouts, metadata, cancellation, transformed grips and undo/redo. 327 block regressions pass (6 existing opt-in checks ignored).
This commit is contained in:
1 parent
ddf348af22
commit
f67f2cf4f5
8 files changed
+804
-26
No files matched your search
+145
-1
@@ -102,6 +102,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert,
|
||||
document,
|
||||
definition,
|
||||
parameter,
|
||||
@@ -110,6 +111,10 @@ impl OpenCADStudio {
|
||||
if self.tabs[i].id != document {
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(insert) = insert {
|
||||
return self
|
||||
.evaluate_reference_parameter(insert, definition, parameter, position);
|
||||
}
|
||||
let Some(test) = self.tabs[i].block_test.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
@@ -193,6 +198,7 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
let Some(test) = tab.block_test.as_ref() else {
|
||||
self.refresh_reference_action_grips(wo);
|
||||
return;
|
||||
};
|
||||
if tab.selected_handle != Some(test.insert) {
|
||||
@@ -237,7 +243,7 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
let Some(test) = tab.block_test.as_ref() else {
|
||||
return false;
|
||||
return self.start_reference_action_grip(handle, id);
|
||||
};
|
||||
if handle != test.insert {
|
||||
return false;
|
||||
@@ -252,6 +258,7 @@ impl OpenCADStudio {
|
||||
return false;
|
||||
};
|
||||
let command = crate::modules::block_editor::action::TestParameterCommand {
|
||||
insert: None,
|
||||
document: tab.id,
|
||||
definition: test.definition,
|
||||
parameter: p.handle,
|
||||
@@ -263,3 +270,140 @@ impl OpenCADStudio {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
fn refresh_reference_action_grips(&mut self, wo: [f64; 3]) {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
if tab.active_block_edit_session().is_some() || tab.scene.selected.len() != 1 {
|
||||
return;
|
||||
}
|
||||
let Some(handle) = tab.selected_handle else {
|
||||
return;
|
||||
};
|
||||
let Some(def) = actions::instance::definition(&tab.scene.document, handle) else {
|
||||
return;
|
||||
};
|
||||
let Some(EntityType::Insert(reference)) = tab.scene.document.get_entity(handle) else {
|
||||
return;
|
||||
};
|
||||
let transform =
|
||||
crate::scene::render_graph::insert_transform(&tab.scene.document, reference);
|
||||
let all = actions::actions(&tab.scene.document, def);
|
||||
let mut grips = vec![];
|
||||
for (index, p) in actions::parameters(&tab.scene.document, def, Kind::Move)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if !all.iter().any(|(_, _, _, id)| *id == p.id) {
|
||||
continue;
|
||||
}
|
||||
let Ok(local) = actions::instance::current(&tab.scene.document, handle, &p) else {
|
||||
continue;
|
||||
};
|
||||
let world =
|
||||
actions::v(transform.apply(actions::native(local))) - glam::DVec3::from_array(wo);
|
||||
grips.push(crate::scene::model::object::GripDef {
|
||||
id: TEST_GRIP_START + index,
|
||||
world,
|
||||
is_midpoint: false,
|
||||
shape: crate::scene::model::object::GripShape::Triangle,
|
||||
dir: None,
|
||||
axis: None,
|
||||
});
|
||||
}
|
||||
for grip in grips {
|
||||
self.tabs[i].selected_grips.push(grip);
|
||||
self.tabs[i].selected_grip_handles.push(handle);
|
||||
}
|
||||
}
|
||||
fn start_reference_action_grip(&mut self, handle: Handle, id: usize) -> bool {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
if tab.active_block_edit_session().is_some() || tab.selected_handle != Some(handle) {
|
||||
return false;
|
||||
}
|
||||
let Some(def) = actions::instance::definition(&tab.scene.document, handle) else {
|
||||
return false;
|
||||
};
|
||||
if tab.scene.is_layer_locked(handle) {
|
||||
return true;
|
||||
}
|
||||
let Some(p) = actions::parameters(&tab.scene.document, def, Kind::Move)
|
||||
.get(id - TEST_GRIP_START)
|
||||
.cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let command = crate::modules::block_editor::action::TestParameterCommand {
|
||||
insert: Some(handle),
|
||||
document: tab.id,
|
||||
definition: def,
|
||||
parameter: p.handle,
|
||||
};
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
true
|
||||
}
|
||||
fn evaluate_reference_parameter(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
definition: Handle,
|
||||
parameter: Handle,
|
||||
position: glam::DVec3,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
if tab.active_block_edit_session().is_some()
|
||||
|| tab.block_test.is_some()
|
||||
|| actions::instance::definition(&tab.scene.document, handle) != Some(definition)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if tab.scene.is_layer_locked(handle) {
|
||||
return Task::none();
|
||||
}
|
||||
let Some(EntityType::Insert(reference)) = tab.scene.document.get_entity(handle) else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(inverse) = crate::scene::inverse_affine(
|
||||
&crate::scene::render_graph::insert_transform(&tab.scene.document, reference),
|
||||
) else {
|
||||
return Task::none();
|
||||
};
|
||||
let local = actions::v(inverse.apply(actions::native(position)));
|
||||
match actions::instance::evaluate(&tab.scene.document, handle, parameter, local) {
|
||||
Ok(staged) => {
|
||||
if let Some(mut staged) = staged {
|
||||
self.push_undo_snapshot(i, "Block reference parameter");
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
let changes = staged
|
||||
.entities()
|
||||
.filter(|e| doc.get_entity(e.common().handle) != Some(*e))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
// Structure swapping retains the live store and its transaction recorder.
|
||||
doc.swap_structure(staged.snapshot_structure());
|
||||
for entity in changes {
|
||||
let h = entity.common().handle;
|
||||
if let Some(current) = doc.get_entity_mut(h) {
|
||||
*current = entity
|
||||
} else {
|
||||
doc.restore_entity_arc(std::sync::Arc::new(entity));
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.finish_pending_history(i);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.refresh_properties();
|
||||
}
|
||||
Err(e) => self.command_line.push_error(crate::t!(e).as_ref()),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,125 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_drawing_grips_history_cancel_transform_and_reopen() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].clone();
|
||||
let targets = members(&app);
|
||||
actions::create(
|
||||
&mut app.tabs[i].scene.document,
|
||||
def,
|
||||
Kind::Move,
|
||||
p.handle,
|
||||
&targets,
|
||||
glam::DVec3::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
let sibling = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Insert(Insert::new(
|
||||
"EDIT_ME",
|
||||
Vector3::new(40., 0., 0.),
|
||||
)))
|
||||
.unwrap();
|
||||
let EntityType::Insert(ins) = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(reference)
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
ins.insert_point = Vector3::new(20., 30., 0.);
|
||||
ins.rotation = std::f64::consts::FRAC_PI_2;
|
||||
ins.set_x_scale(2.);
|
||||
ins.set_y_scale(2.);
|
||||
ins.set_z_scale(2.);
|
||||
let original = app.tabs[i].scene.document.clone();
|
||||
app.tabs[i].scene.selected.insert(reference);
|
||||
app.tabs[i].selected_handle = Some(reference);
|
||||
app.refresh_properties();
|
||||
let grip = app.tabs[i]
|
||||
.selected_grips
|
||||
.iter()
|
||||
.find(|g| super::block_actions::is_test_grip(g.id))
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert!(grip.world.distance(glam::DVec3::new(20., 50., 0.)) < 1e-8);
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
assert!(app.start_test_action_grip(reference, grip.id));
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(app.tabs[i].scene.document, original);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
// Escape also clears the ordinary drawing selection.
|
||||
app.tabs[i].scene.selected.insert(reference);
|
||||
app.refresh_properties();
|
||||
assert!(app.start_test_action_grip(reference, grip.id));
|
||||
feed(&mut app, "20,70");
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
let changed = app.tabs[i].scene.document.clone();
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||
let EntityType::Insert(ins) = changed.get_entity(reference).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_ne!(ins.block_name, "EDIT_ME");
|
||||
let member = changed
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.unwrap()
|
||||
.entity_handles[0];
|
||||
let EntityType::Line(line) = changed.get_entity(member).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert!((line.start.x - 10.).abs() < 1e-8);
|
||||
assert_eq!(
|
||||
changed.get_entity(targets[0]),
|
||||
original.get_entity(targets[0])
|
||||
);
|
||||
assert_eq!(changed.get_entity(sibling), original.get_entity(sibling));
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(reference),
|
||||
original.get_entity(reference)
|
||||
);
|
||||
assert!(app.tabs[i].scene.document.get_entity(member).is_none());
|
||||
app.redo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(member),
|
||||
changed.get_entity(member)
|
||||
);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(reference),
|
||||
changed.get_entity(reference)
|
||||
);
|
||||
let bytes = codec::DwgWriter::write_to_vec(&app.tabs[i].scene.document).unwrap();
|
||||
app.tabs[i].scene.document = codec::DwgReader::from_stream(std::io::Cursor::new(bytes))
|
||||
.read()
|
||||
.unwrap();
|
||||
app.refresh_properties();
|
||||
let grip = app.tabs[i]
|
||||
.selected_grips
|
||||
.iter()
|
||||
.find(|g| super::block_actions::is_test_grip(g.id))
|
||||
.unwrap();
|
||||
assert!(grip.world.distance(glam::DVec3::new(20., 70., 0.)) < 1e-8);
|
||||
let id = grip.id;
|
||||
assert!(app.start_test_action_grip(reference, id));
|
||||
feed(&mut app, "20,50");
|
||||
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity(member).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert!(line.start.x.abs() < 1e-8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoring_edit_basepoint_attribute_relocation_undo_and_cancel() {
|
||||
let (mut app, reference) = fixture();
|
||||
@@ -478,6 +597,7 @@ fn block_actions_command_selection_cancel_undo_and_test_block_isolation() {
|
||||
let shared_before = app.tabs[test].scene.document.clone();
|
||||
let id = app.tabs[test].id;
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert: None,
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
@@ -558,6 +678,7 @@ fn block_actions_test_reference_transform_visibility_and_noop_history() {
|
||||
.apply(actions::native(p.end)),
|
||||
);
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert: None,
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
@@ -596,6 +717,7 @@ fn block_actions_test_reference_transform_visibility_and_noop_history() {
|
||||
);
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert: None,
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
|
||||
@@ -1530,6 +1530,7 @@ pub enum CmdResult {
|
||||
location: DVec3,
|
||||
},
|
||||
EvaluateBlockParameter {
|
||||
insert: Option<Handle>,
|
||||
document: u64,
|
||||
definition: Handle,
|
||||
parameter: Handle,
|
||||
|
||||
@@ -178,19 +178,25 @@ impl CadCommand for ActionCommand {
|
||||
}
|
||||
|
||||
pub(crate) struct TestParameterCommand {
|
||||
pub insert: Option<Handle>,
|
||||
pub document: u64,
|
||||
pub definition: Handle,
|
||||
pub parameter: Handle,
|
||||
}
|
||||
impl CadCommand for TestParameterCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"BTESTBLOCK"
|
||||
if self.insert.is_some() {
|
||||
"BLOCKPARAMETERGRIP"
|
||||
} else {
|
||||
"BTESTBLOCK"
|
||||
}
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
crate::t!("Test Block Specify the new parameter endpoint (Escape cancels):").into_owned()
|
||||
crate::t!("Specify the new block parameter endpoint (Escape cancels):").into_owned()
|
||||
}
|
||||
fn on_point(&mut self, position: DVec3) -> CmdResult {
|
||||
CmdResult::EvaluateBlockParameter {
|
||||
insert: self.insert,
|
||||
document: self.document,
|
||||
definition: self.definition,
|
||||
parameter: self.parameter,
|
||||
|
||||
+12
-11
@@ -1,4 +1,4 @@
|
||||
//! Native Move/Scale authoring and deterministic evaluation in Test Block.
|
||||
//! Native Move/Scale authoring and deterministic dynamic-block evaluation.
|
||||
use super::block_authoring::{self as authoring, *};
|
||||
use codec::objects::{
|
||||
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockBasePointAction,
|
||||
@@ -7,6 +7,7 @@ use codec::objects::{
|
||||
use codec::types::{Transform, Vector3};
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
pub(crate) mod instance;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -379,15 +380,15 @@ pub(crate) fn evaluate(
|
||||
position: DVec3,
|
||||
) -> Result<bool, &'static str> {
|
||||
let graph = authoring::graph(baseline, definition)?
|
||||
.ok_or("The test authoring graph is unavailable.")?;
|
||||
.ok_or("The block authoring graph is unavailable.")?;
|
||||
if baseline.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if o.owner==graph && matches!(&o.data,
|
||||
DynamicBlockData::StretchAction(_)|DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::RotateAction(_)|DynamicBlockData::FlipAction(_)|DynamicBlockData::ArrayAction(_)))) {
|
||||
return Err("This block contains action types not supported in Test Block yet.");
|
||||
return Err("This block contains action types not supported yet.");
|
||||
}
|
||||
let p = parameters(baseline, definition, Kind::Move)
|
||||
.into_iter()
|
||||
.find(|p| p.handle == parameter)
|
||||
.ok_or("The test parameter is unavailable.")?;
|
||||
.ok_or("The block parameter is unavailable.")?;
|
||||
if !position.is_finite() {
|
||||
return Err("Specify a finite parameter position.");
|
||||
}
|
||||
@@ -408,16 +409,16 @@ pub(crate) fn evaluate(
|
||||
let ObjectType::DynamicBlock(o) = staged
|
||||
.objects
|
||||
.get_mut(&p.grip)
|
||||
.ok_or("The test grip is unavailable.")?
|
||||
.ok_or("The parameter grip is unavailable.")?
|
||||
else {
|
||||
return Err("The test grip is unavailable.");
|
||||
return Err("The parameter grip is unavailable.");
|
||||
};
|
||||
match &mut o.data {
|
||||
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => {
|
||||
g.location = native(position)
|
||||
}
|
||||
DynamicBlockData::LinearGrip(g) => g.grip.location = native(position),
|
||||
_ => return Err("The test grip is unavailable."),
|
||||
_ => return Err("The parameter grip is unavailable."),
|
||||
}
|
||||
let all = actions(baseline, definition);
|
||||
let mut affected = std::collections::BTreeSet::new();
|
||||
@@ -445,7 +446,7 @@ pub(crate) fn evaluate(
|
||||
.find(|p| p.id == *id)
|
||||
.ok_or("An action has an unsupported parameter binding.")?;
|
||||
if !test_parameter_supported(baseline, q.handle) {
|
||||
return Err("This parameter uses value constraints or chaining not supported in Test Block yet.");
|
||||
return Err("This parameter uses value constraints or chaining not supported yet.");
|
||||
}
|
||||
let end = current(&staged, &q);
|
||||
let transform = match kind {
|
||||
@@ -464,7 +465,7 @@ pub(crate) fn evaluate(
|
||||
// Imported actions with custom multipliers/offsets/chaining must
|
||||
// not silently behave like the simple actions authored here.
|
||||
if !a.action_ids.is_empty() {
|
||||
return Err("Chained actions are not supported in Test Block yet.");
|
||||
return Err("Chained actions are not supported yet.");
|
||||
}
|
||||
let ObjectType::DynamicBlock(o) = &baseline.objects[action_handle] else {
|
||||
unreachable!()
|
||||
@@ -499,7 +500,7 @@ pub(crate) fn evaluate(
|
||||
]
|
||||
} =>
|
||||
{
|
||||
return Err("This Move action uses options not supported in Test Block yet.")
|
||||
return Err("This Move action uses options not supported yet.")
|
||||
}
|
||||
DynamicBlockData::ScaleAction(s)
|
||||
if !s.action.dependent
|
||||
@@ -515,7 +516,7 @@ pub(crate) fn evaluate(
|
||||
name: name.into(),
|
||||
}) =>
|
||||
{
|
||||
return Err("This Scale action uses options not supported in Test Block yet.")
|
||||
return Err("This Scale action uses options not supported yet.")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
//! Saved INSERT evaluation: definition geometry stays immutable; parameter
|
||||
//! values live in the native per-reference AppDataCache.
|
||||
use super::*;
|
||||
use codec::objects::{Dictionary, XRecord, XRecordEntry, XRecordValue};
|
||||
|
||||
fn entry(doc: &CadDocument, owner: Handle, key: &str) -> Result<Option<Handle>, &'static str> {
|
||||
let Some(ObjectType::Dictionary(d)) = doc.objects.get(&owner) else {
|
||||
return Err("The reference contains an invalid parameter dictionary.");
|
||||
};
|
||||
Ok(d.entries
|
||||
.iter()
|
||||
.find(|(k, _)| k.eq_ignore_ascii_case(key))
|
||||
.map(|(_, h)| *h))
|
||||
}
|
||||
|
||||
fn cache(doc: &CadDocument, insert: Handle, id: i32) -> Result<Option<Handle>, &'static str> {
|
||||
let Some(mut owner) = doc.extension_dictionary_handle(insert) else {
|
||||
return Ok(None);
|
||||
};
|
||||
for key in [
|
||||
"AcDbBlockRepresentation",
|
||||
"AppDataCache",
|
||||
"ACAD_ENHANCEDBLOCKDATA",
|
||||
] {
|
||||
let Some(next) = entry(doc, owner, key)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
owner = next;
|
||||
}
|
||||
entry(doc, owner, &id.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn current(
|
||||
doc: &CadDocument,
|
||||
insert: Handle,
|
||||
p: &Parameter,
|
||||
) -> Result<DVec3, &'static str> {
|
||||
let Some(h) = cache(doc, insert, p.id)? else {
|
||||
return Ok(p.end);
|
||||
};
|
||||
let Some(ObjectType::XRecord(r)) = doc.objects.get(&h) else {
|
||||
return Err("The reference contains an invalid parameter record.");
|
||||
};
|
||||
if !r.entries_complete {
|
||||
return Err("The reference parameter cache is incomplete.");
|
||||
}
|
||||
let points = r
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|e| e.value.as_point3d())
|
||||
.map(|(x, y, z)| DVec3::new(x, y, z))
|
||||
.collect::<Vec<_>>();
|
||||
if p.xy {
|
||||
let (Some(a), Some(b)) = (points.first(), points.get(1)) else {
|
||||
return Err("The XY parameter cache has no finite endpoint.");
|
||||
};
|
||||
if !a.is_finite()
|
||||
|| !b.is_finite()
|
||||
|| (a.x - p.base.x).abs() > 1e-8
|
||||
|| (b.y - p.base.y).abs() > 1e-8
|
||||
|| (a.z - p.base.z).abs() > 1e-8
|
||||
|| (b.z - p.end.z).abs() > 1e-8
|
||||
{
|
||||
return Err("This reference uses an unsupported XY parameter frame.");
|
||||
}
|
||||
return Ok(DVec3::new(b.x, a.y, b.z));
|
||||
}
|
||||
let index = usize::from(!p.point);
|
||||
if !p.point && points.first().is_some_and(|a| a.distance(p.base) > 1e-8) {
|
||||
return Err("This reference uses a relocated parameter base that is not supported yet.");
|
||||
}
|
||||
points
|
||||
.get(index)
|
||||
.copied()
|
||||
.filter(|v| v.is_finite())
|
||||
.ok_or("The reference parameter cache has no finite endpoint.")
|
||||
}
|
||||
|
||||
fn ensure_dictionary(
|
||||
doc: &mut CadDocument,
|
||||
owner: Handle,
|
||||
key: &str,
|
||||
) -> Result<Handle, &'static str> {
|
||||
if let Some(h) = entry(doc, owner, key)? {
|
||||
if matches!(doc.objects.get(&h), Some(ObjectType::Dictionary(_))) {
|
||||
return Ok(h);
|
||||
}
|
||||
return Err("The reference contains an invalid parameter dictionary.");
|
||||
}
|
||||
let h = doc.allocate_handle();
|
||||
let mut d = Dictionary::new();
|
||||
d.handle = h;
|
||||
d.owner = owner;
|
||||
d.reactors.push(owner);
|
||||
doc.objects.insert(h, ObjectType::Dictionary(d));
|
||||
if let Some(ObjectType::Dictionary(d)) = doc.objects.get_mut(&owner) {
|
||||
d.add_entry(key, h);
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
fn save(
|
||||
doc: &mut CadDocument,
|
||||
insert: Handle,
|
||||
p: &Parameter,
|
||||
position: DVec3,
|
||||
) -> Result<(), &'static str> {
|
||||
let mut owner = doc.ensure_extension_dictionary(insert);
|
||||
for key in [
|
||||
"AcDbBlockRepresentation",
|
||||
"AppDataCache",
|
||||
"ACAD_ENHANCEDBLOCKDATA",
|
||||
] {
|
||||
owner = ensure_dictionary(doc, owner, key)?;
|
||||
}
|
||||
let key = p.id.to_string();
|
||||
let h = if let Some(h) = entry(doc, owner, &key)? {
|
||||
h
|
||||
} else {
|
||||
let h = doc.allocate_handle();
|
||||
let mut r = XRecord::named(&key);
|
||||
r.handle = h;
|
||||
r.owner = owner;
|
||||
r.reactors.push(owner);
|
||||
r.cloning_flags = codec::objects::DictionaryCloningFlags::KeepExisting;
|
||||
r.entries = vec![
|
||||
XRecordEntry::int32(1071, 0),
|
||||
XRecordEntry::int32(1071, 0),
|
||||
XRecordEntry::int16(70, 25),
|
||||
XRecordEntry::int16(70, 104),
|
||||
];
|
||||
let points = if p.point {
|
||||
vec![p.end]
|
||||
} else if p.xy {
|
||||
vec![
|
||||
DVec3::new(p.base.x, p.end.y, p.base.z),
|
||||
DVec3::new(p.end.x, p.base.y, p.end.z),
|
||||
-DVec3::Z,
|
||||
]
|
||||
} else if !p.linear {
|
||||
vec![p.base, p.end, -DVec3::Z, p.end]
|
||||
} else {
|
||||
vec![p.base, p.end, -DVec3::Z]
|
||||
};
|
||||
for v in points {
|
||||
r.entries
|
||||
.push(XRecordEntry::new(10, XRecordValue::Point3D(v.x, v.y, v.z)));
|
||||
}
|
||||
doc.objects.insert(h, ObjectType::XRecord(r));
|
||||
if let Some(ObjectType::Dictionary(d)) = doc.objects.get_mut(&owner) {
|
||||
d.add_entry(&key, h);
|
||||
}
|
||||
h
|
||||
};
|
||||
let Some(ObjectType::XRecord(r)) = doc.objects.get_mut(&h) else {
|
||||
return Err("Invalid parameter record.");
|
||||
};
|
||||
if p.xy {
|
||||
let mut points = r
|
||||
.entries
|
||||
.iter_mut()
|
||||
.filter(|e| matches!(e.value, XRecordValue::Point3D(..)));
|
||||
let a = points
|
||||
.next()
|
||||
.ok_or("The XY parameter cache has no base point.")?;
|
||||
a.value = XRecordValue::Point3D(p.base.x, position.y, p.base.z);
|
||||
let b = points
|
||||
.next()
|
||||
.ok_or("The XY parameter cache has no endpoint.")?;
|
||||
b.value = XRecordValue::Point3D(position.x, p.base.y, position.z);
|
||||
} else {
|
||||
let index = usize::from(!p.point);
|
||||
let e = r
|
||||
.entries
|
||||
.iter_mut()
|
||||
.filter(|e| matches!(e.value, XRecordValue::Point3D(..)))
|
||||
.nth(index)
|
||||
.ok_or("The reference parameter cache has no endpoint.")?;
|
||||
e.value = XRecordValue::Point3D(position.x, position.y, position.z);
|
||||
}
|
||||
// Retain unknown metadata and references, but invalidate stale raw bytes.
|
||||
r.raw_data.clear();
|
||||
r.raw_dwg_data = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn definition(doc: &CadDocument, insert: Handle) -> Option<Handle> {
|
||||
let EntityType::Insert(i) = doc.get_entity(insert)? else {
|
||||
return None;
|
||||
};
|
||||
let s = i.x_scale().abs();
|
||||
if s < 1e-12
|
||||
|| (i.y_scale().abs() - s).abs() > 1e-10 * s.max(1.)
|
||||
|| (i.z_scale().abs() - s).abs() > 1e-10 * s.max(1.)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
doc.dynamic_definition_for_insert(insert)
|
||||
}
|
||||
|
||||
/// Stage and validate completely before exposing any changes to the drawing.
|
||||
pub(crate) fn evaluate(
|
||||
doc: &CadDocument,
|
||||
insert: Handle,
|
||||
parameter: Handle,
|
||||
local: DVec3,
|
||||
) -> Result<Option<CadDocument>, &'static str> {
|
||||
let def =
|
||||
definition(doc, insert).ok_or("Select a uniformly scaled dynamic block reference.")?;
|
||||
let parameters = parameters(doc, def, Kind::Move);
|
||||
let selected = parameters
|
||||
.iter()
|
||||
.find(|p| p.handle == parameter)
|
||||
.ok_or("The reference parameter is unavailable.")?;
|
||||
let mut evaluated = doc.clone();
|
||||
for p in ¶meters {
|
||||
let value = current(doc, insert, p)?;
|
||||
let Some(ObjectType::DynamicBlock(o)) = evaluated.objects.get_mut(&p.grip) else {
|
||||
return Err("Missing parameter grip.");
|
||||
};
|
||||
match &mut o.data {
|
||||
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => {
|
||||
g.location = native(value)
|
||||
}
|
||||
DynamicBlockData::LinearGrip(g) => g.grip.location = native(value),
|
||||
_ => return Err("Unsupported parameter grip."),
|
||||
}
|
||||
}
|
||||
if !super::evaluate(&mut evaluated, doc, def, parameter, local)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let value = super::current(&evaluated, selected);
|
||||
let mut scene = crate::scene::Scene::new();
|
||||
scene.document = doc.clone();
|
||||
scene.dynamic_representation(insert);
|
||||
let Some(EntityType::Insert(reference)) = scene.document.get_entity(insert) else {
|
||||
return Err("Missing block reference.");
|
||||
};
|
||||
let name = reference.block_name.clone();
|
||||
if scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&name)
|
||||
.is_none_or(|b| b.handle == def)
|
||||
{
|
||||
return Err("Could not isolate the dynamic block reference.");
|
||||
}
|
||||
// Evaluate only temporary definition members, then copy the affected geometry
|
||||
// into this reference's representation, preserving identity and visibility.
|
||||
let source = doc
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|b| b.handle == def)
|
||||
.ok_or("Missing definition.")?;
|
||||
let target = scene.document.block_records.get(&name).unwrap();
|
||||
if source.entity_handles.len() != target.entity_handles.len() {
|
||||
return Err("The reference topology no longer matches its definition.");
|
||||
}
|
||||
let affected = actions(doc, def)
|
||||
.iter()
|
||||
.flat_map(|(_, a, _, _)| a.dependencies.iter().copied())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
let pairs = source
|
||||
.entity_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(target.entity_handles.iter().copied())
|
||||
.collect::<Vec<_>>();
|
||||
for (from, to) in pairs {
|
||||
if !affected.contains(&from) {
|
||||
continue;
|
||||
}
|
||||
let mut e = evaluated
|
||||
.get_entity(from)
|
||||
.cloned()
|
||||
.ok_or("Missing action target.")?;
|
||||
let target = scene
|
||||
.document
|
||||
.get_entity_mut(to)
|
||||
.ok_or("Missing representation target.")?;
|
||||
if std::mem::discriminant(&e) != std::mem::discriminant(target) {
|
||||
return Err("The reference topology no longer matches its definition.");
|
||||
}
|
||||
*e.common_mut() = target.common().clone();
|
||||
*target = e;
|
||||
}
|
||||
save(&mut scene.document, insert, selected, value)?;
|
||||
Ok(Some(scene.document))
|
||||
}
|
||||
@@ -40,6 +40,198 @@ fn start(doc: &CadDocument, h: Handle) -> Vector3 {
|
||||
};
|
||||
l.start
|
||||
}
|
||||
|
||||
fn instance_line(doc: &CadDocument, insert: Handle) -> Vector3 {
|
||||
let EntityType::Insert(i) = doc.get_entity(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
start(
|
||||
doc,
|
||||
doc.block_records.get(&i.block_name).unwrap().entity_handles[0],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_saved_xy_polar_cache_metadata_and_rejections() {
|
||||
for kind in [distance::Kind::XY, distance::Kind::Polar] {
|
||||
let (mut doc, def, handles) = fixture();
|
||||
let p = distance::create(
|
||||
&mut doc,
|
||||
def,
|
||||
kind,
|
||||
Vector3::new(2., 3., 0.),
|
||||
Vector3::new(12., 13., 0.),
|
||||
Vector3::new(5., 6., 0.),
|
||||
)
|
||||
.unwrap();
|
||||
create(&mut doc, def, Kind::Move, p, &[handles[0]], DVec3::ZERO).unwrap();
|
||||
let a = doc
|
||||
.add_entity(EntityType::Insert(codec::entities::Insert::new(
|
||||
"ACTIONS",
|
||||
Vector3::ZERO,
|
||||
)))
|
||||
.unwrap();
|
||||
let original = doc.clone();
|
||||
let mut doc = instance::evaluate(&doc, a, p, DVec3::new(20., 18., 0.))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(instance_line(&doc, a), Vector3::new(9., 5., 0.));
|
||||
let parameter = parameters(&doc, def, Kind::Move).remove(0);
|
||||
assert_eq!(
|
||||
instance::current(&doc, a, ¶meter).unwrap(),
|
||||
DVec3::new(20., 18., 0.)
|
||||
);
|
||||
let r = doc
|
||||
.objects
|
||||
.values_mut()
|
||||
.find_map(|o| match o {
|
||||
ObjectType::XRecord(r) => Some(r),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
r.entries.push(codec::objects::XRecordEntry::string(
|
||||
1,
|
||||
"retain opaque metadata",
|
||||
));
|
||||
doc = instance::evaluate(&doc, a, p, DVec3::new(22., 21., 0.))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(doc.objects.values().any(|o|matches!(o,ObjectType::XRecord(r) if r.entries.iter().any(|e|e.value.as_string()==Some("retain opaque metadata")))));
|
||||
let reopened = DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
|
||||
.read()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
instance::current(&reopened, a, ¶meter).unwrap(),
|
||||
DVec3::new(22., 21., 0.)
|
||||
);
|
||||
assert_eq!(start(&reopened,handles[0]),start(&original,handles[0]));
|
||||
assert!(instance::evaluate(&doc, a, p, DVec3::splat(f64::NAN)).is_err());
|
||||
let mut invalid = doc.clone();
|
||||
let EntityType::Insert(i) = invalid.get_entity_mut(a).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
i.set_x_scale(2.);
|
||||
assert!(instance::definition(&invalid, a).is_none());
|
||||
assert!(instance::evaluate(&invalid, a, p, DVec3::ONE).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_saved_references_independent_absolute_and_roundtrip() {
|
||||
let (mut doc, def, handles) = fixture();
|
||||
let p = linear(&mut doc, def);
|
||||
let q = point::create(&mut doc, def, Vector3::ZERO, Vector3::UNIT_Y).unwrap();
|
||||
create(&mut doc, def, Kind::Scale, p, &[handles[1]], DVec3::ZERO).unwrap();
|
||||
create(&mut doc, def, Kind::Move, q, &[handles[0]], DVec3::ZERO).unwrap();
|
||||
let a = doc
|
||||
.add_entity(EntityType::Insert(codec::entities::Insert::new(
|
||||
"ACTIONS",
|
||||
Vector3::ZERO,
|
||||
)))
|
||||
.unwrap();
|
||||
let b = doc
|
||||
.add_entity(EntityType::Insert(codec::entities::Insert::new(
|
||||
"ACTIONS",
|
||||
Vector3::new(30., 0., 0.),
|
||||
)))
|
||||
.unwrap();
|
||||
let original = doc.clone();
|
||||
doc = instance::evaluate(&doc, a, q, DVec3::new(3., 4., 0.))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc = instance::evaluate(&doc, a, p, DVec3::new(20., 7., 0.))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(instance_line(&doc, a), Vector3::new(4., 4., 0.));
|
||||
assert_eq!(instance_line(&doc, b), Vector3::UNIT_X);
|
||||
for h in &handles {
|
||||
assert_eq!(doc.get_entity(*h), original.get_entity(*h));
|
||||
}
|
||||
for (h, o) in &original.objects {
|
||||
assert_eq!(doc.objects.get(h), Some(o));
|
||||
}
|
||||
let mut saved = vec![doc.clone()];
|
||||
saved.push(
|
||||
DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
|
||||
.read()
|
||||
.unwrap(),
|
||||
);
|
||||
saved.push(
|
||||
DxfReader::from_reader(Cursor::new(DxfWriter::new(&doc).write_to_vec().unwrap()))
|
||||
.unwrap()
|
||||
.read()
|
||||
.unwrap(),
|
||||
);
|
||||
for doc in saved {
|
||||
let parameter = parameters(&doc, def, Kind::Move)
|
||||
.into_iter()
|
||||
.find(|x| x.handle == p)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
instance::current(&doc, a, ¶meter).unwrap(),
|
||||
DVec3::new(20., 0., 0.)
|
||||
);
|
||||
let props = doc.dynamic_block_properties(def, Some(a)).unwrap();
|
||||
assert!(props
|
||||
.iter()
|
||||
.any(|x| x.parameter_handle == p && x.value == BlockEvalValue::Real(20.)));
|
||||
assert!(instance::evaluate(&doc, a, p, DVec3::new(20., 0., 0.))
|
||||
.unwrap()
|
||||
.is_none());
|
||||
let next = instance::evaluate(&doc, a, q, DVec3::new(-2., 1., 0.))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(instance_line(&next, a), Vector3::new(-1., 1., 0.));
|
||||
let EntityType::Insert(i) = next.get_entity(a).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let EntityType::Circle(c) = next
|
||||
.get_entity(
|
||||
next.block_records
|
||||
.get(&i.block_name)
|
||||
.unwrap()
|
||||
.entity_handles[1],
|
||||
)
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(c.radius, 4.);
|
||||
assert_eq!(instance_line(&next, b), Vector3::UNIT_X);
|
||||
let resaved = DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&next).unwrap()))
|
||||
.read()
|
||||
.unwrap();
|
||||
assert_eq!(instance_line(&resaved, a), Vector3::new(-1., 1., 0.));
|
||||
let point = parameters(&resaved, def, Kind::Move)
|
||||
.into_iter()
|
||||
.find(|x| x.handle == q)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
instance::current(&resaved, a, &point).unwrap(),
|
||||
DVec3::new(-2., 1., 0.)
|
||||
);
|
||||
// A copied reference may share its evaluated block, but must isolate on edit.
|
||||
let mut scene = Scene::new();
|
||||
scene.document = next.clone();
|
||||
let c = scene.copy_entities(
|
||||
&[a],
|
||||
&crate::command::EntityTransform::Translate(DVec3::new(40., 0., 0.)),
|
||||
)[0];
|
||||
let copy = instance::evaluate(&scene.document, c, q, DVec3::new(7., 8., 0.))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(instance_line(©, c), Vector3::new(8., 8., 0.));
|
||||
assert_eq!(instance_line(©, a), Vector3::new(-1., 1., 0.));
|
||||
assert_eq!(
|
||||
instance::current(©, c, ¶meter).unwrap(),
|
||||
DVec3::new(20., 0., 0.)
|
||||
);
|
||||
let reset = instance::evaluate(&next, a, q, DVec3::ZERO)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(instance_line(&reset, a), Vector3::UNIT_X);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn block_actions_native_bindings_roundtrip_and_append_preserves_neighbors() {
|
||||
let (mut doc, def, handles) = fixture();
|
||||
|
||||
@@ -468,6 +468,13 @@ impl Scene {
|
||||
/// again with its current state. Returns the copy's name; a reference
|
||||
/// already showing its own copy, or not dynamic, gets `None`.
|
||||
pub fn visibility_representation(&mut self, insert: Handle) -> Option<String> {
|
||||
let definition = self.document.dynamic_definition_for_insert(insert)?;
|
||||
parameter(&self.document, definition)?;
|
||||
self.dynamic_representation(insert)
|
||||
}
|
||||
|
||||
/// Isolate an evaluated dynamic reference, including copies sharing a representation.
|
||||
pub(crate) fn dynamic_representation(&mut self, insert: Handle) -> Option<String> {
|
||||
let Some(EntityType::Insert(reference)) = self.document.get_entity(insert) else {
|
||||
return None;
|
||||
};
|
||||
@@ -477,7 +484,6 @@ impl Scene {
|
||||
.get(&reference.block_name)?
|
||||
.handle;
|
||||
let definition = self.document.dynamic_definition_for_insert(insert)?;
|
||||
parameter(&self.document, definition)?;
|
||||
let direct = source == definition;
|
||||
let shared = || {
|
||||
self.document.entities().any(|entity| {
|
||||
@@ -525,18 +531,35 @@ impl Scene {
|
||||
// The reference: AcDbBlockRepresentation / AcDbRepData names the
|
||||
// definition it was evaluated from.
|
||||
let holder = self.document.ensure_extension_dictionary(insert);
|
||||
let representation = self.document.allocate_handle();
|
||||
let existing = match self.document.objects.get(&holder) {
|
||||
Some(ObjectType::Dictionary(d)) => d
|
||||
.entries
|
||||
.iter()
|
||||
.find(|(key, _)| key.eq_ignore_ascii_case("AcDbBlockRepresentation"))
|
||||
.map(|(_, handle)| *handle),
|
||||
_ => None,
|
||||
};
|
||||
let representation = existing.unwrap_or_else(|| self.document.allocate_handle());
|
||||
let data = self.document.allocate_handle();
|
||||
let mut dictionary = codec::objects::Dictionary::new();
|
||||
dictionary.handle = representation;
|
||||
dictionary.owner = holder;
|
||||
dictionary.reactors.push(holder);
|
||||
dictionary.add_entry("AcDbRepData", data);
|
||||
self.document
|
||||
.objects
|
||||
.insert(representation, ObjectType::Dictionary(dictionary));
|
||||
if let Some(ObjectType::Dictionary(holder)) = self.document.objects.get_mut(&holder) {
|
||||
holder.add_entry("AcDbBlockRepresentation", representation);
|
||||
if existing.is_none() {
|
||||
let mut dictionary = codec::objects::Dictionary::new();
|
||||
dictionary.handle = representation;
|
||||
dictionary.owner = holder;
|
||||
dictionary.reactors.push(holder);
|
||||
self.document
|
||||
.objects
|
||||
.insert(representation, ObjectType::Dictionary(dictionary));
|
||||
if let Some(ObjectType::Dictionary(holder)) = self.document.objects.get_mut(&holder) {
|
||||
holder.add_entry("AcDbBlockRepresentation", representation);
|
||||
}
|
||||
}
|
||||
if let Some(ObjectType::Dictionary(dictionary)) =
|
||||
self.document.objects.get_mut(&representation)
|
||||
{
|
||||
dictionary
|
||||
.entries
|
||||
.retain(|(key, _)| !key.eq_ignore_ascii_case("AcDbRepData"));
|
||||
dictionary.add_entry("AcDbRepData", data);
|
||||
}
|
||||
let mut link = dynamic(
|
||||
data,
|
||||
|
||||
Reference in new issue
Block a user