forked from 1b-agent/opencadstudio-upstream
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:
1 parent
308990e672
commit
daa26007f9
24 files changed
+1341
-34
No files matched your search
@@ -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(),
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in new issue
Block a user