forked from 1b-agent/opencadstudio-upstream
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:
7 files changed
+326
-86
No files matched your search
@@ -32,6 +32,9 @@ pub struct AppConfig {
|
||||
pub dock: crate::ui::dock::DockState,
|
||||
/// Whether the optional Graphic Attributes palette is visible.
|
||||
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.
|
||||
pub annotation_auto_scale: i8,
|
||||
/// Ribbon collapse density.
|
||||
@@ -55,6 +58,7 @@ impl Default for AppConfig {
|
||||
statusbar: StatusBarConfig::default(),
|
||||
dock: crate::ui::dock::DockState::default(),
|
||||
show_graphic_attributes: false,
|
||||
graphic_fills_on_current_layer: false,
|
||||
annotation_auto_scale: -4,
|
||||
ribbon: RibbonConfig::default(),
|
||||
plot: PlotDialogState::default(),
|
||||
|
||||
@@ -1087,6 +1087,7 @@ impl OpenCADStudio {
|
||||
dock
|
||||
},
|
||||
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,
|
||||
ribbon: crate::app::config::RibbonConfig {
|
||||
collapse: self.ribbon.collapse_mode(),
|
||||
@@ -1146,6 +1147,10 @@ impl OpenCADStudio {
|
||||
dock.ensure_settings();
|
||||
self.dock = dock;
|
||||
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
|
||||
.set_graphic_attributes(self.show_graphic_attributes);
|
||||
self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4);
|
||||
|
||||
@@ -233,6 +233,11 @@ impl OpenCADStudio {
|
||||
M::SetAllByLayer => self.on_graphic_attributes_set_all(true),
|
||||
M::SetAllByBlock => self.on_graphic_attributes_set_all(false),
|
||||
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 => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::command::FreeTextValuePromptCommand::new(
|
||||
@@ -668,11 +673,12 @@ impl OpenCADStudio {
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.is_some_and(is_open_fill_boundary)
|
||||
!self.tabs[i].scene.is_layer_locked(*handle)
|
||||
&& self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.is_some_and(is_open_fill_boundary)
|
||||
})
|
||||
.collect();
|
||||
if !open_boundaries.is_empty() {
|
||||
@@ -712,6 +718,19 @@ impl OpenCADStudio {
|
||||
selected: &[Handle],
|
||||
to_close: &[Handle],
|
||||
) -> 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 mut changed = false;
|
||||
for handle in to_close {
|
||||
@@ -809,7 +828,12 @@ impl OpenCADStudio {
|
||||
.with_origin(origin)
|
||||
.with_pattern(name.clone(), pattern.clone());
|
||||
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);
|
||||
} else if !is_open_fill_boundary(entity) {
|
||||
// Open boundaries the user chose not to close are expected
|
||||
@@ -861,8 +885,7 @@ impl OpenCADStudio {
|
||||
for entity in updated_hatches {
|
||||
self.tabs[i].scene.update_entity(entity);
|
||||
}
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
for hatch in new_hatches {
|
||||
for (hatch, layer) in new_hatches {
|
||||
self.tabs[i].scene.add_hatch(hatch, Some(&layer), None);
|
||||
}
|
||||
true
|
||||
@@ -1690,4 +1713,107 @@ mod tests {
|
||||
assert!(!app.ribbon.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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4161,7 +4161,9 @@ impl OpenCADStudio {
|
||||
Message::ToggleGraphicAttributes => {
|
||||
self.show_graphic_attributes ^= true;
|
||||
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.save_config();
|
||||
|
||||
+13
-12
@@ -123,7 +123,7 @@ pub struct DockState {
|
||||
impl Default for DockState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
left: vec![PanelId::Properties, PanelId::GraphicAttributes],
|
||||
left: vec![PanelId::Properties],
|
||||
right: vec![PanelId::BlockPalette],
|
||||
panels: BTreeMap::new(),
|
||||
}
|
||||
@@ -159,13 +159,18 @@ impl DockState {
|
||||
] {
|
||||
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);
|
||||
} else {
|
||||
self.left.push(PanelId::GraphicAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dock `id` directly below Properties (or at the end of the left edge)
|
||||
/// unless it already has a place, which is then kept.
|
||||
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.
|
||||
@@ -273,10 +278,6 @@ mod tests {
|
||||
fn default_docks_each_known_panel_on_an_edge() {
|
||||
let state = DockState::default();
|
||||
assert_eq!(state.location(PanelId::Properties), Some((DockSide::Left, 0)));
|
||||
assert_eq!(
|
||||
state.location(PanelId::GraphicAttributes),
|
||||
Some((DockSide::Left, 1))
|
||||
);
|
||||
assert_eq!(
|
||||
state.location(PanelId::BlockPalette),
|
||||
Some((DockSide::Right, 0))
|
||||
@@ -302,7 +303,7 @@ mod tests {
|
||||
Some((DockSide::Right, 0))
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,12 @@ pub struct GradientEditorState {
|
||||
pub inverted: bool,
|
||||
pub angle: String,
|
||||
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)]
|
||||
pub enum GradientMsg {
|
||||
Open,
|
||||
@@ -77,6 +81,7 @@ impl GradientEditorState {
|
||||
inverted,
|
||||
angle: format!("{:.1}°", hatch.gradient_color.angle.to_degrees()),
|
||||
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> {
|
||||
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> {
|
||||
preview_image(preview_sized(
|
||||
&GradientEditorState::from_hatch(Vec::new(), hatch),
|
||||
256,
|
||||
26,
|
||||
))
|
||||
pub(crate) fn compact_preview(
|
||||
hatch: &acadrust::entities::Hatch,
|
||||
) -> Option<iced::widget::image::Handle> {
|
||||
preview_sized(&GradientEditorState::from_hatch(Vec::new(), hatch), 256, 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 {
|
||||
Some(handle) => image(handle)
|
||||
.width(Fill)
|
||||
|
||||
@@ -6,15 +6,16 @@ use crate::app::{ColorPickTarget, Message};
|
||||
use crate::scene::Scene;
|
||||
use crate::ui::dock::{DockMsg, PanelId};
|
||||
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::{CadDocument, EntityType, Handle};
|
||||
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,
|
||||
};
|
||||
use iced::{Background, Border, Element, Fill, Length, Padding, Theme};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::cell::RefCell;
|
||||
|
||||
type DrawDepth = FxHashMap<u64, [f32; 2]>;
|
||||
|
||||
@@ -101,6 +102,8 @@ pub enum GraphicAttributesMsg {
|
||||
SetAllByBlock,
|
||||
RemoveReferences,
|
||||
CreateLayer,
|
||||
/// Put new fills on the current layer instead of their object's layer.
|
||||
ToggleFillOnCurrentLayer,
|
||||
Fill(GraphicAttribute),
|
||||
LineColor(AcadColor),
|
||||
LineLinetype(String),
|
||||
@@ -145,6 +148,12 @@ pub struct GraphicAttributesState {
|
||||
pub hatch_editor: Option<HatchEditorState>,
|
||||
pub gradient_editor: Option<GradientEditorState>,
|
||||
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 {
|
||||
@@ -154,6 +163,36 @@ impl GraphicAttributesState {
|
||||
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.
|
||||
pub fn tracks_selection(&self) -> bool {
|
||||
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,
|
||||
/// otherwise the kind of the associative HATCH it bounds.
|
||||
pub(crate) fn current(document: &CadDocument, selected: &[Handle]) -> GraphicAttribute {
|
||||
// Scan the document once, rather than once per selected object on every frame.
|
||||
let mut fills: FxHashMap<Handle, Option<GraphicAttribute>> =
|
||||
selected.iter().map(|handle| (*handle, None)).collect();
|
||||
for entity in document.entities() {
|
||||
let EntityType::Hatch(hatch) = entity else {
|
||||
continue;
|
||||
};
|
||||
let value = GraphicAttribute::of(hatch);
|
||||
if let Some(fill) = fills.get_mut(&entity.common().handle) {
|
||||
fill.get_or_insert(value);
|
||||
}
|
||||
if !hatch.is_associative {
|
||||
continue;
|
||||
}
|
||||
for handle in hatch.paths.iter().flat_map(|path| &path.boundary_handles) {
|
||||
if let Some(fill) = fills.get_mut(handle) {
|
||||
fill.get_or_insert(value);
|
||||
/// Associative HATCH entities per boundary object, in document order.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct FillIndex {
|
||||
by_boundary: FxHashMap<Handle, Vec<Handle>>,
|
||||
}
|
||||
|
||||
impl FillIndex {
|
||||
pub(crate) fn build(document: &CadDocument) -> Self {
|
||||
let mut by_boundary: FxHashMap<Handle, Vec<Handle>> = FxHashMap::default();
|
||||
for entity in document.entities() {
|
||||
let EntityType::Hatch(hatch) = entity else {
|
||||
continue;
|
||||
};
|
||||
if !hatch.is_associative {
|
||||
continue;
|
||||
}
|
||||
for boundary in hatch.paths.iter().flat_map(|path| &path.boundary_handles) {
|
||||
let fills = by_boundary.entry(*boundary).or_default();
|
||||
if !fills.contains(&hatch.common.handle) {
|
||||
fills.push(hatch.common.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self { by_boundary }
|
||||
}
|
||||
let Some((&first, rest)) = selected.split_first() else {
|
||||
return GraphicAttribute::None;
|
||||
};
|
||||
let first = fills[&first].unwrap_or(GraphicAttribute::None);
|
||||
if rest
|
||||
.iter()
|
||||
.any(|handle| fills[handle].unwrap_or(GraphicAttribute::None) != first)
|
||||
{
|
||||
GraphicAttribute::Varies
|
||||
} else {
|
||||
first
|
||||
|
||||
/// The fill kind of each selected object: its own kind for a selected
|
||||
/// HATCH, otherwise the kind of the first associative HATCH it bounds.
|
||||
pub(crate) fn current(&self, document: &CadDocument, selected: &[Handle]) -> GraphicAttribute {
|
||||
let fill_of = |handle: &Handle| {
|
||||
let own = match document.get_entity(*handle) {
|
||||
Some(EntityType::Hatch(hatch)) => return GraphicAttribute::of(hatch),
|
||||
_ => None,
|
||||
};
|
||||
own.or_else(|| {
|
||||
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.
|
||||
/// Scans the whole drawing; the palette view uses its cached [`FillIndex`].
|
||||
pub(crate) fn fill_handles(document: &CadDocument, selected: &[Handle]) -> Vec<Handle> {
|
||||
let selected: FxHashSet<_> = selected.iter().copied().collect();
|
||||
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()
|
||||
FillIndex::build(document).fill_handles(document, selected)
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
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(
|
||||
crate::ui::icons::MENU,
|
||||
12.0,
|
||||
@@ -747,6 +815,14 @@ fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> {
|
||||
item("Set all attributes ByBlock", GraphicAttributesMsg::SetAllByBlock),
|
||||
item("Remove ByLayer / ByBlock", GraphicAttributesMsg::RemoveReferences),
|
||||
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)
|
||||
.padding(2);
|
||||
@@ -759,7 +835,12 @@ fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> {
|
||||
.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 {
|
||||
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
|
||||
} else {
|
||||
@@ -785,7 +866,7 @@ fn header<'a>(side: DockSide, auto_collapse: bool, menu_open: bool) -> Element<'
|
||||
row![
|
||||
text(crate::t!("Graphic Attributes")).size(12),
|
||||
Space::new().width(Fill),
|
||||
header_menu(side, menu_open),
|
||||
header_menu(side, menu_open, fill_on_current_layer),
|
||||
pin,
|
||||
close
|
||||
]
|
||||
@@ -1202,7 +1283,7 @@ pub fn view<'a>(
|
||||
let draw_depth = draw_depth.as_ref();
|
||||
let open_menu = state.open_menu;
|
||||
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);
|
||||
body = body.extend(line_section(
|
||||
@@ -1215,7 +1296,6 @@ pub fn view<'a>(
|
||||
open_menu,
|
||||
));
|
||||
|
||||
let current = current(document, &selected);
|
||||
body = body
|
||||
.push(Space::new().height(2))
|
||||
.push(text(crate::t!("Fill")).size(11))
|
||||
@@ -1279,7 +1359,7 @@ pub fn view<'a>(
|
||||
}
|
||||
GraphicAttribute::Gradient => {
|
||||
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) {
|
||||
iced::widget::stack![
|
||||
preview,
|
||||
@@ -1331,7 +1411,12 @@ pub fn view<'a>(
|
||||
}
|
||||
|
||||
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)
|
||||
])
|
||||
.width(Length::Fixed(width))
|
||||
|
||||
Reference in new issue
Block a user