Files
OneCAD-Draft/src/app/visibility.rs
T
Hakan SevenandClaude Opus 5.5 5f5b64ef70 Properties: a dynamic block's visibility state as a dropdown
A reference to a block with a visibility parameter gets a Custom row named
after the parameter, listing its states with the one shown selected.
Picking a state applies it the same way as the reference's visibility
grip, one undo step.

Refs #1862

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 19:21:59 +03:00

256 lines
9.6 KiB
Rust

//! Dynamic-block visibility-state UI.
//!
//! A dynamic block with a visibility parameter shows a small "lookup" grip;
//! clicking it opens a dropdown of named states. Picking one toggles which of
//! the inserted (anonymous) block's member entities are visible.
//!
//! This is kept separate from the static `GripMenuAction` grip-menu system:
//! the state list is data-driven (owned strings from the file) and the apply
//! step mutates *other* entities (the anonymous block's members), which the
//! per-entity `apply_grip_menu` path can't reach.
use std::collections::HashSet;
use codec::objects::BlockVisibilityParameter;
use codec::types::Vector3;
use codec::{EntityType, Handle};
use crate::scene::model::object::{GripDef, GripShape};
use super::OpenCADStudio;
/// Sentinel grip id for the visibility (lookup) grip, distinct from any
/// entity's own grip ids so the click handler routes it to the dropdown.
pub(super) const VIS_GRIP_ID: usize = usize::MAX;
/// Resolved visibility info for the active tab's selected dynamic-block INSERT.
#[derive(Clone, Debug)]
pub(super) struct VisibilityGrip {
pub insert_handle: Handle,
pub state_names: Vec<String>,
pub current: Option<usize>,
}
/// The open visibility-state dropdown.
#[derive(Clone, Debug)]
pub struct VisibilityPopup {
pub insert_handle: Handle,
pub anchor: iced::Point,
pub items: Vec<String>,
pub current: Option<usize>,
}
/// Block-local entity-handle lists for a dynamic definition and the anonymous
/// block actually inserted, plus the state's visible-by-index set.
struct StateMapping {
/// Anonymous (inserted) block member handles, in definition order.
anon_handles: Vec<Handle>,
/// Indices (into `anon_handles`) that the state makes visible.
visible_idx: HashSet<usize>,
}
impl OpenCADStudio {
/// Compute the index sets needed to apply `state` of `param` (governing the
/// dynamic definition `def_block`) to the anonymous block `anon_name`.
///
/// The anonymous block is an evaluated clone of the definition with a
/// parallel member order, so a member visible in the definition maps to the
/// same position in the anonymous block.
fn state_mapping(
doc: &codec::CadDocument,
def_block: Handle,
anon_name: &str,
state_idx: usize,
param: &BlockVisibilityParameter,
) -> Option<StateMapping> {
let def_handles: Vec<Handle> = doc
.block_records
.iter()
.find(|b| b.handle == def_block)?
.entity_handles
.clone();
let anon_handles: Vec<Handle> = doc
.block_records
.iter()
.find(|b| b.name == anon_name)?
.entity_handles
.clone();
let state = param.states.get(state_idx)?;
let visible: HashSet<u64> = state.visible_blocks.iter().map(|h| h.value()).collect();
let visible_idx: HashSet<usize> = def_handles
.iter()
.enumerate()
.filter(|(_, h)| visible.contains(&h.value()))
.map(|(i, _)| i)
.collect();
Some(StateMapping {
anon_handles,
visible_idx,
})
}
/// Which state (if any) matches the anonymous block's current per-member
/// invisibility flags.
fn current_visibility_state(
doc: &codec::CadDocument,
def_block: Handle,
anon_name: &str,
param: &BlockVisibilityParameter,
) -> Option<usize> {
// Currently-visible member positions in the anonymous block.
let anon = doc.block_records.iter().find(|b| b.name == anon_name)?;
let cur_visible: HashSet<usize> = anon
.entity_handles
.iter()
.enumerate()
.filter(|(_, h)| {
doc.get_entity(**h)
.map(|e| !e.common().invisible)
.unwrap_or(false)
})
.map(|(i, _)| i)
.collect();
(0..param.states.len()).find(|&si| {
Self::state_mapping(doc, def_block, anon_name, si, param)
.map(|m| m.visible_idx == cur_visible)
.unwrap_or(false)
})
}
/// The visibility parameter of a dynamic-block reference as a Properties
/// row: the parameter's name, its states, and the one shown now.
pub(super) fn visibility_property(
doc: &codec::CadDocument,
handle: Handle,
) -> Option<crate::scene::model::object::Property> {
let Some(EntityType::Insert(ins)) = doc.get_entity(handle) else {
return None;
};
let (def_block, param) = doc.dynamic_visibility_for_insert(handle)?;
let options: Vec<String> = param.states.iter().map(|s| s.name.clone()).collect();
let current = Self::current_visibility_state(doc, def_block, &ins.block_name, param);
// An unnamed parameter takes the name a new one gets in the file.
let label = if param.element_name.trim().is_empty() {
"Visibility1".to_string()
} else {
param.element_name.clone()
};
Some(crate::scene::model::object::Property {
label,
field: "dyn_visibility",
value: crate::scene::model::object::PropValue::Choice {
selected: current.and_then(|c| options.get(c).cloned()).unwrap_or_default(),
options,
},
})
}
/// Recompute the visibility grip for the active tab's single selection and
/// append it (as a Triangle grip) to `selected_grips`. Clears it when the
/// selection is not a dynamic-block reference. `wo` is the world offset
/// already subtracted from the other grips.
pub(super) fn refresh_visibility_grip(&mut self, wo: [f64; 3]) {
let i = self.active_tab;
self.tabs[i].visibility_grip = None;
let Some(handle) = self.tabs[i].selected_handle else {
return;
};
let doc = &self.tabs[i].scene.document;
let Some(EntityType::Insert(ins)) = doc.get_entity(handle) else {
return;
};
let Some((def_block, param)) = doc.dynamic_visibility_for_insert(handle) else {
return;
};
// `def_point` is a block-definition coordinate (see the codec's
// BlockVisibilityParameter), so the grip follows the same base-point
// convention as the rendered reference.
let wp = crate::scene::render_graph::insert_transform(doc, ins).apply(Vector3::new(
param.def_point.x,
param.def_point.y,
param.def_point.z,
));
let state_names: Vec<String> = param.states.iter().map(|s| s.name.clone()).collect();
let anon_name = ins.block_name.clone();
let current = Self::current_visibility_state(doc, def_block, &anon_name, param);
self.tabs[i].selected_grips.push(GripDef {
id: VIS_GRIP_ID,
world: glam::DVec3::new(wp.x - wo[0], wp.y - wo[1], wp.z - wo[2]),
is_midpoint: false,
shape: GripShape::Triangle,
dir: None,
axis: None,
});
self.tabs[i].selected_grip_handles.push(handle);
self.tabs[i].visibility_grip = Some(VisibilityGrip {
insert_handle: handle,
state_names,
current,
});
}
/// Open the visibility dropdown at `anchor` for the active tab's grip.
pub(super) fn open_visibility_popup(&mut self, anchor: iced::Point) {
let i = self.active_tab;
if let Some(vg) = &self.tabs[i].visibility_grip {
self.visibility_popup = Some(VisibilityPopup {
insert_handle: vg.insert_handle,
anchor,
items: vg.state_names.clone(),
current: vg.current,
});
}
}
/// Apply visibility `state_idx` to the dynamic-block reference: set each
/// anonymous-block member visible/invisible per the state, then rebuild.
pub(super) fn apply_visibility_state(&mut self, insert_handle: Handle, state_idx: usize) {
let i = self.active_tab;
if self.reject_locked_edit(i, insert_handle) {
self.visibility_popup = None;
return;
}
// A reference to the definition itself, or to a copy other
// references share, gets its own evaluated copy first, so the state
// picked for it leaves the definition and the others alone.
self.push_undo_snapshot(i, "Visibility State");
self.tabs[i].scene.visibility_representation(insert_handle);
// Resolve everything against an immutable borrow first.
let mapping = {
let doc = &self.tabs[i].scene.document;
let Some(EntityType::Insert(ins)) = doc.get_entity(insert_handle) else {
return;
};
let anon_name = ins.block_name.clone();
let Some((def_block, param)) = doc.dynamic_visibility_for_insert(insert_handle) else {
return;
};
Self::state_mapping(doc, def_block, &anon_name, state_idx, param)
};
let Some(mapping) = mapping else {
return;
};
// Apply invisibility flags to the anonymous block's members.
{
let doc = &mut self.tabs[i].scene.document;
for (idx, h) in mapping.anon_handles.iter().enumerate() {
let visible = mapping.visible_idx.contains(&idx);
if let Some(e) = doc.get_entity_mut(*h) {
e.common_mut().invisible = !visible;
}
}
}
self.visibility_popup = None;
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.refresh_selected_grips();
}
}