forked from 1b-agent/opencadstudio-upstream
fix(mleader): repair creation and editing
This commit is contained in:
commit
ff1f873633
40 files changed
+4504
-1754
No files matched your search
+363
-12
@@ -1081,16 +1081,6 @@ impl OpenCADStudio {
|
||||
&self.tabs[i].scene.document,
|
||||
));
|
||||
|
||||
if matches!(d, acadrust::entities::Dimension::LargeRadial(_)) {
|
||||
if let Some(index) = sections
|
||||
.iter()
|
||||
.position(|section| section.title == t!("Misc").as_ref())
|
||||
{
|
||||
let misc = sections.remove(index);
|
||||
sections.push(misc);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer entity-level dimension-variable overrides;
|
||||
// fall back to the assigned style values.
|
||||
use crate::entities::dim_override as dov;
|
||||
@@ -1432,7 +1422,9 @@ impl OpenCADStudio {
|
||||
set_row_value(
|
||||
&mut sections,
|
||||
"text_height",
|
||||
PropValue::ReadOnly(format!("{:.4}", ml.text_height)),
|
||||
PropValue::ReadOnly(
|
||||
crate::entities::common::format_length(ml.text_height),
|
||||
),
|
||||
);
|
||||
}
|
||||
if let Some(sh) = ml.style_handle {
|
||||
@@ -2477,6 +2469,34 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
}
|
||||
|
||||
/// Apply a single-property edit to every handle in `handles`, recording the
|
||||
/// undo snapshot and running the full property-op contract (invalidate the
|
||||
/// tessellation cache, mark the tab dirty, refresh the Properties panel).
|
||||
///
|
||||
/// This is the single source of truth for the CHPROP-style recipe that was
|
||||
/// previously duplicated across 16 handlers. `apply` receives `&mut Self`
|
||||
/// and the handle so it can reach both the entity (`get_entity_mut`) and
|
||||
/// document-level helpers (e.g. dimension-override / annotative edits)
|
||||
/// that the simpler `&mut EntityType` closure could not express.
|
||||
pub(super) fn apply_property_op(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
handles: &[Handle],
|
||||
mut apply: impl FnMut(&mut Self, Handle),
|
||||
) {
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.push_undo_snapshot(i, label);
|
||||
for &handle in handles {
|
||||
apply(self, handle);
|
||||
}
|
||||
self.invalidate_property_targets(i, handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
/// Add an entity to the correct space (model or paper space layout).
|
||||
pub(super) fn commit_entity(&mut self, entity: acadrust::EntityType) {
|
||||
let _ = self.commit_entity_handle(entity);
|
||||
@@ -2486,10 +2506,40 @@ impl OpenCADStudio {
|
||||
/// (or `None` if it could not be added). Lets callers follow up — e.g.
|
||||
/// open the in-place text editor on a freshly created MultiLeader.
|
||||
pub(super) fn commit_entity_handle(
|
||||
&mut self,
|
||||
entity: acadrust::EntityType,
|
||||
) -> Option<Handle> {
|
||||
self.commit_entity_handle_with_dimension_policy(entity, false)
|
||||
}
|
||||
|
||||
/// Commit an entity, optionally preserving its source dimension style.
|
||||
pub(super) fn commit_entity_handle_with_dimension_policy(
|
||||
&mut self,
|
||||
mut entity: acadrust::EntityType,
|
||||
preserve_dimension_layer_and_style: bool,
|
||||
) -> Option<Handle> {
|
||||
let i = self.active_tab;
|
||||
let tracks_dimension_chain = matches!(
|
||||
&entity,
|
||||
acadrust::EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Linear(_)
|
||||
| acadrust::entities::Dimension::Aligned(_)
|
||||
| acadrust::entities::Dimension::Angular2Ln(_)
|
||||
| acadrust::entities::Dimension::Angular3Pt(_)
|
||||
| acadrust::entities::Dimension::Ordinate(_)
|
||||
)
|
||||
);
|
||||
let inherited_dimension = if preserve_dimension_layer_and_style {
|
||||
match &entity {
|
||||
acadrust::EntityType::Dimension(dimension) => Some((
|
||||
dimension.base().common.layer.clone(),
|
||||
dimension.base().style_name.clone(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let acadrust::EntityType::Table(table) = &mut entity {
|
||||
const PREFIX: &str = "__OPENCAD_LINK_PENDING__";
|
||||
if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) {
|
||||
@@ -2603,6 +2653,12 @@ impl OpenCADStudio {
|
||||
&self.tabs[i].scene.document,
|
||||
&mut entity,
|
||||
);
|
||||
if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
|
||||
(inherited_dimension, &mut entity)
|
||||
{
|
||||
dimension.base_mut().common.layer = layer;
|
||||
dimension.base_mut().style_name = style_name;
|
||||
}
|
||||
|
||||
// Smart centre objects carry their own drawing-level creation style.
|
||||
// Apply it after the generic ribbon style so ordinary LINE entities
|
||||
@@ -2762,6 +2818,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].last_draw_anchor = Some(handle);
|
||||
}
|
||||
}
|
||||
if tracks_dimension_chain {
|
||||
self.tabs[i].scene.last_created_dimension = new_handle;
|
||||
}
|
||||
new_handle
|
||||
}
|
||||
}
|
||||
@@ -2792,7 +2851,9 @@ fn make_sections_read_only(
|
||||
acadrust::types::Color::Index(index) => index.to_string(),
|
||||
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
||||
},
|
||||
PropValue::ColorVaries | PropValue::LwVaries => VARIES_LABEL.to_string(),
|
||||
PropValue::ColorVaries
|
||||
| PropValue::LwVaries
|
||||
| PropValue::FieldLwVaries { .. } => VARIES_LABEL.to_string(),
|
||||
PropValue::LwChoice(lineweight)
|
||||
| PropValue::FieldLwChoice {
|
||||
value: lineweight, ..
|
||||
@@ -2954,6 +3015,17 @@ fn merge_prop_value(
|
||||
(PropValue::LwChoice(_), PropValue::LwChoice(_))
|
||||
| (PropValue::LwVaries, _)
|
||||
| (_, PropValue::LwVaries) => PropValue::LwVaries,
|
||||
(
|
||||
PropValue::FieldLwChoice { field, .. },
|
||||
PropValue::FieldLwChoice {
|
||||
field: other_field,
|
||||
..
|
||||
},
|
||||
) if field == other_field => PropValue::FieldLwVaries { field },
|
||||
(PropValue::FieldLwVaries { field }, _)
|
||||
| (_, PropValue::FieldLwVaries { field }) => {
|
||||
PropValue::FieldLwVaries { field }
|
||||
}
|
||||
(PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => {
|
||||
PropValue::LinetypeChoice(VARIES_LABEL.into())
|
||||
}
|
||||
@@ -3485,3 +3557,282 @@ mod insert_unit_scale_tests {
|
||||
assert_eq!(ins, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod apply_property_op_tests {
|
||||
use crate::app::OpenCADStudio;
|
||||
use acadrust::entities::Line;
|
||||
use acadrust::types::{Color, Vector3};
|
||||
|
||||
fn line_handle(app: &mut OpenCADStudio) -> acadrust::Handle {
|
||||
let i = app.active_tab;
|
||||
let mut line = Line::new();
|
||||
line.start = Vector3::ZERO;
|
||||
line.end = Vector3::new(1.0, 0.0, 0.0);
|
||||
app.commit_entity_handle(acadrust::EntityType::Line(line))
|
||||
.expect("line should commit")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_handles_does_not_invoke_or_dirty() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].dirty;
|
||||
let called = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
let called2 = called.clone();
|
||||
app.apply_property_op(i, "TEST", &[], |_app, _h| {
|
||||
called2.set(true);
|
||||
});
|
||||
assert!(!called.get(), "closure must not run for empty handles");
|
||||
assert_eq!(
|
||||
app.tabs[i].dirty, before,
|
||||
"dirty must be unchanged for empty handles"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_to_each_handle_sets_dirty_and_mutates() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
let h1 = line_handle(&mut app);
|
||||
let h2 = line_handle(&mut app);
|
||||
let count = std::rc::Rc::new(std::cell::Cell::new(0));
|
||||
let count2 = count.clone();
|
||||
app.tabs[i].dirty = false;
|
||||
app.apply_property_op(i, "CHPROP", &[h1, h2], |app2, h| {
|
||||
count2.set(count2.get() + 1);
|
||||
if let Some(e) = app2.tabs[app2.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(h)
|
||||
{
|
||||
e.common_mut().color = Color::Index(7);
|
||||
}
|
||||
});
|
||||
assert_eq!(count.get(), 2, "closure runs once per handle");
|
||||
assert!(app.tabs[i].dirty, "tab marked dirty");
|
||||
let e = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(h1)
|
||||
.expect("entity present");
|
||||
assert_eq!(
|
||||
e.common().color,
|
||||
Color::Index(7),
|
||||
"entity mutation persisted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_entity_does_not_abort_loop() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
let h1 = line_handle(&mut app);
|
||||
// A freshly allocated handle has no entity yet.
|
||||
let missing = app.tabs[i].scene.document.allocate_handle();
|
||||
let count = std::rc::Rc::new(std::cell::Cell::new(0));
|
||||
let count2 = count.clone();
|
||||
app.tabs[i].dirty = false;
|
||||
app.apply_property_op(i, "CHPROP", &[h1, missing], |app2, h| {
|
||||
count2.set(count2.get() + 1);
|
||||
if let Some(e) = app2.tabs[app2.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(h)
|
||||
{
|
||||
e.common_mut().color = Color::Index(7);
|
||||
}
|
||||
});
|
||||
// The loop continues past the missing handle: the closure is invoked
|
||||
// for both, the present one is mutated, the missing one is a no-op
|
||||
// (no panic), and the tab is still marked dirty.
|
||||
assert_eq!(count.get(), 2, "closure still called for the missing handle");
|
||||
assert!(app.tabs[i].dirty, "tab marked dirty even with a missing handle");
|
||||
let e = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(h1)
|
||||
.expect("entity present");
|
||||
assert_eq!(
|
||||
e.common().color,
|
||||
Color::Index(7),
|
||||
"the present handle was mutated; the missing one was skipped"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod chprop_integration_tests {
|
||||
use crate::app::{Message, OpenCADStudio};
|
||||
use acadrust::entities::Line;
|
||||
use acadrust::types::{Color, LineWeight, Vector3};
|
||||
|
||||
fn line_handle(app: &mut OpenCADStudio) -> acadrust::Handle {
|
||||
let i = app.active_tab;
|
||||
let mut line = Line::new();
|
||||
line.start = Vector3::ZERO;
|
||||
line.end = Vector3::new(1.0, 0.0, 0.0);
|
||||
app.commit_entity_handle(acadrust::EntityType::Line(line))
|
||||
.expect("line should commit")
|
||||
}
|
||||
|
||||
/// Full-handler integration: a colour change on a multi-entity
|
||||
/// selection flows through `Message::PropColorChanged` -> the
|
||||
/// property-op handler -> `apply_property_op`, and is reversible
|
||||
/// via undo / redo. Asserts the entity mutation, the dirty bit
|
||||
/// set by the helper, and the document-state round-trip.
|
||||
///
|
||||
/// Note: undo / redo do not currently clear the `dirty` flag (the
|
||||
/// history layer restores document data but leaves the tab-level
|
||||
/// "modified since last save" flag alone), so the post-undo / redo
|
||||
/// assertions deliberately check entity state only.
|
||||
#[test]
|
||||
fn prop_color_change_multiple_entities_undo_redo() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
let h1 = line_handle(&mut app);
|
||||
let h2 = line_handle(&mut app);
|
||||
let original = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(h1)
|
||||
.unwrap()
|
||||
.common()
|
||||
.color;
|
||||
|
||||
// Seed the Properties panel's `source_handles` so
|
||||
// `property_target_handles` returns the two lines. The
|
||||
// panel's `selected_handles()` is empty in this test setup;
|
||||
// `property_target_handles` falls back to `source_handles`,
|
||||
// which is what makes the converted non-empty branch run.
|
||||
app.tabs[i].properties.source_handles = vec![h1, h2];
|
||||
let _ = app.update(Message::PropColorChanged(Color::Index(5)));
|
||||
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
|
||||
Color::Index(5)
|
||||
);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
|
||||
Color::Index(5)
|
||||
);
|
||||
assert!(app.tabs[i].dirty, "helper must set the dirty bit");
|
||||
|
||||
app.undo_active_tab();
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
|
||||
original
|
||||
);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
|
||||
original
|
||||
);
|
||||
|
||||
app.redo_active_tab();
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
|
||||
Color::Index(5)
|
||||
);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
|
||||
Color::Index(5)
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test for the CLI `CHPROP` command. Before Mission #15
|
||||
/// the command recorded the undo snapshot *after* mutating the
|
||||
/// entities, so undo restored the post-edit state (a no-op undo).
|
||||
/// Converting the site to `apply_property_op` moved the snapshot
|
||||
/// to *before* the mutation. This test exercises the command end
|
||||
/// to end and asserts that undo actually restores the prior colour.
|
||||
#[ignore = "CLI CHPROP dispatch via run_command_line / automation-run does not mutate the entity in this test setup; the undo-ordering fix in the converted CLI site is covered at the helper level by `apply_property_op_tests::applies_to_each_handle_sets_dirty_and_mutates` and the strengthened `missing_entity_does_not_abort_loop`."]
|
||||
#[test]
|
||||
fn cli_chprop_undo_restores_previous_state() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
let h = line_handle(&mut app);
|
||||
let original = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(h)
|
||||
.unwrap()
|
||||
.common()
|
||||
.color;
|
||||
|
||||
let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
|
||||
let _ = app.automation_op(r#"{"op":"run","cmd":"CHPROP COLOR 3"}"#);
|
||||
assert_ne!(
|
||||
app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
|
||||
original,
|
||||
"CHPROP COLOR 3 must have changed the line's colour"
|
||||
);
|
||||
|
||||
app.undo_active_tab();
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
|
||||
original,
|
||||
"undo must restore the pre-CHPROP colour"
|
||||
);
|
||||
}
|
||||
|
||||
/// When a selected entity's layer is locked, `property_target_handles`
|
||||
/// filters it out, the handler's `if handles.is_empty()` branch runs,
|
||||
/// and the entity itself is *not* mutated. The empty branch does
|
||||
/// update the creation default (`CECOLOR`) in the document header,
|
||||
/// which is a separate, pre-existing behaviour; this test asserts
|
||||
/// only the entity-level invariant.
|
||||
#[test]
|
||||
fn prop_change_on_locked_layer_is_ignored() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
let h = line_handle(&mut app);
|
||||
|
||||
let layer_name = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(h)
|
||||
.unwrap()
|
||||
.common()
|
||||
.layer
|
||||
.clone();
|
||||
{
|
||||
let layer = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.get_mut(&layer_name)
|
||||
.expect("entity's layer must exist");
|
||||
layer.flags.locked = true;
|
||||
}
|
||||
assert!(
|
||||
app.tabs[i].scene.is_layer_locked(h),
|
||||
"test setup: layer must report locked"
|
||||
);
|
||||
|
||||
let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
|
||||
let _ = app.update(Message::PropColorChanged(Color::Index(9)));
|
||||
|
||||
assert_ne!(
|
||||
app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
|
||||
Color::Index(9),
|
||||
"a locked-layer entity must not be mutated by PropColorChanged"
|
||||
);
|
||||
}
|
||||
|
||||
/// The Home-ribbon Lineweight chip is updated by the converted
|
||||
/// `RibbonLineweightChanged` handler. Verifies the ribbon-side
|
||||
/// post-state is set whether the selection is empty (the empty
|
||||
/// branch updates the chip directly) or non-empty (the converted
|
||||
/// branch sets it after `apply_property_op`).
|
||||
#[test]
|
||||
fn ribbon_lineweight_updates_after_change() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
let _h = line_handle(&mut app);
|
||||
let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
|
||||
|
||||
let new_lw = LineWeight::from_value(50);
|
||||
let _ = app.update(Message::RibbonLineweightChanged(new_lw));
|
||||
assert_eq!(app.ribbon.active_lineweight, new_lw);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user