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

+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);