Graphic Attributes: faster view, fills on object layer, dock on first show

- The palette view looks fills up through an index of associative fills
  that is rebuilt only when the scene's geometry epoch changes, instead
  of scanning the drawing twice per frame. The gradient previews are
  cached and rebuilt only when the gradient changes.
- New fills go to the layer of the filled object. A checkbox in the
  palette menu ("Create fills on the current layer") restores the HATCH
  behaviour and is saved in the config. Objects on locked layers are
  skipped with a message.
- The palette is no longer part of the default dock layout; it is docked
  below Properties the first time it is shown and keeps a place the user
  chose. The original dock test is restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgkJN8DEojuPTumAXLHotu
This commit is contained in:
Claude committed 2026-10-07 17:45:38 +00:00
1 parent 3c859c6b0e
commit 119131ee10
7 files changed
+326 -86

No files matched your search

+4
View File
@@ -32,6 +32,9 @@ pub struct AppConfig {
pub dock: crate::ui::dock::DockState, pub dock: crate::ui::dock::DockState,
/// Whether the optional Graphic Attributes palette is visible. /// Whether the optional Graphic Attributes palette is visible.
pub show_graphic_attributes: bool, pub show_graphic_attributes: bool,
/// Graphic Attributes creates fills on the current layer instead of the
/// layer of the filled object.
pub graphic_fills_on_current_layer: bool,
/// Add a newly selected annotation scale to existing annotative objects. /// Add a newly selected annotation scale to existing annotative objects.
pub annotation_auto_scale: i8, pub annotation_auto_scale: i8,
/// Ribbon collapse density. /// Ribbon collapse density.
@@ -55,6 +58,7 @@ impl Default for AppConfig {
statusbar: StatusBarConfig::default(), statusbar: StatusBarConfig::default(),
dock: crate::ui::dock::DockState::default(), dock: crate::ui::dock::DockState::default(),
show_graphic_attributes: false, show_graphic_attributes: false,
graphic_fills_on_current_layer: false,
annotation_auto_scale: -4, annotation_auto_scale: -4,
ribbon: RibbonConfig::default(), ribbon: RibbonConfig::default(),
plot: PlotDialogState::default(), plot: PlotDialogState::default(),
+5
View File
@@ -1087,6 +1087,7 @@ impl OpenCADStudio {
dock dock
}, },
show_graphic_attributes: self.show_graphic_attributes, show_graphic_attributes: self.show_graphic_attributes,
graphic_fills_on_current_layer: self.graphic_attributes.fill_on_current_layer,
annotation_auto_scale: self.annotation_auto_scale, annotation_auto_scale: self.annotation_auto_scale,
ribbon: crate::app::config::RibbonConfig { ribbon: crate::app::config::RibbonConfig {
collapse: self.ribbon.collapse_mode(), collapse: self.ribbon.collapse_mode(),
@@ -1146,6 +1147,10 @@ impl OpenCADStudio {
dock.ensure_settings(); dock.ensure_settings();
self.dock = dock; self.dock = dock;
self.show_graphic_attributes = cfg.show_graphic_attributes; self.show_graphic_attributes = cfg.show_graphic_attributes;
self.graphic_attributes.fill_on_current_layer = cfg.graphic_fills_on_current_layer;
if self.show_graphic_attributes {
self.dock.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes);
}
self.ribbon self.ribbon
.set_graphic_attributes(self.show_graphic_attributes); .set_graphic_attributes(self.show_graphic_attributes);
self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4); self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4);
+134 -8
View File
@@ -233,6 +233,11 @@ impl OpenCADStudio {
M::SetAllByLayer => self.on_graphic_attributes_set_all(true), M::SetAllByLayer => self.on_graphic_attributes_set_all(true),
M::SetAllByBlock => self.on_graphic_attributes_set_all(false), M::SetAllByBlock => self.on_graphic_attributes_set_all(false),
M::RemoveReferences => self.on_graphic_attributes_remove_references(), M::RemoveReferences => self.on_graphic_attributes_remove_references(),
M::ToggleFillOnCurrentLayer => {
self.graphic_attributes.fill_on_current_layer ^= true;
self.save_config();
Task::none()
}
M::CreateLayer => { M::CreateLayer => {
use crate::command::CadCommand; use crate::command::CadCommand;
let command = crate::command::FreeTextValuePromptCommand::new( let command = crate::command::FreeTextValuePromptCommand::new(
@@ -668,11 +673,12 @@ impl OpenCADStudio {
.iter() .iter()
.copied() .copied()
.filter(|handle| { .filter(|handle| {
self.tabs[i] !self.tabs[i].scene.is_layer_locked(*handle)
.scene && self.tabs[i]
.document .scene
.get_entity(*handle) .document
.is_some_and(is_open_fill_boundary) .get_entity(*handle)
.is_some_and(is_open_fill_boundary)
}) })
.collect(); .collect();
if !open_boundaries.is_empty() { if !open_boundaries.is_empty() {
@@ -712,6 +718,19 @@ impl OpenCADStudio {
selected: &[Handle], selected: &[Handle],
to_close: &[Handle], to_close: &[Handle],
) -> Task<Message> { ) -> Task<Message> {
let (selected, locked): (Vec<Handle>, Vec<Handle>) = selected
.iter()
.partition(|handle| !self.tabs[i].scene.is_layer_locked(**handle));
if !locked.is_empty() {
self.command_line.push_info(
crate::tf!(
"Graphic Attributes: {} object(s) on a locked layer were not changed.",
locked.len()
)
.as_ref(),
);
}
let selected = selected.as_slice();
let pending = self.begin_undo(i, "Graphic Attributes", selected.len(), true); let pending = self.begin_undo(i, "Graphic Attributes", selected.len(), true);
let mut changed = false; let mut changed = false;
for handle in to_close { for handle in to_close {
@@ -809,7 +828,12 @@ impl OpenCADStudio {
.with_origin(origin) .with_origin(origin)
.with_pattern(name.clone(), pattern.clone()); .with_pattern(name.clone(), pattern.clone());
if let crate::command::CmdResult::CommitHatch(hatch) = command.finish_selected() { if let crate::command::CmdResult::CommitHatch(hatch) = command.finish_selected() {
new_hatches.push(hatch); let layer = if self.graphic_attributes.fill_on_current_layer {
self.tabs[i].active_layer.clone()
} else {
entity.common().layer.clone()
};
new_hatches.push((hatch, layer));
changed_boundaries.insert(*handle); changed_boundaries.insert(*handle);
} else if !is_open_fill_boundary(entity) { } else if !is_open_fill_boundary(entity) {
// Open boundaries the user chose not to close are expected // Open boundaries the user chose not to close are expected
@@ -861,8 +885,7 @@ impl OpenCADStudio {
for entity in updated_hatches { for entity in updated_hatches {
self.tabs[i].scene.update_entity(entity); self.tabs[i].scene.update_entity(entity);
} }
let layer = self.tabs[i].active_layer.clone(); for (hatch, layer) in new_hatches {
for hatch in new_hatches {
self.tabs[i].scene.add_hatch(hatch, Some(&layer), None); self.tabs[i].scene.add_hatch(hatch, Some(&layer), None);
} }
true true
@@ -1690,4 +1713,107 @@ mod tests {
assert!(!app.ribbon.show_graphic_attributes); assert!(!app.ribbon.show_graphic_attributes);
assert!(!app.current_config().show_graphic_attributes); assert!(!app.current_config().show_graphic_attributes);
} }
fn closed_square_on(app: &mut OpenCADStudio, layer: &str) -> Handle {
let i = app.active_tab;
if app.tabs[i].scene.document.layers.get(layer).is_none() {
let mut new_layer = acadrust::tables::Layer::new(layer);
new_layer.handle = app.tabs[i].scene.document.allocate_handle();
let _ = app.tabs[i].scene.document.layers.add(new_layer);
}
let handle = open_square(app);
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
if let EntityType::LwPolyline(polyline) = entity {
polyline.is_closed = true;
}
entity.common_mut().layer = layer.to_string();
}
handle
}
fn fill_layers(app: &OpenCADStudio) -> Vec<String> {
app.tabs[app.active_tab]
.scene
.document
.entities()
.filter_map(|entity| match entity {
EntityType::Hatch(hatch) => Some(hatch.common.layer.clone()),
_ => None,
})
.collect()
}
#[test]
fn new_fills_follow_the_object_layer_unless_the_menu_option_is_set() {
let mut app = fresh();
closed_square_on(&mut app, "Walls");
let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
assert_eq!(fill_layers(&app), vec!["Walls".to_string()]);
let mut app = fresh();
let current = app.tabs[app.active_tab].active_layer.clone();
closed_square_on(&mut app, "Walls");
let _ = app.update(ga(GraphicAttributesMsg::ToggleFillOnCurrentLayer));
assert!(app.current_config().graphic_fills_on_current_layer);
let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
assert_eq!(fill_layers(&app), vec![current]);
}
#[test]
fn objects_on_locked_layers_get_no_fill() {
let mut app = fresh();
let i = app.active_tab;
closed_square_on(&mut app, "Locked");
closed_square_on(&mut app, "Open");
app.tabs[i]
.scene
.document
.layers
.get_mut("Locked")
.expect("locked layer")
.lock();
let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
assert_eq!(fill_layers(&app), vec!["Open".to_string()]);
}
#[test]
fn palette_fill_lookup_follows_document_changes() {
let mut app = fresh();
let i = app.active_tab;
let boundary = closed_square_on(&mut app, "0");
let selected = vec![boundary];
let (fills, kind) = app.graphic_attributes.fills(&app.tabs[i].scene, &selected);
assert!(fills.is_empty());
assert_eq!(kind, GraphicAttribute::None);
let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
let (fills, kind) = app.graphic_attributes.fills(&app.tabs[i].scene, &selected);
assert_eq!(fills.len(), 1);
assert_eq!(kind, GraphicAttribute::Solid);
}
#[test]
fn showing_the_palette_docks_it_below_properties_once() {
use crate::app::config::DockSide;
use crate::ui::dock::PanelId;
let mut app = OpenCADStudio::new_for_test();
app.apply_config(crate::app::config::AppConfig::default());
assert_eq!(app.dock.location(PanelId::GraphicAttributes), None);
let _ = app.update(Message::ToggleGraphicAttributes);
let properties = app.dock.location(PanelId::Properties).expect("properties docked");
assert_eq!(
app.dock.location(PanelId::GraphicAttributes),
Some((properties.0, properties.1 + 1))
);
// A place the user chose is kept when the palette is shown again.
app.dock.dock(PanelId::GraphicAttributes, DockSide::Right, 0);
let _ = app.update(Message::ToggleGraphicAttributes);
let _ = app.update(Message::ToggleGraphicAttributes);
assert_eq!(
app.dock.location(PanelId::GraphicAttributes),
Some((DockSide::Right, 0))
);
}
} }
+3 -1
View File
@@ -4161,7 +4161,9 @@ impl OpenCADStudio {
Message::ToggleGraphicAttributes => { Message::ToggleGraphicAttributes => {
self.show_graphic_attributes ^= true; self.show_graphic_attributes ^= true;
self.ribbon.set_graphic_attributes(self.show_graphic_attributes); self.ribbon.set_graphic_attributes(self.show_graphic_attributes);
if !self.show_graphic_attributes { if self.show_graphic_attributes {
self.dock.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes);
} else {
self.graphic_attributes.close_popups(); self.graphic_attributes.close_popups();
} }
self.save_config(); self.save_config();
+13 -12
View File
@@ -123,7 +123,7 @@ pub struct DockState {
impl Default for DockState { impl Default for DockState {
fn default() -> Self { fn default() -> Self {
Self { Self {
left: vec![PanelId::Properties, PanelId::GraphicAttributes], left: vec![PanelId::Properties],
right: vec![PanelId::BlockPalette], right: vec![PanelId::BlockPalette],
panels: BTreeMap::new(), panels: BTreeMap::new(),
} }
@@ -159,13 +159,18 @@ impl DockState {
] { ] {
self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id)); self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id));
} }
if self.location(PanelId::GraphicAttributes).is_none() { }
if let Some((side, index)) = self.location(PanelId::Properties) {
self.stack_mut(side).insert(index + 1, PanelId::GraphicAttributes); /// Dock `id` directly below Properties (or at the end of the left edge)
} else { /// unless it already has a place, which is then kept.
self.left.push(PanelId::GraphicAttributes); pub fn dock_below_properties(&mut self, id: PanelId) {
} if self.location(id).is_some() {
return;
} }
let (side, index) = self
.location(PanelId::Properties)
.map_or((DockSide::Left, usize::MAX), |(side, index)| (side, index + 1));
self.dock(id, side, index);
} }
/// Where (if anywhere) a panel is currently docked. /// Where (if anywhere) a panel is currently docked.
@@ -273,10 +278,6 @@ mod tests {
fn default_docks_each_known_panel_on_an_edge() { fn default_docks_each_known_panel_on_an_edge() {
let state = DockState::default(); let state = DockState::default();
assert_eq!(state.location(PanelId::Properties), Some((DockSide::Left, 0))); assert_eq!(state.location(PanelId::Properties), Some((DockSide::Left, 0)));
assert_eq!(
state.location(PanelId::GraphicAttributes),
Some((DockSide::Left, 1))
);
assert_eq!( assert_eq!(
state.location(PanelId::BlockPalette), state.location(PanelId::BlockPalette),
Some((DockSide::Right, 0)) Some((DockSide::Right, 0))
@@ -302,7 +303,7 @@ mod tests {
Some((DockSide::Right, 0)) Some((DockSide::Right, 0))
); );
// It no longer occupies the left edge. // It no longer occupies the left edge.
assert_eq!(state.left, vec![PanelId::GraphicAttributes]); assert!(state.left.is_empty());
assert_eq!(state.right.len(), 2); assert_eq!(state.right.len(), 2);
} }
+25 -8
View File
@@ -27,8 +27,12 @@ pub struct GradientEditorState {
pub inverted: bool, pub inverted: bool,
pub angle: String, pub angle: String,
pub centered: bool, pub centered: bool,
/// Large preview, rebuilt only when a setting it depends on changes.
preview: std::cell::RefCell<Option<(PreviewKey, Option<iced::widget::image::Handle>)>>,
} }
type PreviewKey = (GradientColorMode, AcadColor, AcadColor, u32, GradientKind, bool, String, bool);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum GradientMsg { pub enum GradientMsg {
Open, Open,
@@ -77,6 +81,7 @@ impl GradientEditorState {
inverted, inverted,
angle: format!("{:.1}°", hatch.gradient_color.angle.to_degrees()), angle: format!("{:.1}°", hatch.gradient_color.angle.to_degrees()),
centered: hatch.gradient_color.shift.abs() < 1e-9, centered: hatch.gradient_color.shift.abs() < 1e-9,
preview: Default::default(),
} }
} }
@@ -191,18 +196,30 @@ fn preview_sized(
} }
fn preview(state: &GradientEditorState) -> Element<'static, Message> { fn preview(state: &GradientEditorState) -> Element<'static, Message> {
preview_image(preview_sized(state, 370, 80)) let key: PreviewKey = (
state.color_mode,
state.color_1,
state.color_2,
state.shade_tint.to_bits(),
state.kind,
state.inverted,
state.angle.clone(),
state.centered,
);
let mut cache = state.preview.borrow_mut();
if cache.as_ref().is_none_or(|(cached, _)| *cached != key) {
*cache = Some((key, preview_sized(state, 370, 80)));
}
preview_image(cache.as_ref().and_then(|(_, handle)| handle.clone()))
} }
pub(crate) fn compact_preview(hatch: &acadrust::entities::Hatch) -> Element<'static, Message> { pub(crate) fn compact_preview(
preview_image(preview_sized( hatch: &acadrust::entities::Hatch,
&GradientEditorState::from_hatch(Vec::new(), hatch), ) -> Option<iced::widget::image::Handle> {
256, preview_sized(&GradientEditorState::from_hatch(Vec::new(), hatch), 256, 26)
26,
))
} }
fn preview_image(handle: Option<iced::widget::image::Handle>) -> Element<'static, Message> { pub(crate) fn preview_image(handle: Option<iced::widget::image::Handle>) -> Element<'static, Message> {
match handle { match handle {
Some(handle) => image(handle) Some(handle) => image(handle)
.width(Fill) .width(Fill)
+142 -57
View File
@@ -6,15 +6,16 @@ use crate::app::{ColorPickTarget, Message};
use crate::scene::Scene; use crate::scene::Scene;
use crate::ui::dock::{DockMsg, PanelId}; use crate::ui::dock::{DockMsg, PanelId};
use crate::ui::window::gradient_editor::{GradientEditorState, GradientMsg}; use crate::ui::window::gradient_editor::{GradientEditorState, GradientMsg};
use acadrust::entities::{EntityCommon, Hatch}; use acadrust::entities::{EntityCommon, Hatch, HatchGradientPattern};
use acadrust::types::{Color as AcadColor, LineWeight, Transparency}; use acadrust::types::{Color as AcadColor, LineWeight, Transparency};
use acadrust::{CadDocument, EntityType, Handle}; use acadrust::{CadDocument, EntityType, Handle};
use iced::widget::{ use iced::widget::{
button, column, combo_box, container, mouse_area, row, slider, text, text_input, tooltip, button, image, column, combo_box, container, mouse_area, row, slider, text, text_input, tooltip,
Space, Space,
}; };
use iced::{Background, Border, Element, Fill, Length, Padding, Theme}; use iced::{Background, Border, Element, Fill, Length, Padding, Theme};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use std::cell::RefCell;
type DrawDepth = FxHashMap<u64, [f32; 2]>; type DrawDepth = FxHashMap<u64, [f32; 2]>;
@@ -101,6 +102,8 @@ pub enum GraphicAttributesMsg {
SetAllByBlock, SetAllByBlock,
RemoveReferences, RemoveReferences,
CreateLayer, CreateLayer,
/// Put new fills on the current layer instead of their object's layer.
ToggleFillOnCurrentLayer,
Fill(GraphicAttribute), Fill(GraphicAttribute),
LineColor(AcadColor), LineColor(AcadColor),
LineLinetype(String), LineLinetype(String),
@@ -145,6 +148,12 @@ pub struct GraphicAttributesState {
pub hatch_editor: Option<HatchEditorState>, pub hatch_editor: Option<HatchEditorState>,
pub gradient_editor: Option<GradientEditorState>, pub gradient_editor: Option<GradientEditorState>,
pub pending_fill_close: Option<PendingFillClose>, pub pending_fill_close: Option<PendingFillClose>,
/// New fills go to the current layer instead of their object's layer.
pub fill_on_current_layer: bool,
/// View-side caches; `view` runs after every message, so neither the
/// fill lookup nor the gradient preview is rebuilt unless its input changed.
fill_index: RefCell<Option<((usize, u64), FillIndex)>>,
gradient_preview: RefCell<Option<(HatchGradientPattern, Option<image::Handle>)>>,
} }
impl GraphicAttributesState { impl GraphicAttributesState {
@@ -154,6 +163,36 @@ impl GraphicAttributesState {
self.gradient_editor = None; self.gradient_editor = None;
} }
/// The fills and fill kind of `selected`, from an index of associative
/// fills that is rebuilt only when the scene's geometry changes.
pub(crate) fn fills(&self, scene: &Scene, selected: &[Handle]) -> (Vec<Handle>, GraphicAttribute) {
let key = (std::ptr::from_ref(scene) as usize, scene.geometry_epoch);
let mut cache = self.fill_index.borrow_mut();
if cache.as_ref().is_none_or(|(cached, _)| *cached != key) {
*cache = Some((key, FillIndex::build(&scene.document)));
}
let index = &cache.as_ref().expect("fill index was just built").1;
(
index.fill_handles(&scene.document, selected),
index.current(&scene.document, selected),
)
}
fn gradient_preview(&self, hatch: &Hatch) -> Element<'static, Message> {
let mut cache = self.gradient_preview.borrow_mut();
if cache
.as_ref()
.is_none_or(|(gradient, _)| *gradient != hatch.gradient_color)
{
*cache = Some((
hatch.gradient_color.clone(),
crate::ui::window::gradient_editor::compact_preview(hatch),
));
}
let handle = cache.as_ref().and_then(|(_, handle)| handle.clone());
crate::ui::window::gradient_editor::preview_image(handle)
}
/// Whether a flyout or question is bound to the selection it was opened for. /// Whether a flyout or question is bound to the selection it was opened for.
pub fn tracks_selection(&self) -> bool { pub fn tracks_selection(&self) -> bool {
self.hatch_editor.is_some() self.hatch_editor.is_some()
@@ -190,62 +229,91 @@ impl GraphicAttributesState {
} }
} }
/// The fill kind of each selected object: its own kind for a selected HATCH, /// Associative HATCH entities per boundary object, in document order.
/// otherwise the kind of the associative HATCH it bounds. #[derive(Debug, Default)]
pub(crate) fn current(document: &CadDocument, selected: &[Handle]) -> GraphicAttribute { pub(crate) struct FillIndex {
// Scan the document once, rather than once per selected object on every frame. by_boundary: FxHashMap<Handle, Vec<Handle>>,
let mut fills: FxHashMap<Handle, Option<GraphicAttribute>> = }
selected.iter().map(|handle| (*handle, None)).collect();
for entity in document.entities() { impl FillIndex {
let EntityType::Hatch(hatch) = entity else { pub(crate) fn build(document: &CadDocument) -> Self {
continue; let mut by_boundary: FxHashMap<Handle, Vec<Handle>> = FxHashMap::default();
}; for entity in document.entities() {
let value = GraphicAttribute::of(hatch); let EntityType::Hatch(hatch) = entity else {
if let Some(fill) = fills.get_mut(&entity.common().handle) { continue;
fill.get_or_insert(value); };
} if !hatch.is_associative {
if !hatch.is_associative { continue;
continue; }
} for boundary in hatch.paths.iter().flat_map(|path| &path.boundary_handles) {
for handle in hatch.paths.iter().flat_map(|path| &path.boundary_handles) { let fills = by_boundary.entry(*boundary).or_default();
if let Some(fill) = fills.get_mut(handle) { if !fills.contains(&hatch.common.handle) {
fill.get_or_insert(value); fills.push(hatch.common.handle);
}
} }
} }
Self { by_boundary }
} }
let Some((&first, rest)) = selected.split_first() else {
return GraphicAttribute::None; /// The fill kind of each selected object: its own kind for a selected
}; /// HATCH, otherwise the kind of the first associative HATCH it bounds.
let first = fills[&first].unwrap_or(GraphicAttribute::None); pub(crate) fn current(&self, document: &CadDocument, selected: &[Handle]) -> GraphicAttribute {
if rest let fill_of = |handle: &Handle| {
.iter() let own = match document.get_entity(*handle) {
.any(|handle| fills[handle].unwrap_or(GraphicAttribute::None) != first) Some(EntityType::Hatch(hatch)) => return GraphicAttribute::of(hatch),
{ _ => None,
GraphicAttribute::Varies };
} else { own.or_else(|| {
first self.by_boundary.get(handle)?.iter().find_map(|fill| {
match document.get_entity(*fill)? {
EntityType::Hatch(hatch) => Some(GraphicAttribute::of(hatch)),
_ => None,
}
})
})
.unwrap_or(GraphicAttribute::None)
};
let Some((first, rest)) = selected.split_first() else {
return GraphicAttribute::None;
};
let first = fill_of(first);
if rest.iter().any(|handle| fill_of(handle) != first) {
GraphicAttribute::Varies
} else {
first
}
}
/// HATCH entities that are selected or associated with a selected boundary.
pub(crate) fn fill_handles(&self, document: &CadDocument, selected: &[Handle]) -> Vec<Handle> {
let mut seen = FxHashSet::default();
let mut fills = Vec::new();
for handle in selected {
if matches!(document.get_entity(*handle), Some(EntityType::Hatch(_)))
&& seen.insert(*handle)
{
fills.push(*handle);
}
for fill in self.by_boundary.get(handle).into_iter().flatten() {
if document.get_entity(*fill).is_some() && seen.insert(*fill) {
fills.push(*fill);
}
}
}
fills
} }
} }
/// The fill kind of the selection; see [`FillIndex::current`].
#[cfg(test)]
pub(crate) fn current(document: &CadDocument, selected: &[Handle]) -> GraphicAttribute {
FillIndex::build(document).current(document, selected)
}
/// HATCH entities that are selected or associated with a selected boundary. /// HATCH entities that are selected or associated with a selected boundary.
/// Scans the whole drawing; the palette view uses its cached [`FillIndex`].
pub(crate) fn fill_handles(document: &CadDocument, selected: &[Handle]) -> Vec<Handle> { pub(crate) fn fill_handles(document: &CadDocument, selected: &[Handle]) -> Vec<Handle> {
let selected: FxHashSet<_> = selected.iter().copied().collect(); FillIndex::build(document).fill_handles(document, selected)
document
.entities()
.filter_map(|entity| {
let EntityType::Hatch(hatch) = entity else {
return None;
};
let directly_selected = selected.contains(&entity.common().handle);
let selected_boundary = hatch.is_associative
&& hatch
.paths
.iter()
.flat_map(|path| &path.boundary_handles)
.any(|handle| selected.contains(handle));
(directly_selected || selected_boundary).then_some(entity.common().handle)
})
.collect()
} }
/// Selected non-HATCH objects plus the boundaries of selected associative /// Selected non-HATCH objects plus the boundaries of selected associative
@@ -717,7 +785,7 @@ fn transparency_control<'a>(
row![control, more].spacing(4).align_y(iced::Center).into() row![control, more].spacing(4).align_y(iced::Center).into()
} }
fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> { fn header_menu(side: DockSide, open: bool, fill_on_current_layer: bool) -> Element<'static, Message> {
let menu_button = button(crate::ui::icons::themed_secondary( let menu_button = button(crate::ui::icons::themed_secondary(
crate::ui::icons::MENU, crate::ui::icons::MENU,
12.0, 12.0,
@@ -747,6 +815,14 @@ fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> {
item("Set all attributes ByBlock", GraphicAttributesMsg::SetAllByBlock), item("Set all attributes ByBlock", GraphicAttributesMsg::SetAllByBlock),
item("Remove ByLayer / ByBlock", GraphicAttributesMsg::RemoveReferences), item("Remove ByLayer / ByBlock", GraphicAttributesMsg::RemoveReferences),
item("Create Layer with active settings", GraphicAttributesMsg::CreateLayer), item("Create Layer with active settings", GraphicAttributesMsg::CreateLayer),
container(
iced::widget::checkbox(fill_on_current_layer)
.label("Create fills on the current layer")
.on_toggle(|_| msg(GraphicAttributesMsg::ToggleFillOnCurrentLayer))
.size(12)
.text_size(11),
)
.padding([4, 8]),
]) ])
.style(crate::ui::color_select::popup_panel_style) .style(crate::ui::color_select::popup_panel_style)
.padding(2); .padding(2);
@@ -759,7 +835,12 @@ fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> {
.into() .into()
} }
fn header<'a>(side: DockSide, auto_collapse: bool, menu_open: bool) -> Element<'a, Message> { fn header<'a>(
side: DockSide,
auto_collapse: bool,
menu_open: bool,
fill_on_current_layer: bool,
) -> Element<'a, Message> {
let pin_icon = if auto_collapse { let pin_icon = if auto_collapse {
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
} else { } else {
@@ -785,7 +866,7 @@ fn header<'a>(side: DockSide, auto_collapse: bool, menu_open: bool) -> Element<'
row![ row![
text(crate::t!("Graphic Attributes")).size(12), text(crate::t!("Graphic Attributes")).size(12),
Space::new().width(Fill), Space::new().width(Fill),
header_menu(side, menu_open), header_menu(side, menu_open, fill_on_current_layer),
pin, pin,
close close
] ]
@@ -1202,7 +1283,7 @@ pub fn view<'a>(
let draw_depth = draw_depth.as_ref(); let draw_depth = draw_depth.as_ref();
let open_menu = state.open_menu; let open_menu = state.open_menu;
let line_handles = line_handles(document, &selected); let line_handles = line_handles(document, &selected);
let fill_handles = fill_handles(document, &selected); let (fill_handles, current) = state.fills(scene, &selected);
let mut body = column![text(crate::t!("Line")).size(11)].spacing(6); let mut body = column![text(crate::t!("Line")).size(11)].spacing(6);
body = body.extend(line_section( body = body.extend(line_section(
@@ -1215,7 +1296,6 @@ pub fn view<'a>(
open_menu, open_menu,
)); ));
let current = current(document, &selected);
body = body body = body
.push(Space::new().height(2)) .push(Space::new().height(2))
.push(text(crate::t!("Fill")).size(11)) .push(text(crate::t!("Fill")).size(11))
@@ -1279,7 +1359,7 @@ pub fn view<'a>(
} }
GraphicAttribute::Gradient => { GraphicAttribute::Gradient => {
if let Some(hatch) = top_fill_hatch(document, &fill_handles, draw_depth, current) { if let Some(hatch) = top_fill_hatch(document, &fill_handles, draw_depth, current) {
let preview = crate::ui::window::gradient_editor::compact_preview(hatch); let preview = state.gradient_preview(hatch);
let preview = if gradients_vary(document, &fill_handles) { let preview = if gradients_vary(document, &fill_handles) {
iced::widget::stack![ iced::widget::stack![
preview, preview,
@@ -1331,7 +1411,12 @@ pub fn view<'a>(
} }
container(column![ container(column![
header(side, auto_collapse, open_menu == Some(Menu::Header)), header(
side,
auto_collapse,
open_menu == Some(Menu::Header),
state.fill_on_current_layer
),
body.padding(8) body.padding(8)
]) ])
.width(Length::Fixed(width)) .width(Length::Fixed(width))