feat(block-editor): add functional Visibility ribbon controls

Add the current-state selector, Visibility States dialog and mode/show/hide
controls to the contextual ribbon, using shared icons, tooltips, modal UI
and adaptive flyouts. Disable the controls until a Visibility parameter exists.

Stage state creation, rename, deletion, current-state changes and reordering
until OK. Support copied, show-all and hide-all new states in one undo step,
preserve native graph metadata, and restore current-state identity on undo/redo.

Implement nonsaved BVMODE and display-only reveal during BVSHOW selection.
Cancelling selection leaves saved visibility flags and history unchanged.
Keep the existing command-line state workflows available.

Validation: 6 targeted tests; 277 block, 31 app dock, 35 ribbon and 12 history
checks passed (overlapping filters). All 17 local DWG fixtures passed in-memory
DWG/DXF preservation checks with unchanged original checksums.
This commit is contained in:
Karim Jerbi committed 2026-10-15 15:08:21 +01:00
1 parent 308990e672
commit daa26007f9
24 files changed
+1341 -34

No files matched your search

+1
View File
@@ -250,6 +250,7 @@ impl OpenCADStudio {
return None;
}
Some(DeltaSnapshot {
visibility_states: None,
entities,
current_layout_before: session.return_layout.clone(),
current_layout_after: session.return_layout.clone(),
+233
View File
@@ -59,6 +59,239 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
let _ = app.update(Message::CommandSubmit);
}
fn visibility_editor() -> (OpenCADStudio, crate::ui::window::block_visibility::Target) {
let (mut app, reference) = fixture();
begin(&mut app, reference);
let _ = app.run_command_line("BPARAMETER VISIBILITY 0,0");
let target = app
.block_visibility_context()
.expect("placed parameter")
.target;
(app, target)
}
fn visibility_action(app: &mut OpenCADStudio, action: crate::ui::window::block_visibility::Action) {
let _ = app.update(Message::BlockVisibility(action));
}
#[test]
fn visibility_ribbon_manager_cancel_validation_and_single_undo() {
use crate::ui::window::block_visibility::Action as A;
let (mut app, target) = visibility_editor();
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let depth = app.tabs[i].history.undo_stack.len();
visibility_action(&mut app, A::Open(target));
visibility_action(&mut app, A::Name("Alternate".into()));
visibility_action(&mut app, A::New);
assert_eq!(app.tabs[i].scene.document, before, "dialog is staged");
let _ = app.update(Message::CommandEscape);
assert!(app.block_visibility.is_none());
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
assert_eq!(app.tabs[i].scene.document, before);
visibility_action(&mut app, A::Open(target));
visibility_action(&mut app, A::Name("visibilitystate0".into()));
visibility_action(&mut app, A::New);
assert!(app.block_visibility.as_ref().unwrap().error.is_some());
visibility_action(&mut app, A::Delete);
assert_eq!(
app.block_visibility.as_ref().unwrap().edited.states.len(),
1
);
visibility_action(&mut app, A::Name("Alternate".into()));
visibility_action(&mut app, A::New);
visibility_action(&mut app, A::SetCurrent);
visibility_action(&mut app, A::Name("Alternate renamed".into()));
visibility_action(&mut app, A::Rename);
visibility_action(&mut app, A::Move(true));
visibility_action(&mut app, A::Apply);
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
assert_eq!(
app.block_visibility_context().unwrap().current.as_deref(),
Some("Alternate renamed")
);
app.undo_steps(1);
assert_eq!(
app.block_visibility_context().unwrap().names,
["VisibilityState0"]
);
app.redo_steps(1);
assert_eq!(
app.block_visibility_context().unwrap().names,
["Alternate renamed", "VisibilityState0"]
);
assert_eq!(
app.block_visibility_context().unwrap().current.as_deref(),
Some("Alternate renamed")
);
}
#[test]
fn visibility_ribbon_new_modes_state_selection_and_native_roundtrip() {
use crate::ui::window::block_visibility::{Action as A, NewVisibility as V};
let (mut app, target) = visibility_editor();
let i = app.active_tab;
let member = members(&app)[0];
visibility_action(&mut app, A::Open(target));
for (name, mode) in [("Hidden", V::Hide), ("Shown", V::Show), ("Copy", V::Keep)] {
visibility_action(&mut app, A::Name(name.into()));
visibility_action(&mut app, A::NewVisibility(mode));
visibility_action(&mut app, A::New);
}
visibility_action(&mut app, A::Apply);
let parameter =
crate::scene::block_visibility::parameter(&app.tabs[i].scene.document, target.definition)
.unwrap();
assert!(parameter.states[0].visible_blocks.contains(&member));
assert!(parameter.states[1].visible_blocks.is_empty());
assert!(parameter.states[2].visible_blocks.contains(&member));
assert!(parameter.states[3].visible_blocks.contains(&member));
visibility_action(&mut app, A::Current(target, "Hidden".into()));
assert!(
app.tabs[i]
.scene
.document
.get_entity(member)
.unwrap()
.common()
.invisible
);
visibility_action(&mut app, A::Current(target, "Shown".into()));
visibility_action(&mut app, A::Current(target, "Copy".into()));
app.undo_steps(1);
assert_eq!(
app.block_visibility_context().unwrap().current.as_deref(),
Some("Shown"),
"identical states retain their identity in history"
);
app.redo_steps(1);
assert_eq!(
app.block_visibility_context().unwrap().current.as_deref(),
Some("Copy")
);
app.save_block_edit(i);
for ext in ["dwg", "dxf"] {
let document = &app.tabs[i].scene.document;
let bytes = crate::io::save_to_bytes(document, ext, document.version).unwrap();
let loaded = crate::io::load_bytes(&format!("visibility.{ext}"), bytes).unwrap();
let parameter =
crate::scene::block_visibility::parameter(&loaded, target.definition).unwrap();
assert_eq!(
parameter
.states
.iter()
.map(|state| state.name.as_str())
.collect::<Vec<_>>(),
["VisibilityState0", "Hidden", "Shown", "Copy"]
);
assert!(parameter.states[1].visible_blocks.is_empty());
assert!(parameter.states[2].visible_blocks.contains(&member));
}
}
#[test]
fn visibility_ribbon_mode_and_make_visible_cancel_do_not_change_saved_flags() {
use crate::ui::window::block_visibility::Action as A;
let (mut app, target) = visibility_editor();
let i = app.active_tab;
let member = members(&app)[0];
app.tabs[i].scene.select_entity(member, true);
let _ = app.run_command_line("BVHIDE");
app.finish_pending_history(i);
let before = app.tabs[i].scene.document.clone();
let depth = app.tabs[i].history.undo_stack.len();
let dirty = app.tabs[i].dirty;
let wires = |app: &OpenCADStudio| {
app.tabs[i]
.scene
.entity_wires()
.iter()
.filter(|wire| crate::scene::Scene::handle_from_wire_name(&wire.name) == Some(member))
.cloned()
.collect::<Vec<_>>()
};
assert!(wires(&app).is_empty());
visibility_action(&mut app, A::Mode(target));
assert!(
!wires(&app).is_empty(),
"hidden member is rendered for picking"
);
assert_eq!(app.tabs[i].scene.document, before);
visibility_action(&mut app, A::Mode(target));
assert!(wires(&app).is_empty());
let _ = app.run_command_line("BVSHOW");
app.sync_block_visibility_display();
assert!(!wires(&app).is_empty());
let _ = app.update(Message::CommandEscape);
assert!(wires(&app).is_empty());
assert_eq!(app.tabs[i].scene.document, before);
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
assert_eq!(app.tabs[i].dirty, dirty);
let _ = app.run_command_line("BVMODE 1");
assert!(app.block_visibility_mode);
let _ = app.run_command_line("SETVAR BVMODE 2");
assert!(app.block_visibility_mode);
let _ = app.run_command_line("BVMODE 0");
assert!(!app.block_visibility_mode);
assert!(
app.pending_setvar.is_none(),
"inline variable must close its probe prompt"
);
let _ = app.run_command_line("BVSHOW");
app.tabs[i].scene.select_entity(member, true);
let result = app.tabs[i]
.active_cmd
.as_mut()
.unwrap()
.on_selection_complete(vec![member]);
let _ = app.apply_cmd_result(result);
feed(&mut app, "");
assert!(
!app.tabs[i]
.scene
.document
.get_entity(member)
.unwrap()
.common()
.invisible
);
app.undo_steps(1);
assert!(
app.tabs[i]
.scene
.document
.get_entity(member)
.unwrap()
.common()
.invisible,
"undo uses the hidden flags from before picking"
);
}
#[test]
fn visibility_ribbon_manager_rejects_stale_target_and_stale_document() {
use crate::ui::window::block_visibility::Action as A;
let (mut app, target) = visibility_editor();
visibility_action(&mut app, A::Open(target));
visibility_action(&mut app, A::Name("Alternate".into()));
visibility_action(&mut app, A::New);
app.tabs[app.active_tab].scene.bump_geometry();
visibility_action(&mut app, A::Apply);
assert!(app.block_visibility.as_ref().unwrap().error.is_some());
assert_eq!(
app.block_visibility_context().unwrap().names,
["VisibilityState0"]
);
let _ = app.update(Message::CloseModal);
let mut stale = target;
stale.document += 1;
visibility_action(&mut app, A::Open(stale));
assert!(app.block_visibility.is_none());
assert!(app.active_modal.is_none());
let _ = app.run_command_line("BVSTATE DIALOG");
assert_eq!(app.active_modal, Some(ModalKind::BlockVisibility));
}
#[test]
fn local_undo_preserves_imported_native_networks_when_constraints_are_unchanged() {
use codec::objects::{
+261
View File
@@ -0,0 +1,261 @@
use super::{Message, ModalKind, OpenCADStudio};
use crate::scene::block_visibility as visibility;
use crate::ui::window::block_visibility::{Action, NewVisibility, RibbonContext, State, Target};
use iced::Task;
impl OpenCADStudio {
pub(super) fn block_visibility_context(&self) -> Option<RibbonContext> {
let tab = self.tabs.get(self.active_tab)?;
let definition = tab.active_block_edit_session()?.br_handle;
let parameter = visibility::parameter(&tab.scene.document, definition)?;
if parameter.states.is_empty() {
return None;
}
let current = self.visibility_current(self.active_tab, definition);
Some(RibbonContext {
target: Target {
document: tab.id,
definition,
parameter: parameter.handle,
},
names: parameter
.states
.iter()
.map(|state| state.name.clone())
.collect(),
current: parameter
.states
.get(current)
.map(|state| state.name.clone()),
mode: self.block_visibility_mode,
})
}
fn visibility_target_valid(&self, target: Target) -> bool {
self.block_visibility_context()
.is_some_and(|context| context.target == target)
}
pub(super) fn sync_block_visibility_display(&mut self) {
for tab in &mut self.tabs {
let definition = tab
.active_block_edit_session()
.map(|session| session.br_handle);
// Resolve imported state flags once when an editor opens, not on
// every view refresh. Subsequent state identity follows history.
if let Some(definition) = definition {
if !tab.scene.visibility_states.contains_key(&definition) {
if let Some(parameter) = visibility::parameter(&tab.scene.document, definition)
{
let current =
visibility::shown_state(&tab.scene.document, parameter.handle);
tab.scene.visibility_states.insert(definition, current);
}
}
}
let editing = definition.is_some();
let picking = tab
.active_cmd
.as_ref()
.is_some_and(|command| command.reveals_hidden_block_objects());
tab.scene
.set_block_visibility_reveal(editing && (self.block_visibility_mode || picking));
}
}
pub(super) fn handle_block_visibility(&mut self, action: Action) -> Task<Message> {
let i = self.active_tab;
match action {
Action::Open(target) => {
if !self.visibility_target_valid(target) {
return Task::none();
}
// A ribbon click can interrupt a running authoring command.
let cancellation =
if self.tabs[i].active_cmd.is_some() || self.pending_setvar.is_some() {
self.on_command_escape()
} else {
Task::none()
};
self.sync_block_visibility_display();
let original =
visibility::parameter(&self.tabs[i].scene.document, target.definition)
.unwrap()
.clone();
let current = self
.visibility_current(i, target.definition)
.min(original.states.len().saturating_sub(1));
let name = original
.states
.get(current)
.map(|state| state.name.clone())
.unwrap_or_default();
self.block_visibility = Some(State {
target,
edited: original.clone(),
original,
current,
selected: current,
name,
new_visibility: NewVisibility::Keep,
error: None,
epoch: self.tabs[i].scene.geometry_epoch,
});
self.active_modal = Some(ModalKind::BlockVisibility);
return cancellation;
}
Action::Current(target, name) => {
if !self.visibility_target_valid(target) {
return Task::none();
}
let parameter =
visibility::parameter(&self.tabs[i].scene.document, target.definition).unwrap();
let Some(index) = parameter.states.iter().position(|state| state.name == name)
else {
return Task::none();
};
if self.visibility_current(i, target.definition) != index {
let cancellation =
if self.tabs[i].active_cmd.is_some() || self.pending_setvar.is_some() {
self.on_command_escape()
} else {
Task::none()
};
self.push_undo_snapshot(i, "Visibility state");
self.show_visibility_state(i, target.definition, target.parameter, index);
self.finish_pending_history(i);
return cancellation;
}
}
Action::Mode(target) => {
if self.visibility_target_valid(target) {
self.block_visibility_mode = !self.block_visibility_mode;
self.sync_block_visibility_display();
}
}
Action::Apply => {
let Some(state) = self.block_visibility.as_ref() else {
return Task::none();
};
if !self.visibility_target_valid(state.target)
|| self.tabs[i].scene.geometry_epoch != state.epoch
|| visibility::parameter(&self.tabs[i].scene.document, state.target.definition)
!= Some(&state.original)
{
self.block_visibility.as_mut().unwrap().error = Some(
crate::t!("The block changed. Cancel and reopen Visibility States.")
.into_owned(),
);
return Task::none();
}
let state = self.block_visibility.take().unwrap();
if state.edited != state.original
|| state.current != self.visibility_current(i, state.target.definition)
{
self.push_undo_snapshot(i, "Visibility states");
visibility::replace_states(
&mut self.tabs[i].scene.document,
state.target.definition,
state.edited,
);
self.show_visibility_state(
i,
state.target.definition,
state.target.parameter,
state.current,
);
self.finish_pending_history(i);
self.refresh_properties();
}
self.active_modal = None;
}
action => {
let Some(state) = self.block_visibility.as_mut() else {
return Task::none();
};
state.error = None;
match action {
Action::Select(index) => state.select(index),
Action::Name(name) => state.name = name,
Action::NewVisibility(mode) => state.new_visibility = mode,
Action::New | Action::Rename => {
let rename = matches!(action, Action::Rename);
if let Some(error) = state.name_error(rename) {
state.error = Some(error);
return Task::none();
}
let name = state.name.trim().to_string();
if rename {
state.edited.states[state.selected].name = name;
} else {
let mut new = state.edited.states[state.current].clone();
new.name = name;
if state.new_visibility != NewVisibility::Keep {
// Newly controlled geometry was visible in all existing states.
let document = &self.tabs[i].scene.document;
let members = document
.block_records
.iter()
.find(|record| record.handle == state.target.definition)
.map(|record| record.entity_handles.clone())
.unwrap_or_default();
for handle in members {
if !state.edited.all_blocks.contains(&handle) {
state.edited.all_blocks.push(handle);
if document
.get_entity(handle)
.is_some_and(|entity| !entity.common().invisible)
{
for old in &mut state.edited.states {
old.visible_blocks.push(handle);
}
}
}
}
new.visible_blocks = if state.new_visibility == NewVisibility::Show
{
state.edited.all_blocks.clone()
} else {
Vec::new()
};
}
state.edited.states.push(new);
state.select(state.edited.states.len() - 1);
}
}
Action::Delete if state.edited.states.len() > 1 => {
let removed = state.selected;
state.edited.states.remove(removed);
if state.current == removed {
state.current = 0;
} else if state.current > removed {
state.current -= 1;
}
state.select(removed.min(state.edited.states.len() - 1));
}
Action::SetCurrent => state.current = state.selected,
Action::Move(up) => {
let other = if up {
state.selected.checked_sub(1)
} else {
Some(state.selected + 1)
};
if let Some(other) =
other.filter(|other| *other < state.edited.states.len())
{
state.edited.states.swap(state.selected, other);
if state.current == state.selected {
state.current = other;
} else if state.current == other {
state.current = state.selected;
}
state.select(other);
}
}
_ => {}
}
}
}
Task::none()
}
}
+58 -20
View File
@@ -97,12 +97,16 @@ impl OpenCADStudio {
}
/// The state the block shows in the editor.
fn visibility_current(&self, i: usize, block: Handle) -> usize {
pub(in crate::app) fn visibility_current(&self, i: usize, block: Handle) -> usize {
let scene = &self.tabs[i].scene;
scene
.visibility_states
.get(&block)
.copied()
.filter(|index| {
visibility::parameter(&scene.document, block)
.is_some_and(|parameter| *index < parameter.states.len())
})
.unwrap_or_else(|| {
visibility::parameter(&scene.document, block).map_or(0, |parameter| {
visibility::shown_state(&scene.document, parameter.handle)
@@ -110,7 +114,13 @@ impl OpenCADStudio {
})
}
fn show_visibility_state(&mut self, i: usize, block: Handle, parameter: Handle, state: usize) {
pub(in crate::app) fn show_visibility_state(
&mut self,
i: usize,
block: Handle,
parameter: Handle,
state: usize,
) {
visibility::apply_state(&mut self.tabs[i].scene.document, parameter, state);
self.tabs[i].scene.visibility_states.insert(block, state);
self.tabs[i].scene.bump_geometry();
@@ -143,6 +153,40 @@ impl OpenCADStudio {
}
}
}
let variable = cmd.strip_prefix("SETVAR ").unwrap_or(cmd);
if variable == "BVMODE" || variable.starts_with("BVMODE ") {
let value = variable.strip_prefix("BVMODE").unwrap().trim();
// Inline command probing may have opened the bare variable prompt.
if !value.is_empty() && self.pending_setvar.as_deref() == Some("BVMODE") {
self.pending_setvar = None;
}
match value {
"" => {
self.command_line.push_output(
crate::tf!(
"BVMODE: enter 0 (hide) or 1 (show dimmed) <{}>:",
u8::from(self.block_visibility_mode)
)
.as_ref(),
);
self.pending_setvar = Some("BVMODE".into());
}
"TOGGLE" => {
self.block_visibility_mode = !self.block_visibility_mode;
self.sync_block_visibility_display();
}
value @ ("0" | "1") => {
self.block_visibility_mode = value == "1";
self.sync_block_visibility_display();
self.command_line
.push_output(crate::tf!("BVMODE = {}", value).as_ref());
}
_ => self
.command_line
.push_error(crate::t!("BVMODE: only 0 or 1 is allowed.").as_ref()),
}
return Some(Task::none());
}
match cmd {
cmd if cmd == "BPARAMETER" || cmd.starts_with("BPARAMETER ") => {
let Some(block) = self.visibility_block(i, "BPARAMETER") else {
@@ -165,6 +209,17 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(command));
Some(Task::none())
}
"BVSTATE DIALOG" => {
if let Some(context) = self.block_visibility_context() {
return Some(self.handle_block_visibility(
crate::ui::window::block_visibility::Action::Open(context.target),
));
}
self.command_line.push_error(
crate::t!("Place a Visibility parameter in the Block Editor first.").as_ref(),
);
Some(Task::none())
}
"BVSTATE" => {
let block = self.visibility_block(i, "BVSTATE")?;
let document = &self.tabs[i].scene.document;
@@ -221,25 +276,8 @@ impl OpenCADStudio {
Some(Task::none())
}
"BVHIDE" | "BVSHOW" => {
let block = self.visibility_block(i, cmd)?;
self.visibility_block(i, cmd)?;
let hide = cmd == "BVHIDE";
if !hide {
// Every object of the block shows while they are picked;
// the state is put back once the pick ends.
let document = &mut self.tabs[i].scene.document;
let members: Vec<Handle> = document
.block_records
.iter()
.find(|record| record.handle == block)
.map(|record| record.entity_handles.clone())
.unwrap_or_default();
for handle in members {
if let Some(entity) = document.get_entity_mut(handle) {
entity.common_mut().invisible = false;
}
}
self.tabs[i].scene.bump_geometry();
}
let pending = if hide { "_BVHIDE" } else { "_BVSHOW" };
if !self.tabs[i].scene.selected.is_empty() {
return Some(self.dispatch_command(pending));
+91
View File
@@ -692,6 +692,97 @@ fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() {
);
}
#[test]
fn visibility_ribbon_real_controls_disable_enable_and_open_manager() {
with_stack(|| {
use crate::ui::window::block_visibility::Action as A;
let mut app = authoring_app();
app.ribbon.set_block_editing(true);
app.ribbon
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Compact);
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let point = authoring_control(&app, "BVSTATE_MANAGER");
authoring_click(&mut app, point);
assert!(app.block_visibility.is_none());
assert_eq!(app.tabs[i].scene.document, before);
authoring_capture(&app, "visibility-ribbon-disabled");
let _ = app.run_command_line("BPARAMETER VISIBILITY 0.5,0.5");
let target = app.block_visibility_context().unwrap().target;
let point = authoring_control(&app, "BVSTATE_MANAGER");
authoring_click(&mut app, point);
assert_eq!(
app.active_modal,
Some(crate::app::ModalKind::BlockVisibility)
);
let _ = app.update(Message::BlockVisibility(A::Name("Alternate".into())));
let _ = app.update(Message::BlockVisibility(A::New));
authoring_capture(&app, "visibility-states-dialog");
let point = find_in_view(&app, "OK");
authoring_click(&mut app, point);
assert!(app.block_visibility.is_none());
assert_eq!(app.block_visibility_context().unwrap().names.len(), 2);
let messages = {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
let target = simulator
.click(iced::widget::Id::new("BVSTATE_CURRENT"))
.expect("current-state selector");
let bounds = target.visible_bounds().unwrap();
authoring_snapshot(
&mut simulator,
&app.active_theme,
"visibility-state-dropdown",
);
// Native pick-list options are painted text, not Text widgets.
// Pick the second row using the measured selector/row height.
let position = iced::Point::new(bounds.center_x(), bounds.y + bounds.height * 2.5);
simulator.point_at(position);
simulator.simulate([
iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }),
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
iced::mouse::Button::Left,
)),
]);
simulator.into_messages().collect::<Vec<_>>()
};
for message in messages {
let _ = app.update(message);
}
assert_eq!(
app.block_visibility_context().unwrap().current.as_deref(),
Some("Alternate")
);
let point = authoring_control(&app, "BVMODE_TOGGLE");
authoring_click(&mut app, point);
assert!(app.block_visibility_mode);
authoring_capture(&app, "visibility-ribbon-enabled");
app.ribbon
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Collapsed);
let point = find_in_view(&app, "Visibility ▾");
authoring_click(&mut app, point);
let messages = {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
simulator
.click("Visibility States")
.expect("manager in flyout");
simulator.into_messages().collect::<Vec<_>>()
};
for message in messages {
let _ = app.update(message);
}
assert_eq!(app.block_visibility.as_ref().unwrap().target, target);
});
}
fn authoring_app() -> OpenCADStudio {
use codec::{
entities::{Insert, Line},
+9
View File
@@ -797,6 +797,9 @@ impl HistorySnapshot {
.saturating_add(d.named_parameters.as_ref().map_or(0, |(before, after)| {
before.len().saturating_add(after.len()).saturating_mul(128)
}))
.saturating_add(d.visibility_states.as_ref().map_or(0, |(before, after)| {
(before.len() + after.len()).saturating_mul(32)
}))
.saturating_add(d.reference_designations.len().saturating_mul(16))
.saturating_add(d.label.len()),
HistorySnapshot::ObjectVisibility(v) => v
@@ -833,6 +836,11 @@ pub(super) struct ObjectVisibilitySnapshot {
/// cloning the flat entity store.
#[derive(Clone)]
pub(super) struct DeltaSnapshot {
/// Editor current-state indices must follow undo even when two states show identical objects.
pub(super) visibility_states: Option<(
rustc_hash::FxHashMap<Handle, usize>,
rustc_hash::FxHashMap<Handle, usize>,
)>,
pub(super) entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)>,
pub(super) current_layout_before: String,
pub(super) current_layout_after: String,
@@ -949,6 +957,7 @@ pub(super) struct HistoryState {
}
pub(super) struct PendingHistorySnapshot {
pub(super) visibility_states_before: rustc_hash::FxHashMap<Handle, usize>,
pub(super) label: String,
pub(super) current_layout: String,
pub(super) active_layer: String,
+17
View File
@@ -216,6 +216,7 @@ impl OpenCADStudio {
}
let selected: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
let delta = DeltaSnapshot {
visibility_states: None,
entities,
current_layout_before: self.tabs[i].scene.current_layout.clone(),
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -346,7 +347,11 @@ impl OpenCADStudio {
})
})
.collect();
let visibility_after = self.tabs[i].scene.visibility_states.clone();
let visibility_states = (pending.visibility_states_before != visibility_after)
.then_some((pending.visibility_states_before, visibility_after));
if !pending.keep_empty
&& visibility_states.is_none()
&& entities.is_empty()
&& reference_designations.is_empty()
&& !structure_changed
@@ -357,6 +362,7 @@ impl OpenCADStudio {
return;
}
let delta = DeltaSnapshot {
visibility_states,
entities,
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -403,6 +409,7 @@ impl OpenCADStudio {
let structure_before = self.tabs[i].scene.document.snapshot_structure();
let recorder = self.tabs[i].scene.document.begin_entity_change_recording();
self.tabs[i].history.pending = Some(PendingHistorySnapshot {
visibility_states_before: self.tabs[i].scene.visibility_states.clone(),
label,
current_layout,
active_layer: self.tabs[i].active_layer.clone(),
@@ -514,6 +521,7 @@ impl OpenCADStudio {
}
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
let delta = DeltaSnapshot {
visibility_states: None,
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -572,6 +580,7 @@ impl OpenCADStudio {
return;
}
let delta = DeltaSnapshot {
visibility_states: None,
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -630,6 +639,7 @@ impl OpenCADStudio {
return;
}
let delta = DeltaSnapshot {
visibility_states: None,
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -696,6 +706,7 @@ impl OpenCADStudio {
return;
}
let delta = DeltaSnapshot {
visibility_states: None,
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -726,6 +737,7 @@ impl OpenCADStudio {
self.finish_pending_history(i);
let (text_names, dim_names, object_handles) = after.changed_keys(&before);
let delta = DeltaSnapshot {
visibility_states: None,
entities: Vec::new(),
current_layout_before: self.tabs[i].scene.current_layout.clone(),
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -932,6 +944,7 @@ impl OpenCADStudio {
structure = Some(StructureSnapshot::Objects(objects));
}
let delta = DeltaSnapshot {
visibility_states: None,
entities,
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
@@ -1131,6 +1144,10 @@ impl OpenCADStudio {
let value = if undo { &entry.before } else { &entry.after };
*scene.parametric_constraint_set_mut(entry.scope) = value.clone();
}
if let Some((before, after)) = &d.visibility_states {
scene.visibility_states = if undo { before } else { after }.clone();
scene.bump_geometry();
}
if let Some((before, after)) = &d.named_parameters {
scene.named_parameters = if undo { before } else { after }.clone();
}
+7
View File
@@ -12,6 +12,7 @@ mod block_editor;
#[cfg(test)]
mod block_editor_tests;
mod block_save_as;
mod block_visibility;
mod block_properties;
#[cfg(test)]
mod block_properties_tests;
@@ -589,6 +590,8 @@ pub(super) struct OpenCADStudio {
block_definition_open_editor: Option<String>,
block_edit_picker: Option<crate::ui::window::block_edit_picker::State>,
block_save_as: Option<crate::ui::window::block_save_as::State>,
block_visibility: Option<crate::ui::window::block_visibility::State>,
block_visibility_mode: bool,
/// PDF dialogs' working copies; `None` while closed.
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
@@ -2239,6 +2242,7 @@ pub enum ModalKind {
BlockEditChanges,
BlockEditPicker,
BlockSaveAs,
BlockVisibility,
SaveDialog,
Recovery,
/// Fonts referenced by the drawing are missing on this machine.
@@ -2884,6 +2888,7 @@ pub enum Message {
BlockEditChangesCancel,
BlockSaveAsName(String),
BlockSaveAsApply,
BlockVisibility(crate::ui::window::block_visibility::Action),
BlockEditPickerName(String),
BlockEditPickerSelect(String),
BlockEditPickerApply,
@@ -4658,6 +4663,8 @@ impl OpenCADStudio {
block_definition_open_editor: None,
block_edit_picker: None,
block_save_as: None,
block_visibility: None,
block_visibility_mode: false,
pdf_attach: None,
point_cloud_attach: None,
attdef_dialog: None,
+1 -1
View File
@@ -1254,7 +1254,7 @@ impl OpenCADStudio {
}
}
pub(super) fn on_command_escape(&mut self) -> Task<Message> {
pub(in crate::app) fn on_command_escape(&mut self) -> Task<Message> {
self.tabs[self.active_tab].measurement.clear();
// Escape answers a pending client getpoint request first: no point.
if self.control.get_point.is_some() {
+6
View File
@@ -236,6 +236,7 @@ impl OpenCADStudio {
Some(AttMan) => self.attman = None,
Some(BlockEditPicker) => self.block_edit_picker = None,
Some(BlockSaveAs) => self.block_save_as = None,
Some(BlockVisibility) => self.block_visibility = None,
Some(PdfAttach) => self.pdf_attach = None,
Some(UnderlayLayers) => self.underlay_layers = None,
Some(PdfImportSettings) => self.pdf_import_settings = None,
@@ -390,6 +391,7 @@ impl OpenCADStudio {
let before = self.tabs.get(self.active_tab).map(|tab| tab.id);
let task = self.update_message(msg);
self.restore_dynamic_ucs_after_command();
self.sync_block_visibility_display();
if self.tabs.get(self.active_tab).map(|tab| tab.id) != before {
self.sync_underlay_tab();
}
@@ -9658,6 +9660,9 @@ impl OpenCADStudio {
}
Message::CloseModal => {
if self.active_modal == Some(super::ModalKind::BlockVisibility) {
self.block_visibility = None;
}
// The Invalid Area dialog's ✕ undoes the boundary change, as its Cancel does.
if self.active_modal == Some(super::ModalKind::CountInvalidArea) {
return self.update(Message::Count(
@@ -10670,6 +10675,7 @@ impl OpenCADStudio {
Task::none()
}
Message::BlockSaveAsApply => self.apply_block_save_as_dialog(),
Message::BlockVisibility(action) => self.handle_block_visibility(action),
Message::BlockEditPickerName(name) => {
if let Some(state) = self.block_edit_picker.as_mut() {
state.name = name.clone();
+1
View File
@@ -2218,6 +2218,7 @@ bg={bg_ms:.1}ms n={view_count}"
self.tabs[self.active_tab].history.undo_stack.len(),
self.tabs[self.active_tab].history.redo_stack.len(),
self.show_block_palette,
self.block_visibility_context(),
));
}
// Split the `chrome` bucket so live `view-detail` traces show
+7
View File
@@ -86,6 +86,7 @@ impl OpenCADStudio {
Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"),
Some(K::BlockEditChanges) => crate::t!("Block Editor").into_owned(),
Some(K::BlockEditPicker) => crate::t!("Edit Block Definition").into_owned(),
Some(K::BlockVisibility) => crate::t!("Visibility States").into_owned(),
Some(K::BlockSaveAs) => crate::t!("Save Block As").into_owned(),
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
@@ -520,6 +521,12 @@ impl OpenCADStudio {
crate::ui::window::block_edit_picker::view(state, flow)
})
}
super::super::ModalKind::BlockVisibility => {
let state = self.block_visibility.as_ref()?;
sized_flow(ex, 580, 470, |flow| {
crate::ui::window::block_visibility::view(state, flow)
})
}
super::super::ModalKind::BlockSaveAs => {
let state = self.block_save_as.as_ref()?;
sized_flow(ex, 430, 370, |flow| {