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| {
+6
View File
@@ -2991,6 +2991,12 @@ pub trait CadCommand: Send {
/// While true, viewport clicks are routed through the normal selection
/// system (single / box / polygon) instead of the command's point-pick path.
/// After each completed selection action the host calls `on_selection_complete`.
/// A selection command may reveal state-hidden BEDIT members for picking.
/// This is a display request only; the document visibility flags stay intact.
fn reveals_hidden_block_objects(&self) -> bool {
false
}
fn is_selection_gathering(&self) -> bool {
false
}
+35
View File
@@ -94,6 +94,41 @@ impl CadModule for BlockEditorModule {
),
);
}
groups.push(RibbonGroup {
title: "Visibility",
tools: vec![
RibbonItem::LargeTool(ToolDef {
id: "BVSTATE_MANAGER",
label: "Visibility States",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/ui/list.svg")),
event: ModuleEvent::Command("BVSTATE DIALOG".into()),
}),
RibbonItem::Tool(ToolDef {
id: "BVMODE_TOGGLE",
label: "Visibility Mode",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/show_all.svg"
)),
event: ModuleEvent::Command("BVMODE TOGGLE".into()),
}),
RibbonItem::Tool(ToolDef {
id: "BVSHOW",
label: "Make Visible",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/show.svg"
)),
event: ModuleEvent::Command("BVSHOW".into()),
}),
RibbonItem::Tool(ToolDef {
id: "BVHIDE",
label: "Make Invisible",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/hide_all.svg"
)),
event: ModuleEvent::Command("BVHIDE".into()),
}),
],
});
groups
})
}
+5 -2
View File
@@ -66,7 +66,8 @@ impl CadCommand for BvStateCommand {
fn prompt(&self) -> String {
match &self.step {
Step::Option => {
crate::t!("BVSTATE Enter an option [New/Set/Rename/Delete] <Set>:").into_owned()
crate::t!("BVSTATE Enter an option [New/Set/Rename/Delete/Dialog] <Set>:")
.into_owned()
}
Step::NewName => crate::t!("BVSTATE Enter name of new visibility state:").into_owned(),
Step::SetName => crate::tf!(
@@ -93,6 +94,7 @@ impl CadCommand for BvStateCommand {
CmdOption::new(crate::t!("Set").as_ref(), "S"),
CmdOption::new(crate::t!("Rename").as_ref(), "R"),
CmdOption::new(crate::t!("Delete").as_ref(), "D"),
CmdOption::new(crate::t!("Dialog").as_ref(), "DIALOG"),
],
_ => Vec::new(),
}
@@ -114,6 +116,7 @@ impl CadCommand for BvStateCommand {
match &self.step {
Step::Option => {
self.step = match text.trim_start_matches('_').to_ascii_uppercase().as_str() {
"DIALOG" => return Some(CmdResult::Dispatch("BVSTATE DIALOG".into())),
"N" | "NEW" => Step::NewName,
"" | "S" | "SET" => Step::SetName,
"R" | "RENAME" => Step::RenameFrom,
@@ -196,5 +199,5 @@ impl CadCommand for BvStateCommand {
}
inventory::submit!(crate::command::CommandRegistration {
names: &["BVSTATE", "BVHIDE", "BVSHOW"]
names: &["BVSTATE", "BVHIDE", "BVSHOW", "BVMODE"]
});
+4
View File
@@ -177,6 +177,10 @@ impl SelectObjectsCommand {
}
impl CadCommand for SelectObjectsCommand {
fn reveals_hidden_block_objects(&self) -> bool {
self.pending_cmd == "_BVSHOW"
}
fn name(&self) -> &'static str {
"SELECT"
}
+127
View File
@@ -32,6 +32,46 @@ const MINOR: i32 = 329;
/// Value code of an expression that holds no value.
const NO_VALUE: i16 = -9999;
impl Scene {
/// BVMODE and BVSHOW picking reveal only state-controlled members of BEDIT.
pub(crate) fn visibility_revealed(&self, handle: Handle) -> bool {
self.block_visibility_reveal
&& self.block_edit_block.is_some_and(|definition| {
self.document.get_entity(handle).is_some_and(|entity| {
entity.common().owner_handle == definition && entity.common().invisible
}) && parameter(&self.document, definition)
.is_some_and(|parameter| parameter.all_blocks.contains(&handle))
})
}
pub(crate) fn visibility_reveal_handles(&self) -> rustc_hash::FxHashSet<Handle> {
self.block_edit_block
.and_then(|definition| parameter(&self.document, definition))
.filter(|_| self.block_visibility_reveal)
.map(|parameter| {
parameter
.all_blocks
.iter()
.copied()
.filter(|handle| self.visibility_revealed(*handle))
.collect()
})
.unwrap_or_default()
}
pub(crate) fn display_entity_invisible(&self, common: &codec::entities::EntityCommon) -> bool {
common.invisible && !self.visibility_revealed(common.handle)
}
pub(crate) fn set_block_visibility_reveal(&mut self, reveal: bool) {
if self.block_visibility_reveal != reveal {
self.block_visibility_reveal = reveal;
self.recolor_meshes();
self.bump_geometry();
}
}
}
/// (DXF name, class, DWG maintenance version) of the classes used here.
const CLASSES: &[(&str, &str, i32)] = &[
("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph", 55),
@@ -131,6 +171,20 @@ fn store(document: &mut CadDocument, parameter: BlockVisibilityParameter) {
);
}
/// Commit a staged state list while retaining the parameter's graph and identity.
pub(crate) fn replace_states(
document: &mut CadDocument,
definition: Handle,
edited: BlockVisibilityParameter,
) {
debug_assert_eq!(
parameter(document, definition).map(|p| p.handle),
Some(edited.handle)
);
store(document, edited);
tag_members(document, definition);
}
/// The objects of a definition, block begin and end left out.
fn members(document: &CadDocument, definition: Handle) -> Vec<Handle> {
document
@@ -784,6 +838,79 @@ mod tests {
use super::*;
use codec::entities::{Circle, Insert, Line};
#[test]
fn visibility_ribbon_reveal_respects_layers_uncontrolled_flags_and_nested_states() {
let mut scene = Scene::new();
scene.define_block_raw(
"nested",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
))],
);
let nested = scene.document.block_records.get("nested").unwrap().handle;
let child = members(&scene.document, nested)[0];
scene
.document
.get_entity_mut(child)
.unwrap()
.common_mut()
.invisible = true;
scene.define_block_raw(
"editor",
Vector3::ZERO,
vec![
EntityType::Line(Line::from_points(Vector3::ZERO, Vector3::UNIT_X)),
EntityType::Line(Line::from_points(
Vector3::UNIT_Y,
Vector3::UNIT_X + Vector3::UNIT_Y,
)),
EntityType::Insert(Insert::new("nested", Vector3::ZERO)),
],
);
let definition = scene.document.block_records.get("editor").unwrap().handle;
let controlled = members(&scene.document, definition)[0];
let independent = members(&scene.document, definition)[1];
scene
.document
.get_entity_mut(independent)
.unwrap()
.common_mut()
.invisible = true;
let parameter = create(&mut scene.document, definition, Vector3::ZERO).unwrap();
set_visible(&mut scene.document, parameter, 0, &[controlled], false);
apply_state(&mut scene.document, parameter, 0);
scene.block_edit_block = Some(definition);
let before = scene.document.clone();
scene.set_block_visibility_reveal(true);
let wires = scene.entity_wires();
assert!(wires
.iter()
.any(|wire| Scene::handle_from_wire_name(&wire.name) == Some(controlled)));
assert!(!scene.visibility_revealed(independent));
assert!(!scene.visibility_revealed(child));
assert_eq!(scene.visibility_reveal_handles().len(), 1);
assert_eq!(
scene.select_all_visible(),
2,
"only revealed line and the insert can be selected"
);
let color = scene.count_color(controlled, [1.0; 4], [0.0, 0.0, 0.0, 1.0]);
assert_eq!(color, [0.5, 0.5, 0.5, 1.0]);
scene.document.layers.get_mut("0").unwrap().flags.off = true;
scene.bump_geometry();
assert_eq!(
scene.select_all_visible(),
0,
"mode never overrides a hidden layer"
);
scene.document.layers.get_mut("0").unwrap().flags.off = false;
scene.set_block_visibility_reveal(false);
assert_eq!(scene.document, before);
assert!(!scene.visibility_revealed(controlled));
}
/// A block given visibility states round-trips them, and a reference
/// given its own copy still finds the definition and its states.
#[test]
+13 -1
View File
@@ -1510,7 +1510,7 @@ impl Scene {
return false;
}
let c = entity.common();
if c.invisible
if self.display_entity_invisible(c)
|| self.entity_temporarily_hidden(handle)
|| layer_hidden(&c.layer)
|| crate::scene::annotative::annotative_offscale_for(
@@ -1811,6 +1811,11 @@ impl Scene {
plot_only: bool,
) -> Vec<HatchModel> {
let depth_map = self.draw_depth_map();
let visibility_reveal = if plot_only {
rustc_hash::FxHashSet::default()
} else {
self.visibility_reveal_handles()
};
let graph = crate::scene::render_graph::RenderSceneGraph::new(
&self.document,
frozen,
@@ -1818,6 +1823,7 @@ impl Scene {
all_visible,
depth_map.as_ref(),
)
.with_visibility_reveal(&visibility_reveal)
.with_annotation_scale(self.annotation_scale)
.with_viewport(viewport);
let mut hatch_block_memo = std::collections::HashMap::new();
@@ -2022,6 +2028,11 @@ impl Scene {
plot_only: bool,
) -> Vec<HatchModel> {
let depth_map = self.draw_depth_map();
let visibility_reveal = if plot_only {
rustc_hash::FxHashSet::default()
} else {
self.visibility_reveal_handles()
};
let graph = crate::scene::render_graph::RenderSceneGraph::new(
&self.document,
frozen,
@@ -2029,6 +2040,7 @@ impl Scene {
all_visible,
depth_map.as_ref(),
)
.with_visibility_reveal(&visibility_reveal)
.with_annotation_scale(self.annotation_scale);
let mut models = Vec::new();
let mut wipeout_sources = rustc_hash::FxHashMap::default();
+67 -8
View File
@@ -2149,6 +2149,7 @@ pub struct Scene {
/// The visibility state each dynamic block shows in the Block Editor,
/// by block record.
pub visibility_states: HashMap<Handle, usize>,
pub(crate) block_visibility_reveal: bool,
/// Currently selected entity handles.
pub selected: HashSet<Handle>,
/// Polylines only part of which is selected (Ctrl+click): the picked
@@ -2871,6 +2872,7 @@ impl Scene {
paper_projected_cache: RefCell::new(HashMap::default()),
current_layout: "Model".to_string(),
block_edit_block: None,
block_visibility_reveal: false,
viewcube_ucs: glam::Mat4::IDENTITY,
hatches: HashMap::default(),
meshes: HashMap::default(),
@@ -4558,6 +4560,11 @@ impl Scene {
/// top-level entity): the count colours, else faded; unchanged outside
/// count mode.
pub(crate) fn count_color(&self, handle: Handle, color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
let color = if self.visibility_revealed(handle) {
crate::scene::cache::block_cache::fade_toward_bg(color, bg)
} else {
color
};
let Some(count) = &self.count_display else {
return color;
};
@@ -4578,7 +4585,9 @@ impl Scene {
fn apply_refedit_fade(&self, wires: &mut Vec<WireModel>, bg: [f32; 4]) {
// Block copies kept as stubs draw in their prototype's colour; while
// copies are recoloured one by one they are expanded instead.
if (self.count_display.is_some() || self.refedit_keep.is_some())
if (self.count_display.is_some()
|| self.refedit_keep.is_some()
|| self.block_visibility_reveal)
&& wires.iter().any(|wire| wire.stub.is_some())
{
*wires = crate::scene::model::instance_model::expand_stubs(wires).into_owned();
@@ -4603,6 +4612,19 @@ impl Scene {
}
}
}
if self.block_visibility_reveal {
for wire in wires.iter_mut() {
if Self::handle_from_wire_name(&wire.name)
.is_some_and(|handle| self.visibility_revealed(handle))
{
wire.color = crate::scene::cache::block_cache::fade_toward_bg(wire.color, bg);
for vertex in &mut wire.text_verts {
vertex.color =
crate::scene::cache::block_cache::fade_toward_bg(vertex.color, bg);
}
}
}
}
let Some(keep) = &self.refedit_keep else {
return;
};
@@ -6665,6 +6687,7 @@ impl Scene {
/// Persisted `EntityCommon::invisible` values stay untouched and continue
/// to serve file/dynamic-block visibility.
pub fn reset_transient_visibility(&mut self) {
self.block_visibility_reveal = false;
self.object_isolation = ObjectIsolationState::default();
self.preview_hidden.clear();
self.command_preview_hidden.clear();
@@ -7398,6 +7421,7 @@ impl Scene {
fn resident_runs_match_memo(&self, block: Handle) -> bool {
self.count_display.is_none()
&& self.refedit_keep.is_none()
&& !self.block_visibility_reveal
&& self.active_live_section(block).is_none()
}
@@ -8597,6 +8621,11 @@ impl Scene {
return Vec::new();
}
let depth_map = self.draw_depth_map();
let visibility_reveal = if plotting {
HashSet::default()
} else {
self.visibility_reveal_handles()
};
let graph = render_graph::RenderSceneGraph::new(
&self.document,
frozen,
@@ -8604,6 +8633,7 @@ impl Scene {
all_visible,
depth_map.as_ref(),
)
.with_visibility_reveal(&visibility_reveal)
.with_annotation_scale(self.annotation_scale)
.with_viewport(viewport);
let mut models = Vec::new();
@@ -8686,6 +8716,15 @@ impl Scene {
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
});
}
if !plotting
&& self.visibility_revealed(if context.is_instanced() {
context.root_handle
} else {
handle
})
{
placed.opacity *= 0.3;
}
placed.draw_depth = context.draw_depth(handle, depth_map.as_ref());
models.push(collect(placed, context));
},
@@ -8968,6 +9007,20 @@ impl Scene {
material.diffuse[3] = style.0[3];
}
}
if self.visibility_revealed(handle) {
for mesh in &mut set.lods {
mesh.color = crate::scene::cache::block_cache::fade_toward_bg(
mesh.color,
self.current_bg(),
);
}
if let Some(material) = set.material.as_mut() {
material.diffuse = crate::scene::cache::block_cache::fade_toward_bg(
material.diffuse,
self.current_bg(),
);
}
}
Some(set)
})
.collect();
@@ -9390,13 +9443,15 @@ impl Scene {
}
let arc = if point_cloud::any_definition(&self.document) {
let depths = rustc_hash::FxHashMap::default();
let visibility_reveal = self.visibility_reveal_handles();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
(!frozen.is_empty()).then_some(frozen),
None,
true,
&depths,
);
)
.with_visibility_reveal(&visibility_reveal);
let mut clouds = Vec::new();
graph.walk_root(
self.render_scene_root(target_block),
@@ -9473,7 +9528,7 @@ impl Scene {
continue;
};
let common = entity.common();
if common.invisible
if self.display_entity_invisible(common)
|| self.entity_temporarily_hidden(common.handle)
|| self
.document
@@ -9639,7 +9694,7 @@ impl Scene {
let Some(c) = self.document.get_entity(handle).map(|e| e.common()) else {
return false;
};
if c.invisible {
if self.display_entity_invisible(c) {
return false;
}
!self.layer_hidden(&c.layer)
@@ -9673,6 +9728,7 @@ impl Scene {
return Vec::new();
}
let depth_map = self.draw_depth_map();
let visibility_reveal = self.visibility_reveal_handles();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
frozen,
@@ -9680,6 +9736,7 @@ impl Scene {
all_visible,
depth_map.as_ref(),
)
.with_visibility_reveal(&visibility_reveal)
.with_annotation_scale(self.annotation_scale)
.with_viewport(viewport);
let mut out = Vec::new();
@@ -9744,7 +9801,7 @@ impl Scene {
self.material_base_dir.as_deref(),
);
}
if self.count_display.is_some() {
if self.count_display.is_some() || self.visibility_revealed(context.root_handle) {
let own = transformed
.instance_color
.or(set.display_color())
@@ -9973,7 +10030,7 @@ impl Scene {
return false;
}
let common = entity.common();
if common.invisible
if self.display_entity_invisible(common)
|| self.entity_temporarily_hidden(handle)
|| self.layer_hidden(&common.layer)
|| self.interaction_layer_frozen(&common.layer)
@@ -10037,6 +10094,7 @@ impl Scene {
.flatten()
.copied()
.collect();
let visibility_reveal = self.visibility_reveal_handles();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
(!frozen.is_empty()).then_some(&frozen),
@@ -10044,6 +10102,7 @@ impl Scene {
all_visible,
depth_map.as_ref(),
)
.with_visibility_reveal(&visibility_reveal)
.with_annotation_scale(self.annotation_scale);
for entity in self.document.entities() {
let contextual = crate::scene::annotative::entity_for_annotation_context(
@@ -10053,7 +10112,7 @@ impl Scene {
);
let host = contextual.as_ref();
let common = host.common();
if common.invisible
if self.display_entity_invisible(common)
|| self.entity_temporarily_hidden(common.handle)
|| layer_hidden(&common.layer)
|| crate::scene::annotative::annotative_offscale_for(
@@ -11638,7 +11697,7 @@ impl Scene {
layer: Option<&codec::tables::layer::Layer>,
) -> bool {
let c = e.common();
if c.invisible {
if self.display_entity_invisible(c) {
return false;
}
// Session-only Isolate / Hide and interactive replacement previews.
+12 -1
View File
@@ -280,6 +280,7 @@ pub struct RenderSceneGraph<'a> {
all_visible: bool,
depths: &'a FxHashMap<u64, [f32; 2]>,
viewport: Option<Handle>,
visibility_reveal: Option<&'a FxHashSet<Handle>>,
}
impl<'a> RenderSceneGraph<'a> {
@@ -298,9 +299,15 @@ impl<'a> RenderSceneGraph<'a> {
all_visible,
depths,
viewport: None,
visibility_reveal: None,
}
}
pub fn with_visibility_reveal(mut self, handles: &'a FxHashSet<Handle>) -> Self {
self.visibility_reveal = Some(handles);
self
}
pub fn with_viewport(mut self, viewport: Option<Handle>) -> Self {
self.viewport = viewport.filter(|handle| handle.is_valid());
self
@@ -579,7 +586,11 @@ impl<'a> RenderSceneGraph<'a> {
fn document_visible(&self, entity: &EntityType) -> bool {
let common = entity.common();
if common.invisible {
if common.invisible
&& !self
.visibility_reveal
.is_some_and(|handles| handles.contains(&common.handle))
{
return false;
}
let layer = self.document.layers.get(&common.layer);
+11 -1
View File
@@ -26,6 +26,7 @@ pub(crate) use draw_panel::tools as panel_tools;
mod color_dropdown;
mod context_tools;
mod modify_panel;
mod visibility_panel;
pub use context_tools::{pdf_underlay_tools, point_cloud_tools, xref_tools, UnderlayContext};
use widgets::{StyleContext, *};
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
@@ -630,6 +631,7 @@ impl Ribbon {
undo_count: usize,
redo_count: usize,
show_block_palette: bool,
visibility: Option<crate::ui::window::block_visibility::RibbonContext>,
) -> Element<'_, Message> {
// ── Quick-access file commands + undo/redo, one merged flow ────────
let lead = iced::widget::Row::with_children(vec![
@@ -825,7 +827,15 @@ impl Ribbon {
// button. See `CollapsePanels`.
let ts = self.toggle_state(show_block_palette);
let panels: Vec<Panel<'_>> = groups
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
.map(|g| {
if self.modules[effective_active].id() == "block_editor"
&& g.title == "Visibility"
{
visibility_panel::panel(visibility.clone())
} else {
self.panel(g, ts, &style_ctx, &|_| Vec::new())
}
})
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
+165
View File
@@ -0,0 +1,165 @@
//! The contextual visibility controls share the ribbon's buttons, tips and collapse layout.
use iced::widget::{button, column, container, pick_list, row, text, tooltip};
use iced::{Element, Fill, Theme};
use super::collapse::Panel;
use super::widgets::{make_icon, tool_btn_style, LARGE_ICON, LARGE_W, SMALL_ICON, TOOL_BAR_H};
use crate::app::Message;
use crate::modules::IconKind;
use crate::ui::window::block_visibility::{Action, RibbonContext};
use crate::ui::ROW_H;
fn tool(
id: &'static str,
label: &'static str,
icon: &'static [u8],
large: bool,
active: bool,
message: Option<Message>,
) -> Element<'static, Message> {
let enabled = message.is_some();
let size = if large { LARGE_ICON } else { SMALL_ICON };
let image = if enabled {
crate::ui::icons::semantic(icon, size)
} else {
crate::ui::icons::semantic_disabled(icon, size)
};
let face: Element<'static, Message> = if large {
column![
image,
text(crate::t!(label).into_owned())
.size(10)
.align_x(iced::Center)
]
.align_x(iced::Center)
.spacing(3)
.into()
} else {
image
};
let mut control = button(face)
.on_press_maybe(message)
.style(move |theme: &Theme, status| {
let mut style = tool_btn_style(theme, active, status);
if !enabled {
style.text_color = style.text_color.scale_alpha(0.42);
style.background = None;
}
style
})
.padding([3, 5]);
control = if large {
control.width(LARGE_W + 8.0).height(3.0 * ROW_H)
} else {
control.width(ROW_H + 4.0).height(ROW_H)
};
tooltip(
super::keytips::anchor(id, container(control).id(id)),
super::tooltip_content(crate::t!(label).into_owned()),
iced::widget::tooltip::Position::Right,
)
.gap(6)
.delay(std::time::Duration::from_millis(400))
.style(super::tooltip_style)
.into()
}
fn view(context: Option<RibbonContext>, compact: bool) -> Element<'static, Message> {
let target = context.as_ref().map(|context| context.target);
let mode = context.as_ref().is_some_and(|context| context.mode);
let manager = tool(
"BVSTATE_MANAGER",
"Visibility States",
include_bytes!("../../../assets/icons/ui/list.svg"),
!compact,
false,
target.map(|target| Message::BlockVisibility(Action::Open(target))),
);
let toggles = row![
tool(
"BVMODE_TOGGLE",
"Visibility Mode",
include_bytes!("../../../assets/icons/constrain/show_all.svg"),
false,
mode,
target.map(|target| Message::BlockVisibility(Action::Mode(target)))
),
tool(
"BVSHOW",
"Make Visible",
include_bytes!("../../../assets/icons/constrain/show.svg"),
false,
false,
target.map(|_| Message::Command("BVSHOW".into()))
),
tool(
"BVHIDE",
"Make Invisible",
include_bytes!("../../../assets/icons/constrain/hide_all.svg"),
false,
false,
target.map(|_| Message::Command("BVHIDE".into()))
),
]
.spacing(2);
let names = context
.as_ref()
.map(|context| context.names.clone())
.unwrap_or_default();
let current = context.as_ref().and_then(|context| context.current.clone());
let mut selector = pick_list(current, names, |name: &String| name.clone())
.placeholder(crate::t!("Visibility state").into_owned())
.text_size(11)
.padding([3, 6])
.width(Fill);
if let Some(target) = target {
selector =
selector.on_select(move |name| Message::BlockVisibility(Action::Current(target, name)));
}
let selector = container(selector).id("BVSTATE_CURRENT").width(Fill);
let controls: Element<'static, Message> = if compact {
column![row![manager, toggles].spacing(3), selector]
.spacing(4)
.width(160)
.height(Fill)
.into()
} else {
row![manager, column![toggles, selector].spacing(6).width(170)]
.spacing(4)
.height(Fill)
.into()
};
column![controls, text(crate::t!("Visibility").into_owned()).size(9)]
.align_x(iced::Center)
.padding([3, 4])
.height(TOOL_BAR_H)
.into()
}
fn collapsed(tight: bool) -> Element<'static, Message> {
let icon = make_icon(
IconKind::Svg(include_bytes!("../../../assets/icons/ui/list.svg")),
if tight { SMALL_ICON } else { LARGE_ICON },
);
button(
column![icon, text(crate::t!("Visibility ▾").into_owned()).size(10)]
.align_x(iced::Center)
.spacing(4),
)
.on_press(Message::ToggleRibbonPanel("Visibility".into()))
.style(|theme, status| tool_btn_style(theme, false, status))
.padding([4, 6])
.into()
}
pub(super) fn panel(context: Option<RibbonContext>) -> Panel<'static> {
Panel {
id: "Visibility".into(),
elements: [
view(context.clone(), false),
view(context, true),
collapsed(false),
collapsed(true),
],
}
}
+203
View File
@@ -0,0 +1,203 @@
use codec::objects::BlockVisibilityParameter;
use codec::Handle;
use iced::widget::{
button, column, container, pick_list, row, scrollable, text, text_input, Space,
};
use iced::{Element, Fill};
use crate::app::Message;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Target {
pub document: u64,
pub definition: Handle,
pub parameter: Handle,
}
#[derive(Debug, Clone)]
pub struct RibbonContext {
pub target: Target,
pub names: Vec<String>,
pub current: Option<String>,
pub mode: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NewVisibility {
Keep,
Show,
Hide,
}
impl std::fmt::Display for NewVisibility {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
crate::t!(match self {
Self::Keep => "Keep current visibility",
Self::Show => "Show all objects",
Self::Hide => "Hide all objects",
})
.as_ref(),
)
}
}
#[derive(Debug, Clone)]
pub enum Action {
Open(Target),
Current(Target, String),
Mode(Target),
Select(usize),
Name(String),
NewVisibility(NewVisibility),
New,
Rename,
Delete,
SetCurrent,
Move(bool),
Apply,
}
pub(crate) struct State {
pub target: Target,
pub original: BlockVisibilityParameter,
pub edited: BlockVisibilityParameter,
pub current: usize,
pub selected: usize,
pub name: String,
pub new_visibility: NewVisibility,
pub error: Option<String>,
/// A dialog cannot overwrite changes made by another entry point.
pub epoch: u64,
}
impl State {
pub fn select(&mut self, index: usize) {
if let Some(state) = self.edited.states.get(index) {
self.selected = index;
self.name = state.name.clone();
self.error = None;
}
}
pub fn name_error(&self, renaming: bool) -> Option<String> {
if self.name.trim().is_empty() || self.name.chars().any(char::is_control) {
return Some(
crate::t!("Enter a nonempty state name without control characters.").into_owned(),
);
}
self.edited
.states
.iter()
.enumerate()
.any(|(index, state)| {
(!renaming || index != self.selected)
&& state.name.eq_ignore_ascii_case(self.name.trim())
})
.then(|| crate::t!("A visibility state with that name already exists.").into_owned())
}
}
pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let send = |action| Message::BlockVisibility(action);
let mut states = column![].spacing(2);
for (index, item) in state.edited.states.iter().enumerate() {
let label = if index == state.current {
crate::tf!("{} (current)", item.name).into_owned()
} else {
item.name.clone()
};
states = states.push(
button(text(label).size(13))
.width(Fill)
.on_press(send(Action::Select(index)))
.style(if index == state.selected {
button::primary
} else {
button::text
}),
);
}
let control = |label: &'static str, action, enabled: bool| {
button(text(crate::t!(label).into_owned()).size(13))
.width(Fill)
.on_press_maybe(enabled.then_some(send(action)))
};
container(
column![
row![
container(scrollable(states))
.width(Fill)
.height(Fill)
.style(container::bordered_box),
column![
control(
"Set Current",
Action::SetCurrent,
state.selected != state.current
),
control("Delete", Action::Delete, state.edited.states.len() > 1),
control("Move Up", Action::Move(true), state.selected > 0),
control(
"Move Down",
Action::Move(false),
state.selected + 1 < state.edited.states.len()
),
]
.spacing(8)
.width(130),
]
.spacing(12)
.height(Fill),
text(crate::t!("State name").into_owned()).size(13),
row![
text_input(crate::t!("State name").as_ref(), &state.name)
.on_input(move |name| send(Action::Name(name)))
.style(crate::ui::style::form::field_style),
button(text(crate::t!("Rename").into_owned())).on_press_maybe(
state
.name_error(true)
.is_none()
.then_some(send(Action::Rename))
),
]
.spacing(8),
row![
pick_list(
Some(state.new_visibility),
[
NewVisibility::Keep,
NewVisibility::Show,
NewVisibility::Hide
],
|mode: &NewVisibility| mode.to_string()
)
.on_select(move |mode| send(Action::NewVisibility(mode)))
.width(Fill),
button(text(crate::t!("New").into_owned())).on_press_maybe(
state
.name_error(false)
.is_none()
.then_some(send(Action::New))
),
]
.spacing(8),
text(state.error.clone().unwrap_or_default())
.size(12)
.style(text::danger),
row![
Space::new().width(Fill),
button(text(crate::t!("OK").into_owned()))
.on_press(send(Action::Apply))
.style(button::primary),
button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal),
]
.spacing(8),
]
.spacing(10),
)
.padding(16)
.width(sizing.width)
.height(sizing.height)
.into()
}
+1
View File
@@ -10,6 +10,7 @@ pub mod block_definition;
pub mod block_edit_picker;
pub mod block_palette;
pub mod block_save_as;
pub mod block_visibility;
pub mod browser;
pub mod count_palette;
pub mod drafting_settings;