From abef285cc60b31afe8617cece3c68758ef7a1cac Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 29 Sep 2026 10:10:05 +0200 Subject: [PATCH] Add Graphic Attributes fill and line controls --- assets/icons/attributes/lineweight.svg | 57 ++ assets/icons/attributes/transparency.svg | 50 ++ src/app/mod.rs | 33 + src/app/update/command.rs | 677 +++++++-------- src/app/update/dialog.rs | 402 ++++++++- src/app/update/mod.rs | 214 ++++- src/app/update/style.rs | 44 +- src/app/view/mod.rs | 7 + src/scene/entity.rs | 19 +- src/scene/hatch_boundary/tests.rs | 40 +- src/scene/model/hatch_model.rs | 17 +- src/scene/paper.rs | 50 +- src/ui/properties.rs | 305 ++++--- src/ui/window/graphic_attributes.rs | 1007 +++++++++++++++++----- src/ui/window/hatch_editor.rs | 36 - src/ui/window/mod.rs | 45 +- 16 files changed, 2112 insertions(+), 891 deletions(-) create mode 100644 assets/icons/attributes/lineweight.svg create mode 100644 assets/icons/attributes/transparency.svg delete mode 100644 src/ui/window/hatch_editor.rs diff --git a/assets/icons/attributes/lineweight.svg b/assets/icons/attributes/lineweight.svg new file mode 100644 index 000000000..1cd7f45e2 --- /dev/null +++ b/assets/icons/attributes/lineweight.svg @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/assets/icons/attributes/transparency.svg b/assets/icons/attributes/transparency.svg new file mode 100644 index 000000000..451288a65 --- /dev/null +++ b/assets/icons/attributes/transparency.svg @@ -0,0 +1,50 @@ + + + + + + + diff --git a/src/app/mod.rs b/src/app/mod.rs index 79dd787e8..6468edd00 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -770,6 +770,12 @@ pub(super) struct OpenCADStudio { show_properties: bool, show_graphic_attributes: bool, graphic_attribute_menu_open: bool, + line_color_menu_open: bool, + line_linetype_menu_open: bool, + line_lineweight_menu_open: bool, + line_transparency_menu_open: bool, + graphic_hatch_pattern_search: String, + graphic_hatch_pattern_focus: usize, solid_fill_color_menu_open: bool, fill_transparency_menu_open: bool, gradient_editor: Option, @@ -1531,6 +1537,7 @@ pub enum ColorPickerTab { #[derive(Debug, Clone)] pub enum ColorPickTarget { Gradient(u8), + GraphicAttributesLine, GraphicAttributesSolid, DimStyle(DsField), MLeader(&'static str), @@ -2057,6 +2064,22 @@ pub enum Message { ToggleGraphicAttributeDropdown, CloseGraphicAttributeDropdown, GraphicAttributeChanged(crate::ui::window::graphic_attributes::GraphicAttribute), + ToggleLineColorDropdown, + CloseLineColorDropdown, + LineColorChanged(AcadColor), + ToggleLineLinetypeMenu, + CloseLineLinetypeMenu, + LineLinetypeChanged(String), + ToggleLineLineweightMenu, + CloseLineLineweightMenu, + LineLineweightChanged(LineWeight), + LineLinetypeScaleChanged(f64), + LineLinetypeScaleInput(String), + ToggleLineTransparencyDropdown, + CloseLineTransparencyDropdown, + LineTransparencyChanged(u8), + LineTransparencyByLayer, + LineTransparencyByBlock, ToggleSolidFillColorDropdown, CloseSolidFillColorDropdown, SolidFillColorChanged(AcadColor), @@ -2067,6 +2090,10 @@ pub enum Message { FillTransparencyByBlock, HatchEditorOpen, HatchEditorClose, + GraphicHatchPatternSearchChanged(String), + GraphicHatchPatternFocus(usize), + GraphicHatchPatternChanged(String), + GraphicHatchPatternConfirm, GradientEditorOpen, GradientColorModeChanged(crate::ui::window::gradient_editor::GradientColorMode), GradientColorToggle(u8), @@ -4130,6 +4157,12 @@ impl OpenCADStudio { show_properties: true, show_graphic_attributes: true, graphic_attribute_menu_open: false, + line_color_menu_open: false, + line_linetype_menu_open: false, + line_lineweight_menu_open: false, + line_transparency_menu_open: false, + graphic_hatch_pattern_search: String::new(), + graphic_hatch_pattern_focus: 0, solid_fill_color_menu_open: false, fill_transparency_menu_open: false, gradient_editor: None, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 78b9be13c..27a1090f0 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -4,23 +4,22 @@ use super::util::*; use super::{format_size, VIEWCUBE_HIT_SIZE}; use crate::app::helpers::{ - parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, - CoordKind, + parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind, }; -use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS}; use crate::app::TextEntryMode; +use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS}; use crate::modules::ModuleEvent; -use crate::scene::pick::grip::{ - find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode, -}; #[cfg(not(target_arch = "wasm32"))] use crate::plugin::v4_support; use crate::scene::model::object::{GripApply, PropValue}; +use crate::scene::pick::grip::{ + find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode, +}; use crate::scene::{ self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX, }; -use crate::ui::PropertiesPanel; use crate::ui::window::attribute_editor::{AttrRow, AttrTab}; +use crate::ui::PropertiesPanel; use acadrust::types::Color as AcadColor; use acadrust::{EntityType as AcadEntityType, Handle}; use iced::time::Instant; @@ -243,12 +242,23 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // Live incremental search for INSERT/MINSERT (see CommandInput) let live = self.command_line.input.clone(); let i = self.active_tab; - let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { - if cmd.on_live_input(&live) { (true, cmd.options(), cmd.prompt()) } else { (false, Vec::new(), String::new()) } - } else { (false, Vec::new(), String::new()) }; + let (should_update, opts, prompt) = + if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { + if cmd.on_live_input(&live) { + (true, cmd.options(), cmd.prompt()) + } else { + (false, Vec::new(), String::new()) + } + } else { + (false, Vec::new(), String::new()) + }; if should_update { self.command_line.set_step_options(opts); - if let Some(last) = self.command_line.history.last_mut() { if last.pinned { last.text = prompt; } } + if let Some(last) = self.command_line.history.last_mut() { + if last.pinned { + last.text = prompt; + } + } } } } @@ -290,12 +300,23 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { { let live = self.command_line.input.clone(); let i = self.active_tab; - let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { - if cmd.on_live_input(&live) { (true, cmd.options(), cmd.prompt()) } else { (false, Vec::new(), String::new()) } - } else { (false, Vec::new(), String::new()) }; + let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() + { + if cmd.on_live_input(&live) { + (true, cmd.options(), cmd.prompt()) + } else { + (false, Vec::new(), String::new()) + } + } else { + (false, Vec::new(), String::new()) + }; if should_update { self.command_line.set_step_options(opts); - if let Some(last) = self.command_line.history.last_mut() { if last.pinned { last.text = prompt; } } + if let Some(last) = self.command_line.history.last_mut() { + if last.pinned { + last.text = prompt; + } + } } } self.focus_cmd_input() @@ -324,9 +345,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { crate::scene::model::object::GripMenuAction::Lengthen => { Some(GripEditMode::Lengthen) } - crate::scene::model::object::GripMenuAction::Radius => { - Some(GripEditMode::Radius) - } + crate::scene::model::object::GripMenuAction::Radius => Some(GripEditMode::Radius), crate::scene::model::object::GripMenuAction::ArcLength => { Some(GripEditMode::ArcLength) } @@ -342,7 +361,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { _ => None, }; expected_mode.is_some_and(|mode| { - !self.tabs[self.active_tab].active_grip.as_ref().is_some_and(|grip| { + !self.tabs[self.active_tab] + .active_grip + .as_ref() + .is_some_and(|grip| { grip.mode == mode && grip.handle == pending.handle && grip.grip_id == pending.grip_id @@ -369,17 +391,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let raw = crate::app::expr_eval::eval_to_string(entered); self.command_line.input.clear(); let Ok(v) = raw.parse::() else { - self.command_line.push_error(crate::tf!( - "{}: expected a number, got \"{raw}\"", - pending.label - ).as_ref()); + self.command_line.push_error( + crate::tf!("{}: expected a number, got \"{raw}\"", pending.label).as_ref(), + ); self.grip_pending = Some(pending); return self.focus_cmd_input(); }; - let interactive_value_grip = self.tabs[i] - .active_grip - .as_ref() - .is_some_and(|grip| { + let interactive_value_grip = self.tabs[i].active_grip.as_ref().is_some_and(|grip| { matches!( (grip.mode, pending.action), ( @@ -401,8 +419,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { GripEditMode::MoveParallel, crate::scene::model::object::GripMenuAction::MoveParallel, ) - ) - && grip.handle == pending.handle + ) && grip.handle == pending.handle && grip.grip_id == pending.grip_id }); if interactive_value_grip { @@ -410,8 +427,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } use crate::entities::traits::EntityTypeOps; self.push_undo_snapshot(i, pending.label); - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(pending.handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(pending.handle) { entity.apply_grip_menu_value(pending.grip_id, pending.action, v); } // A typed grip-menu value reshapes dimensions too — drop a @@ -419,10 +435,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.tabs[i] .scene .invalidate_dim_block_recorded(pending.handle); - self.tabs[i].scene.bump_entities(&[( - pending.handle, - crate::scene::ChangeKind::Modified, - )]); + self.tabs[i] + .scene + .bump_entities(&[(pending.handle, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; self.refresh_selected_grips(); self.refresh_properties(); @@ -439,22 +454,25 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let i = self.active_tab; if let Some(grip) = self.tabs[i].active_grip.clone() { - if matches!(grip.mode, GripEditMode::Stretch | GripEditMode::RectangleResize) { + if matches!( + grip.mode, + GripEditMode::Stretch | GripEditMode::RectangleResize + ) { let dyn_locked = self.tabs[i] .dyn_fields .iter() .any(|field| field.buffer.is_some()); let target = if matches!(grip.mode, GripEditMode::RectangleResize) { - grip.rectangle_frame.map(|(opposite, width_axis, height_axis)| { + grip.rectangle_frame + .map(|(opposite, width_axis, height_axis)| { let cursor_delta = self.tabs[i].last_cursor_world - opposite; let mut width = cursor_delta.dot(width_axis); let mut height = cursor_delta.dot(height_axis); for field in &self.tabs[i].dyn_fields { - let value = field - .buffer - .as_ref() - .and_then(|buffer| crate::app::expr_eval::eval_number(buffer)); + let value = field.buffer.as_ref().and_then(|buffer| { + crate::app::expr_eval::eval_number(buffer) + }); if let Some(value) = value { match field.role { crate::command::DynRole::Width => { @@ -479,9 +497,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.dyn_resolve_point() } else { // Legacy command-line direct-distance entry. - let text = crate::app::expr_eval::eval_to_string( - self.command_line.input.trim(), - ); + let text = + crate::app::expr_eval::eval_to_string(self.command_line.input.trim()); crate::app::expr_eval::eval_number(text.trim()).map(|dist| { let cursor = self.tabs[i].last_cursor_world; @@ -546,7 +563,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } let delta = target - grip.last_world; - let actions: Vec<_> = if let Some((opposite, width_axis, height_axis)) = grip.rectangle_frame { + let actions: Vec<_> = if let Some((opposite, width_axis, height_axis)) = + grip.rectangle_frame + { let opposite_id = (grip.grip_id + 2) % 4; let mut edits = Vec::with_capacity(3); for adjacent_id in [(opposite_id + 1) % 4, (opposite_id + 3) % 4] { @@ -554,44 +573,46 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .selected_grip_handles .iter() .zip(self.tabs[i].selected_grips.iter()) - .find(|(owner, candidate)| **owner == grip.handle && candidate.id == adjacent_id) + .find(|(owner, candidate)| { + **owner == grip.handle && candidate.id == adjacent_id + }) .map(|(_, candidate)| candidate.world) { let d = original - opposite; - let axis = if d.dot(width_axis).abs() >= d.dot(height_axis).abs() { + let axis = + if d.dot(width_axis).abs() >= d.dot(height_axis).abs() { width_axis } else { height_axis }; - edits.push((grip.handle, adjacent_id, GripApply::Absolute( + edits.push(( + grip.handle, + adjacent_id, + GripApply::Absolute( opposite + axis * (target - opposite).dot(axis), - ))); + ), + )); } } edits.push((grip.handle, grip.grip_id, GripApply::Absolute(target))); edits } else { - grip.targets.iter().map(|target_grip| { + grip.targets + .iter() + .map(|target_grip| { let apply = if target_grip.is_translate { GripApply::Translate(delta) } else { - GripApply::Absolute( - target_grip.last_world + delta, - ) + GripApply::Absolute(target_grip.last_world + delta) }; - ( - target_grip.handle, - target_grip.grip_id, - apply, - ) - }).collect() + (target_grip.handle, target_grip.grip_id, apply) + }) + .collect() }; for (handle, grip_id, apply) in actions { - self.tabs[i] - .scene - .apply_grip(handle, grip_id, apply); + self.tabs[i].scene.apply_grip(handle, grip_id, apply); } self.solve_grip_constraints(i, &grip); @@ -901,7 +922,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { /// has an established anchor, and `text` parses as a scalar distance, /// project along the active reference ray (if any) or the current cursor /// direction in the active UCS plane. - pub(in crate::app) fn try_direct_distance_entry(&mut self, text: &str) -> Option> { + pub(in crate::app) fn try_direct_distance_entry( + &mut self, + text: &str, + ) -> Option> { let i = self.active_tab; if self.tabs[i] .active_cmd @@ -1013,10 +1037,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let i = self.active_tab; let grip_dyn_locked = self.tabs[i].active_grip.is_some() && self.dyn_input - && self.tabs[i] - .dyn_fields - .iter() - .any(|field| field.locked()); + && self.tabs[i].dyn_fields.iter().any(|field| field.locked()); if grip_dyn_locked { return self.update(Message::CommandSubmit); @@ -1126,7 +1147,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { sel.box_crossing_locked = false; } if was_pan { - self.command_line.push_output(crate::t!("PAN ended.").as_ref()); + self.command_line + .push_output(crate::t!("PAN ended.").as_ref()); } self.ribbon.deactivate_tool(); return Task::none(); @@ -1212,7 +1234,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { Task::none() } - pub(super) fn on_layer_toggle_vp_freeze(&mut self, layer_idx: usize, vp_col_idx: usize) -> Task { + pub(super) fn on_layer_toggle_vp_freeze( + &mut self, + layer_idx: usize, + vp_col_idx: usize, + ) -> Task { let i = self.active_tab; let vp_handle = self.tabs[i] .layers @@ -1305,9 +1331,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // added row is measured. match new_idx { Some(idx) => iced::widget::operation::scroll_to( - iced::advanced::widget::Id::new( - crate::ui::window::layers::LAYER_TABLE_SCROLL_ID, - ), + iced::advanced::widget::Id::new(crate::ui::window::layers::LAYER_TABLE_SCROLL_ID), iced::widget::scrollable::AbsoluteOffset { x: 0.0, y: idx as f32 * crate::ui::ROW_H, @@ -1326,12 +1350,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } // Layer "0" and the current layer can't be deleted — drop them // from the batch and note it. - let current = self.tabs[i].scene.document.header.current_layer_name.clone(); + let current = self.tabs[i] + .scene + .document + .header + .current_layer_name + .clone(); let before = names.len(); names.retain(|n| n != "0" && *n != current); if names.len() < before { - self.command_line - .push_info(crate::t!("Layer \"0\" and the current layer can't be deleted — skipped.").as_ref()); + self.command_line.push_info( + crate::t!("Layer \"0\" and the current layer can't be deleted — skipped.").as_ref(), + ); } if names.is_empty() { return Task::none(); @@ -1424,7 +1454,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { fn sync_layer_panel(&mut self, i: usize) { let doc_layers = self.tabs[i].scene.document.layers.clone(); let vp_info = self.tabs[i].scene.viewport_list(); - self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info); + self.tabs[i] + .layers + .sync_with_viewports(&doc_layers, vp_info); self.tabs[i].layers.selected = None; self.tabs[i].layers.selected_multi.clear(); self.sync_ribbon_layers(); @@ -1525,10 +1557,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { movement, ); self.tabs[i].dirty = true; - self.tabs[i].scene.bump_entities(&[( - popup.handle, - crate::scene::ChangeKind::Modified, - )]); + self.tabs[i] + .scene + .bump_entities(&[(popup.handle, crate::scene::ChangeKind::Modified)]); } } // Stretch / Move = grab this grip. Engage it so the next @@ -1547,9 +1578,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { Some(acadrust::EntityType::MultiLeader(_)) ); let (grip_id, is_translate) = - if matches!(item.action, GripMenuAction::MoveWithLeader) - && is_multileader - { + if matches!(item.action, GripMenuAction::MoveWithLeader) && is_multileader { (crate::entities::multileader::MOVE_ALL_GRIP, true) } else { ( @@ -1569,14 +1598,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { }; let (frame, plane) = crate::entities::lwpolyline::rectangle_frame(polyline)?; - let opposite = polyline - .vertices - .get((popup.grip_id + 2) % 4)? - .location; + let opposite = polyline.vertices.get((popup.grip_id + 2) % 4)?.location; Some(( - glam::DVec3::from_array( - plane.point_at([opposite.x, opposite.y]), - ), + glam::DVec3::from_array(plane.point_at([opposite.x, opposite.y])), glam::DVec3::from_array(plane.vector_at(frame.width_axis)) .try_normalize()?, glam::DVec3::from_array(plane.vector_at(frame.height_axis)) @@ -1642,9 +1666,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .selected_grip_handles .iter() .zip(self.tabs[i].selected_grips.iter()) - .find(|(owner, grip)| { - **owner == popup.handle && grip.id == popup.grip_id - }) + .find(|(owner, grip)| **owner == popup.handle && grip.id == popup.grip_id) { if self.grip_originals.is_empty() { self.grip_originals = self.tabs[i] @@ -1656,36 +1678,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .unwrap_or_default(); } self.tabs[i].active_grip = Some(match item.action { - GripMenuAction::Radius => GripEdit::radius( - popup.handle, - popup.grip_id, - grip.world, - ), - GripMenuAction::ArcLength => GripEdit::arc_length( - popup.handle, - popup.grip_id, - grip.world, - ), - GripMenuAction::RectangleWidth => GripEdit::rectangle_width( - popup.handle, - popup.grip_id, - grip.world, - ), - GripMenuAction::RectangleHeight => GripEdit::rectangle_height( - popup.handle, - popup.grip_id, - grip.world, - ), - GripMenuAction::MoveParallel => GripEdit::move_parallel( - popup.handle, - popup.grip_id, - grip.world, - ), - _ => GripEdit::lengthen( - popup.handle, - popup.grip_id, - grip.world, - ), + GripMenuAction::Radius => { + GripEdit::radius(popup.handle, popup.grip_id, grip.world) + } + GripMenuAction::ArcLength => { + GripEdit::arc_length(popup.handle, popup.grip_id, grip.world) + } + GripMenuAction::RectangleWidth => { + GripEdit::rectangle_width(popup.handle, popup.grip_id, grip.world) + } + GripMenuAction::RectangleHeight => { + GripEdit::rectangle_height(popup.handle, popup.grip_id, grip.world) + } + GripMenuAction::MoveParallel => { + GripEdit::move_parallel(popup.handle, popup.grip_id, grip.world) + } + _ => GripEdit::lengthen(popup.handle, popup.grip_id, grip.world), }); } // Popup actions do not pass through the normal grip @@ -1695,23 +1703,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // move (and so keyboard input has a field to target). self.sync_dyn_fields(); if matches!(item.action, GripMenuAction::Radius) { - self.command_line.push_info( - crate::t!("Specify point or enter radius:").as_ref(), - ); + self.command_line + .push_info(crate::t!("Specify point or enter radius:").as_ref()); } else if matches!(item.action, GripMenuAction::ArcLength) { - self.command_line.push_info( - crate::t!("Specify point or enter arc length:").as_ref(), - ); + self.command_line + .push_info(crate::t!("Specify point or enter arc length:").as_ref()); } else if matches!(item.action, GripMenuAction::RectangleWidth) { self.command_line.push_info("Specify point or enter width:"); } else if matches!(item.action, GripMenuAction::RectangleHeight) { - self.command_line.push_info("Specify point or enter height:"); + self.command_line + .push_info("Specify point or enter height:"); } else if matches!(item.action, GripMenuAction::MoveParallel) { - self.command_line.push_info("Specify point or enter parallel offset:"); + self.command_line + .push_info("Specify point or enter parallel offset:"); } else { - self.command_line.push_info( - crate::t!("Specify point or enter distance:").as_ref(), - ); + self.command_line + .push_info(crate::t!("Specify point or enter distance:").as_ref()); } } else { self.command_line.push_info(crate::tf!("{label}:").as_ref()); @@ -1737,7 +1744,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.tabs[i].dirty = true; self.refresh_selected_grips(); self.refresh_properties(); - self.command_line.push_output(crate::t!("Polyline broken at vertex.").as_ref()); + self.command_line + .push_output(crate::t!("Polyline broken at vertex.").as_ref()); } None => self .command_line @@ -1763,9 +1771,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { }); if let Some((original, new_gid, old_len)) = placement { let dirty_before = self.tabs[i].dirty; - if let Some(entity) = - self.tabs[i].scene.document.get_entity_mut(popup.handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(popup.handle) { entity.apply_grip_menu(popup.grip_id, item.action); } let inserted = self.tabs[i] @@ -1775,18 +1781,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .and_then(polyline_vertex_count) .is_some_and(|len| len == old_len + 1); if !inserted { - if let Some(entity) = - self.tabs[i].scene.document.get_entity_mut(popup.handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(popup.handle) { *entity = original; } - self.tabs[i].scene.bump_entities(&[ - (popup.handle, crate::scene::ChangeKind::Modified), - ]); + self.tabs[i] + .scene + .bump_entities(&[(popup.handle, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = dirty_before; self.refresh_selected_grips(); self.refresh_properties(); - self.command_line.push_error(crate::t!("Cannot add a vertex here.").as_ref()); + self.command_line + .push_error(crate::t!("Cannot add a vertex here.").as_ref()); return Task::none(); } self.tabs[i] @@ -1799,30 +1804,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .selected_grip_handles .iter() .zip(self.tabs[i].selected_grips.iter()) - .find(|(owner, grip)| { - **owner == popup.handle && grip.id == new_gid - }) + .find(|(owner, grip)| **owner == popup.handle && grip.id == new_gid) .map(|(_, grip)| grip.world); if let Some(grip_world) = grip_world { self.grip_originals = vec![(popup.handle, original)]; self.grip_dirty_before = Some(dirty_before); - self.tabs[i].active_grip = Some(GripEdit::single( - popup.handle, - new_gid, - false, - grip_world, - )); + self.tabs[i].active_grip = + Some(GripEdit::single(popup.handle, new_gid, false, grip_world)); self.command_line .push_info(crate::t!("Specify new vertex location:").as_ref()); } else { - if let Some(entity) = - self.tabs[i].scene.document.get_entity_mut(popup.handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(popup.handle) { *entity = original; } - self.tabs[i].scene.bump_entities(&[ - (popup.handle, crate::scene::ChangeKind::Modified), - ]); + self.tabs[i] + .scene + .bump_entities(&[(popup.handle, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = dirty_before; self.refresh_selected_grips(); self.refresh_properties(); @@ -1842,10 +1839,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { (GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => { crate::entities::spline::shows_fit_points(spline) } - ( - GripMenuAction::ShowControlVertices, - acadrust::EntityType::Spline(spline), - ) => { + (GripMenuAction::ShowControlVertices, acadrust::EntityType::Spline(spline)) => { crate::entities::spline::shows_control_vertices(spline) } _ => false, @@ -1898,12 +1892,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .zip(self.tabs[i].selected_grips.iter()) .find(|(owner, g)| **owner == popup.handle && g.id == new_gid) { - self.tabs[i].active_grip = Some(GripEdit::single( - popup.handle, - new_gid, - false, - g.world, - )); + self.tabs[i].active_grip = + Some(GripEdit::single(popup.handle, new_gid, false, g.world)); self.grip_add_provisional = Some((popup.handle, new_gid)); } } @@ -1934,14 +1924,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // sync clipboard read returns nothing there); native uses // iced's clipboard. #[cfg(target_arch = "wasm32")] - return Task::perform( - crate::sys::read_clipboard_text(), - Message::MTextPasteClip, - ); + return Task::perform(crate::sys::read_clipboard_text(), Message::MTextPasteClip); #[cfg(not(target_arch = "wasm32"))] - return iced::clipboard::read_text().map(|result| { - Message::MTextPasteClip(result.ok().map(|text| (*text).clone())) - }); + return iced::clipboard::read_text() + .map(|result| Message::MTextPasteClip(result.ok().map(|text| (*text).clone()))); } if self.text_inline.is_some() { // Web: the iced text_input can't reach the async clipboard, @@ -1967,9 +1953,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { { Task::perform(crate::sys::read_clipboard_text(), |text| { Message::SystemClipboardPaste(match text { - Some(text) if !text.is_empty() => { - crate::app::SystemClipboardText::Text(text) - } + Some(text) if !text.is_empty() => crate::app::SystemClipboardText::Text(text), _ => crate::app::SystemClipboardText::EmptyOrUnsupported, }) }) @@ -1999,10 +1983,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let i = self.active_tab; self.tabs[i].scene.selection.borrow_mut().context_menu = None; let remembered = self.qselect_settings.clone(); - let scope = remembered.as_ref().map_or( - crate::app::QSelectScope::CurrentSpace, - |settings| settings.scope, - ); + let scope = remembered + .as_ref() + .map_or(crate::app::QSelectScope::CurrentSpace, |settings| { + settings.scope + }); let available_types = self.tabs[i].scene.qselect_entity_type_names(scope); let type_filter = if let Some(settings) = remembered.as_ref() { settings @@ -2036,11 +2021,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let mut operator = remembered .as_ref() .map_or(crate::app::QSelectOp::Eq, |settings| settings.operator); - if matches!(operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt) - && !property.as_ref().is_some_and(|property| { + if matches!( + operator, + crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt + ) && !property.as_ref().is_some_and(|property| { matches!(property.editor, crate::app::QSelectValueEditor::Number) - }) - { + }) { operator = crate::app::QSelectOp::Eq; } let value = remembered.as_ref().map_or_else(String::new, |settings| { @@ -2103,9 +2089,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // baked into the cached wire geometry — re-tessellate so the // change shows immediately (issue #231 class). self.apply_property_op(i, "CHPROP", &handles, |app, handle| { - if let Some(entity) = - app.tabs[i].scene.document.get_entity_mut(handle) - { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { crate::scene::view::dispatch::apply_common_prop(entity, "layer", &layer); } }); @@ -2132,9 +2116,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.ribbon.active_color = color; } else { self.apply_property_op(i, "CHPROP", &handles, |app, handle| { - if let Some(entity) = - app.tabs[i].scene.document.get_entity_mut(handle) - { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { crate::scene::view::dispatch::apply_color(entity, color); } }); @@ -2171,9 +2153,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // re-tessellate so the dashed/solid look updates immediately // (issue #231 class). self.apply_property_op(i, "CHPROP", &handles, |app, handle| { - if let Some(entity) = - app.tabs[i].scene.document.get_entity_mut(handle) - { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { crate::scene::view::dispatch::apply_common_prop(entity, "linetype", <); } }); @@ -2182,7 +2162,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { Task::none() } - pub(super) fn on_ribbon_style_changed(&mut self, key: crate::modules::StyleKey, name: String) -> Task { + pub(super) fn on_ribbon_style_changed( + &mut self, + key: crate::modules::StyleKey, + name: String, + ) -> Task { use crate::modules::StyleKey; self.ribbon.close_dropdown(); match key { @@ -2239,11 +2223,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { pub(super) fn on_prop_hatch_pattern_changed(&mut self, name: String) -> Task { let i = self.active_tab; let handles = self.property_target_handles(i); + self.apply_hatch_pattern(i, &handles, name) + } + + pub(super) fn apply_hatch_pattern( + &mut self, + i: usize, + handles: &[acadrust::Handle], + name: String, + ) -> Task { if !handles.is_empty() { use crate::scene::model::hatch_patterns; if let Some(entry) = hatch_patterns::find(&name) { self.push_undo_snapshot(i, "HATCHEDIT"); - for &handle in &handles { + for &handle in handles { if let Some(acadrust::EntityType::Hatch(dxf)) = self.tabs[i].scene.document.get_entity_mut(handle) { @@ -2261,14 +2254,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { dxf.pattern_angle, ); let origin = dxf.pattern_origin(); - crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y); + crate::entities::hatch::translate_pattern_geometry( + &mut pattern, + origin.x, + origin.y, + ); dxf.pattern = pattern; dxf.is_solid = matches!( entry.gpu, crate::scene::model::hatch_model::HatchPattern::Solid ); - dxf.pattern_type = - acadrust::entities::HatchPatternType::Predefined; + dxf.pattern_type = acadrust::entities::HatchPatternType::Predefined; dxf.gradient_color.enabled = false; } if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) { @@ -2297,8 +2293,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // The Annotative Yes/No list drives the per-object annotative toggle // (MTEXT's own flag, the annotation context for the rest). if field == "annotative" { - let wanted = - value == crate::t!("Yes").as_ref() || value.eq_ignore_ascii_case("yes"); + let wanted = value == crate::t!("Yes").as_ref() || value.eq_ignore_ascii_case("yes"); let Some(handle) = handles.first().copied() else { return Task::none(); }; @@ -2423,14 +2418,15 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if self.tabs[i].scene.is_layer_locked(handle) { continue; } - let Some(mut state) = self.tabs[i] + let Some(mut state) = + self.tabs[i] .scene .document .get_entity(handle) .and_then(|entity| match entity { - acadrust::EntityType::Surface(surface) => Some( - crate::entities::solid3d::surface_property_state(surface), - ), + acadrust::EntityType::Surface(surface) => { + Some(crate::entities::solid3d::surface_property_state(surface)) + } _ => None, }) else { @@ -2452,7 +2448,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { &mut self.tabs[i].scene.document, handle, crate::entities::solid3d::SURFACE_PROPERTIES_APP, - Some(crate::entities::solid3d::surface_property_xdata_values(state)), + Some(crate::entities::solid3d::surface_property_xdata_values( + state, + )), ); if field == PROP_SURFACE_WIREFRAME_TYPE { self.tabs[i].scene.reseed_derived_caches(handle); @@ -2643,15 +2641,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .then(|| { doc.objects.values().find_map(|object| match object { acadrust::objects::ObjectType::MultiLeaderStyle(style) - if style.name == value => Some(style.clone()), + if style.name == value => + { + Some(style.clone()) + } _ => None, }) }) .flatten(); let resolved: Option = match field { - "mleader_style" => { - resolved_mleader_style.as_ref().map(|style| style.handle) - } + "mleader_style" => resolved_mleader_style.as_ref().map(|style| style.handle), "text_style_handle" => doc .text_styles .iter() @@ -2788,9 +2787,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .document .block_records .iter() - .find(|b| { - crate::app::properties::arrowhead_label(&b.name) == value - }) + .find(|b| crate::app::properties::arrowhead_label(&b.name) == value) .map(|b| b.handle) } else { None @@ -2838,12 +2835,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { Some(XDataValue::Handle(acadrust::Handle::NULL)), ); } else if let Some(h) = arrow_h { - dov::set( - doc, - handle, - dov::DIMLDRBLK, - Some(XDataValue::Handle(h)), - ); + dov::set(doc, handle, dov::DIMLDRBLK, Some(XDataValue::Handle(h))); } } _ => {} @@ -2876,10 +2868,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { { if ins.block_name != canon { ins.block_name = canon.clone(); - changes.push(( - handle, - crate::scene::ChangeKind::Modified, - )); + changes.push((handle, crate::scene::ChangeKind::Modified)); } } } @@ -2888,7 +2877,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } } } else if field == "tbl_style_handle" { - let style_handle = self.tabs[i] + let style_handle = + self.tabs[i] .scene .document .objects @@ -2916,7 +2906,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { continue; } if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - crate::scene::view::dispatch::apply_common_prop(entity, "transparency", &value); + crate::scene::view::dispatch::apply_common_prop( + entity, + "transparency", + &value, + ); } } } else if field == "plot_style" { @@ -2927,22 +2921,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // Named plot-style pick: ByLayer / ByBlock clear the // handle; a named style resolves through the drawing's // ACAD_PLOTSTYLENAME dictionary to its placeholder handle. - let dict_h = - self.tabs[i].scene.document.header.acad_plotstylename_dict_handle; + let dict_h = self.tabs[i] + .scene + .document + .header + .acad_plotstylename_dict_handle; let ph: Option = crate::scene::annotative::as_dict(&self.tabs[i].scene.document, dict_h) - .and_then(|d| { - d.entries - .iter() - .find(|(n, _)| *n == value) - .map(|(_, h)| *h) - }); + .and_then(|d| d.entries.iter().find(|(n, _)| *n == value).map(|(_, h)| *h)); for &handle in &handles { if self.tabs[i].scene.is_layer_locked(handle) { continue; } - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { let common = entity.common_mut(); match value.as_str() { "ByLayer" => { @@ -2976,17 +2967,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .objects .iter() .find_map(|(h, o)| match o { - acadrust::objects::ObjectType::Material(m) if m.name == value => { - Some(*h) - } + acadrust::objects::ObjectType::Material(m) if m.name == value => Some(*h), _ => None, }); for &handle in &handles { if self.tabs[i].scene.is_layer_locked(handle) { continue; } - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { let common = entity.common_mut(); match value.as_str() { "ByLayer" => { @@ -3017,7 +3005,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { crate::command::WorkingPlane::default() }; for &handle in &handles { - let mline_style = self.tabs[i] + let mline_style = + self.tabs[i] .scene .document .get_entity(handle) @@ -3038,19 +3027,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { &value, ); } - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { crate::scene::view::dispatch::apply_geom_prop_in_working_plane( - entity, - field, - &value, - plane, + entity, field, &value, plane, ); if matches!(field, "ml_justification" | "ml_scale") { - if let ( - acadrust::EntityType::MLine(mline), - Some(style), - ) = (entity, mline_style.as_ref()) + if let (acadrust::EntityType::MLine(mline), Some(style)) = + (entity, mline_style.as_ref()) { crate::modules::draw::draw::mline::sync_mline_element_parameters( mline, style, @@ -3080,14 +3063,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } else { match field { "transparency" => { - let Some(transparency) = crate::scene::creation_style::parse_current_transparency(&value) else { - self.command_line.push_error("Transparency: expected ByLayer, ByBlock, or an integer from 0 to 90."); + let Some(transparency) = + crate::scene::creation_style::parse_current_transparency(&value) + else { + self.command_line.push_error( + "Transparency: expected ByLayer, ByBlock, or an integer from 0 to 90.", + ); self.refresh_properties(); return Task::none(); }; - if self.tabs[i].scene.document.current_entity_transparency() - != transparency - { + if self.tabs[i].scene.document.current_entity_transparency() != transparency { self.push_undo_snapshot(i, "CETRANSPARENCY"); if self.tabs[i] .scene @@ -3123,8 +3108,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } _ => { if let Some(h) = mat_handle { - self.tabs[i].scene.document.header.current_material_handle = - h; + self.tabs[i].scene.document.header.current_material_handle = h; } } } @@ -3137,12 +3121,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { return Task::none(); } match value.as_str() { - "ByBlock" => { - self.tabs[i].scene.document.header.current_plotstyle_type = 1 - } - "Normal" => { - self.tabs[i].scene.document.header.current_plotstyle_type = 2 - } + "ByBlock" => self.tabs[i].scene.document.header.current_plotstyle_type = 1, + "Normal" => self.tabs[i].scene.document.header.current_plotstyle_type = 2, _ => self.tabs[i].scene.document.header.current_plotstyle_type = 0, } self.tabs[i].dirty = true; @@ -3161,8 +3141,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .objects .values() .find_map(|object| { - let acadrust::objects::ObjectType::Layout(layout) = object - else { + let acadrust::objects::ObjectType::Layout(layout) = object else { return None; }; layout @@ -3170,9 +3149,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .eq_ignore_ascii_case(&layout_name) .then(|| layout.plot_style_sheet.clone()) }) - .unwrap_or_else(|| { - self.tabs[i].scene.document.header.stylesheet.clone() - }); + .unwrap_or_else(|| self.tabs[i].scene.document.header.stylesheet.clone()); if current != next { self.push_undo_snapshot(i, "PLOTSTYLE"); for object in self.tabs[i].scene.document.objects.values_mut() { @@ -3198,7 +3175,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .clone(); if !current.eq_ignore_ascii_case(&value) { self.push_undo_snapshot(i, "CANNOSCALE"); - if self.tabs[i].scene.set_annotation_scale_named(&value).is_some() { + if self.tabs[i] + .scene + .set_annotation_scale_named(&value) + .is_some() + { self.tabs[i].dirty = true; } else { self.discard_last_undo_entry(i); @@ -3375,9 +3356,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { "Flat Shaded" => Some(Mode::FlatShaded), "Gouraud Shaded" => Some(Mode::GouraudShaded), "Flat Shaded + Edges" => Some(Mode::FlatShadedWithEdges), - "Gouraud Shaded + Edges" => { - Some(Mode::GouraudShadedWithEdges) - } + "Gouraud Shaded + Edges" => Some(Mode::GouraudShadedWithEdges), _ => None, }; if let Some(mode) = mode { @@ -3415,8 +3394,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { "start_x" | "start_y" | "start_z" => Some(Some(0)), "end_x" | "end_y" | "end_z" => Some(Some(1)), "center_x" | "center_y" | "center_z" => Some(Some(-3)), - "radius" | "diameter" | "circumference" | "area" - | "major_r" | "minor_r" | "ratio" => Some(None), + "radius" | "diameter" | "circumference" | "area" | "major_r" | "minor_r" + | "ratio" => Some(None), _ => None, }; let retain_size = self.constraint_solve_mode @@ -3442,9 +3421,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .iter() .flat_map(|section| section.props.iter()) .find(|property| property.field == field) - .is_some_and(|property| { - matches!(property.value, PropValue::EditText(_)) - }); + .is_some_and(|property| matches!(property.value, PropValue::EditText(_))); if let Some(raw_val) = self.tabs[i] .properties .edit_buf @@ -3464,20 +3441,15 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .filter_map(|handle| { let entity = self.tabs[i].scene.document.get_entity(*handle)?; match (field, entity) { - ( - "current_fit_point", - acadrust::EntityType::Spline(spline), - ) => Some(spline.fit_points.len()), - ( - "current_control_point", - acadrust::EntityType::Spline(spline), - ) => Some( - crate::entities::spline::control_vertex_count(spline), - ), - ( - "pm_current_vertex", - acadrust::EntityType::PolygonMesh(mesh), - ) => Some(mesh.vertices.len()), + ("current_fit_point", acadrust::EntityType::Spline(spline)) => { + Some(spline.fit_points.len()) + } + ("current_control_point", acadrust::EntityType::Spline(spline)) => { + Some(crate::entities::spline::control_vertex_count(spline)) + } + ("pm_current_vertex", acadrust::EntityType::PolygonMesh(mesh)) => { + Some(mesh.vertices.len()) + } _ => None, } }) @@ -3495,7 +3467,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.push_undo_snapshot(i, "CHPROP"); if field == "tbl_style_handle" { - let style_handle = self.tabs[i] + let style_handle = + self.tabs[i] .scene .document .objects @@ -3637,13 +3610,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { ); if applied && field == "dim_text_inside" { if let Some(acadrust::EntityType::Dimension( - acadrust::entities::Dimension::LargeRadial( - dimension, - ), - )) = self.tabs[i] - .scene - .document - .get_entity_mut(handle) + acadrust::entities::Dimension::LargeRadial(dimension), + )) = self.tabs[i].scene.document.get_entity_mut(handle) { dimension.base.text_user_positioned = false; } @@ -3656,9 +3624,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let vals = if val.trim().is_empty() { None } else { - Some(vec![acadrust::xdata::XDataValue::String( - val.clone(), - )]) + Some(vec![acadrust::xdata::XDataValue::String(val.clone())]) }; crate::scene::view::dispatch::set_entity_xdata( &mut self.tabs[i].scene.document, @@ -3681,12 +3647,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { }; let trimmed = val.trim(); if trimmed.is_empty() { - dov::set( - &mut self.tabs[i].scene.document, - handle, - code, - None, - ); + dov::set(&mut self.tabs[i].scene.document, handle, code, None); } else if let Ok(n) = trimmed.parse::() { dov::set( &mut self.tabs[i].scene.document, @@ -3712,29 +3673,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } } _ => { - if crate::scene::model::solid_history::is_primitive_property( - field, - ) { - self.tabs[i].scene.apply_solid_history_property( - handle, - field, - &val, - ); + if crate::scene::model::solid_history::is_primitive_property(field) + { + self.tabs[i] + .scene + .apply_solid_history_property(handle, field, &val); } else if self.tabs[i] .scene - .apply_solid_position_property( - handle, - field, - &val, - plane, - ) + .apply_solid_position_property(handle, field, &val, plane) .is_none() { - let mline_style = self.tabs[i] - .scene - .document - .get_entity(handle) - .and_then(|entity| match entity { + let mline_style = + self.tabs[i].scene.document.get_entity(handle).and_then( + |entity| match entity { acadrust::EntityType::MLine(mline) => { crate::entities::mline::resolved_mline_style( mline, @@ -3743,7 +3694,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .cloned() } _ => None, - }); + }, + ); if matches!(field, "text_x" | "text_y") { crate::entities::dimension::materialize_large_radial_text_position( &mut self.tabs[i].scene.document, @@ -3751,10 +3703,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { &val, ); } - if let Some(entity) = self.tabs[i] - .scene - .document - .get_entity_mut(handle) + if let Some(entity) = + self.tabs[i].scene.document.get_entity_mut(handle) { crate::scene::view::dispatch::apply_geom_prop_in_working_plane( entity, @@ -3774,8 +3724,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } } } - if matches!(field, "srf_u_isolines" | "srf_v_isolines") - { + if matches!(field, "srf_u_isolines" | "srf_v_isolines") { self.tabs[i].scene.reseed_derived_caches(handle); } } @@ -3802,9 +3751,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { ) } None => { - crate::scene::parametric_constraints::ParametricRef::whole( - handle, - ) + crate::scene::parametric_constraints::ParametricRef::whole(handle) } }) }) @@ -3840,7 +3787,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } // Existing target → swap the selected references to it. Anonymous, // xref and xref-dependent definitions are not valid targets. - let target = self.tabs[i].scene.document.block_records.get(new).map(|br| { + let target = self.tabs[i] + .scene + .document + .block_records + .get(new) + .map(|br| { ( br.name.clone(), br.is_anonymous() || br.flags.is_xref || br.name.contains('|'), @@ -3943,9 +3895,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .collect::>(); Ok((ins.block_name.clone(), rows)) } - Some(acadrust::EntityType::Insert(_)) => { - Err("ATTEDIT This block has no attributes.") - } + Some(acadrust::EntityType::Insert(_)) => Err("ATTEDIT This block has no attributes."), _ => Err("ATTEDIT Select a block with attributes."), }; match result { @@ -3999,9 +3949,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // The block's layer may have been locked while the editor was open — // refuse to write attributes to a locked-layer block. if let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) { - self.command_line.push_info(crate::tf!( + self.command_line.push_info( + crate::tf!( "Object is on locked layer \"{layer}\" — unlock the layer to edit its attributes." - ).as_ref()); + ) + .as_ref(), + ); return Task::none(); } // Snapshot the working copy so the document can be mutated while the @@ -4185,7 +4138,13 @@ mod layer_rename_tests { let i = app.active_tab; let old_name = app.tabs[i].layers.edit_buf.clone(); rename_layer(&mut app, &old_name, "TEST"); - let handle = app.tabs[i].scene.document.layers.get("TEST").unwrap().handle; + let handle = app.tabs[i] + .scene + .document + .layers + .get("TEST") + .unwrap() + .handle; rename_layer(&mut app, "TEST", "test"); @@ -4213,7 +4172,10 @@ mod layer_rename_tests { fn current_layer_rename_updates_name_and_allocated_handle() { let mut app = app_with_editing_layer(); let i = app.active_tab; - let idx = app.tabs[i].layers.editing.expect("new layer is being edited"); + let idx = app.tabs[i] + .layers + .editing + .expect("new layer is being edited"); let old_name = app.tabs[i].layers.layers[idx].name.clone(); app.tabs[i] .scene @@ -4227,12 +4189,7 @@ mod layer_rename_tests { rename_layer(&mut app, &old_name, "Renamed"); - let layer = app.tabs[i] - .scene - .document - .layers - .get("Renamed") - .unwrap(); + let layer = app.tabs[i].scene.document.layers.get("Renamed").unwrap(); assert!(layer.handle.is_valid()); assert_eq!(app.tabs[i].active_layer, "Renamed"); assert_eq!(app.tabs[i].layers.current_layer, "Renamed"); @@ -4251,7 +4208,10 @@ mod layer_rename_tests { fn layer_rename_undo_redo_restores_active_layer() { let mut app = app_with_editing_layer(); let i = app.active_tab; - let idx = app.tabs[i].layers.editing.expect("new layer is being edited"); + let idx = app.tabs[i] + .layers + .editing + .expect("new layer is being edited"); let old_name = app.tabs[i].layers.layers[idx].name.clone(); app.tabs[i].layers.selected = Some(idx); let _ = app.on_layer_set_current(); @@ -4277,7 +4237,10 @@ mod layer_rename_tests { let old_name = app.tabs[i].layers.edit_buf.clone(); rename_layer(&mut app, &old_name, "A"); let _ = app.on_layer_new(); - let b_idx = app.tabs[i].layers.editing.expect("new layer is being edited"); + let b_idx = app.tabs[i] + .layers + .editing + .expect("new layer is being edited"); app.tabs[i].layers.selected = Some(b_idx); let _ = app.on_layer_set_current(); let header_name = app.tabs[i].scene.document.header.current_layer_name.clone(); diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 2c214f62e..76884f72f 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -34,12 +34,15 @@ fn set_hatch_graphic_attribute( hatch.gradient_color.enabled = false; } GraphicAttribute::Hatch => { - let Some(entry) = crate::scene::model::hatch_patterns::catalog().iter().find(|entry| { + let Some(entry) = crate::scene::model::hatch_patterns::catalog() + .iter() + .find(|entry| { matches!( entry.gpu, crate::scene::model::hatch_model::HatchPattern::Pattern(_) ) - }) else { + }) + else { return; }; let mut pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry); @@ -49,11 +52,7 @@ fn set_hatch_graphic_attribute( ); crate::entities::hatch::rotate_pattern_geometry(&mut pattern, hatch.pattern_angle); let origin = hatch.pattern_origin(); - crate::entities::hatch::translate_pattern_geometry( - &mut pattern, - origin.x, - origin.y, - ); + crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y); hatch.pattern = pattern; hatch.pattern_type = acadrust::entities::HatchPatternType::Predefined; hatch.is_solid = false; @@ -200,10 +199,16 @@ impl OpenCADStudio { use crate::scene::model::hatch_model::{GradientKind, HatchPattern}; use crate::ui::window::graphic_attributes::GraphicAttribute; self.graphic_attribute_menu_open = false; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; self.solid_fill_color_menu_open = false; self.fill_transparency_menu_open = false; self.gradient_editor = None; self.hatch_editor_handles = None; + self.graphic_hatch_pattern_search.clear(); + self.graphic_hatch_pattern_focus = 0; if value == GraphicAttribute::Varies { return iced::Task::none(); } @@ -220,8 +225,8 @@ impl OpenCADStudio { return iced::Task::none(); } let selected_handles: rustc_hash::FxHashSet<_> = selected.iter().copied().collect(); - let (associated, new_hatches, updated_hatches, skipped) = - if value == GraphicAttribute::None { + let (associated, new_hatches, updated_hatches, skipped) = if value == GraphicAttribute::None + { let associated: Vec<_> = self.tabs[i] .scene .document @@ -294,7 +299,8 @@ impl OpenCADStudio { self.tabs[i].scene.document.get_entity(*handle) { let mut hatch = hatch.clone(); - let visible_color = crate::scene::view::render::render_style_for_common_viewport( + let visible_color = + crate::scene::view::render::render_style_for_common_viewport( &self.tabs[i].scene.document, &hatch.common, None, @@ -402,7 +408,7 @@ impl OpenCADStudio { self.solid_fill_color_menu_open = false; let i = self.active_tab; let selected = self.tabs[i].scene.selected_handles_in_order(); - let handles = crate::ui::window::graphic_attributes::solid_fill_handles( + let handles = crate::ui::window::graphic_attributes::fill_handles( &self.tabs[i].scene.document, &selected, ); @@ -410,7 +416,7 @@ impl OpenCADStudio { .into_iter() .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) .collect(); - self.apply_property_op(i, "Solid fill color", &unlocked, |app, handle| { + self.apply_property_op(i, "Fill color", &unlocked, |app, handle| { if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { crate::scene::view::dispatch::apply_color(entity, color); } @@ -418,6 +424,146 @@ impl OpenCADStudio { iced::Task::none() } + pub(super) fn on_line_color(&mut self, color: acadrust::types::Color) -> iced::Task { + self.line_color_menu_open = false; + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + if selected.is_empty() { + return self.on_ribbon_color_changed(color); + } + let handles = crate::ui::window::graphic_attributes::line_handles( + &self.tabs[i].scene.document, + &selected, + ); + let unlocked: Vec<_> = handles + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + self.apply_property_op(i, "Line color", &unlocked, |app, handle| { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_color(entity, color); + } + }); + iced::Task::none() + } + + pub(super) fn on_line_linetype(&mut self, linetype: String) -> iced::Task { + self.line_linetype_menu_open = false; + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + if selected.is_empty() { + return self.on_ribbon_linetype_changed(linetype); + } + let handles = crate::ui::window::graphic_attributes::line_handles( + &self.tabs[i].scene.document, + &selected, + ); + let unlocked: Vec<_> = handles + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + self.apply_property_op(i, "Line linetype", &unlocked, |app, handle| { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_common_prop(entity, "linetype", &linetype); + } + }); + iced::Task::none() + } + + pub(super) fn on_line_lineweight( + &mut self, + lineweight: acadrust::types::LineWeight, + ) -> iced::Task { + self.line_lineweight_menu_open = false; + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + if selected.is_empty() { + self.tabs[i].scene.document.header.current_line_weight = lineweight.value(); + self.tabs[i].dirty = true; + self.ribbon.active_lineweight = lineweight; + self.refresh_properties(); + return iced::Task::none(); + } + let handles = crate::ui::window::graphic_attributes::line_handles( + &self.tabs[i].scene.document, + &selected, + ); + let unlocked: Vec<_> = handles + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + self.apply_property_op(i, "Line weight", &unlocked, |app, handle| { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_line_weight(entity, lineweight); + } + }); + iced::Task::none() + } + + pub(super) fn on_line_linetype_scale(&mut self, scale: f64) -> iced::Task { + let scale = scale.clamp(0.01, 1000.0); + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + if selected.is_empty() { + self.tabs[i] + .scene + .document + .header + .current_entity_linetype_scale = scale; + self.tabs[i].dirty = true; + self.refresh_properties(); + return iced::Task::none(); + } + let handles = crate::ui::window::graphic_attributes::line_handles( + &self.tabs[i].scene.document, + &selected, + ); + let unlocked: Vec<_> = handles + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + self.apply_property_op(i, "Linetype scale", &unlocked, |app, handle| { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + entity.common_mut().linetype_scale = scale; + } + }); + iced::Task::none() + } + + pub(super) fn on_line_transparency( + &mut self, + transparency: acadrust::types::Transparency, + ) -> iced::Task { + self.line_transparency_menu_open = false; + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + if selected.is_empty() { + if self.tabs[i] + .scene + .document + .set_current_entity_transparency(transparency) + { + self.tabs[i].dirty = true; + self.refresh_properties(); + } + return iced::Task::none(); + } + let handles = crate::ui::window::graphic_attributes::line_handles( + &self.tabs[i].scene.document, + &selected, + ); + let unlocked: Vec<_> = handles + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + self.apply_property_op(i, "Line transparency", &unlocked, |app, handle| { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + entity.common_mut().transparency = transparency; + } + }); + iced::Task::none() + } + pub(super) fn on_fill_transparency( &mut self, transparency: acadrust::types::Transparency, @@ -520,8 +666,11 @@ impl OpenCADStudio { Task::none() } - -pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEvent) -> Task { + pub(super) fn on_ribbon_tool_click( + &mut self, + tool_id: String, + event: ModuleEvent, + ) -> Task { // Commands use `start_allowed`; other events need a drawing // and stay blocked on the Start page (#299, #388, #389). if self.tabs[self.active_tab].is_start && !matches!(event, ModuleEvent::Command(_)) { @@ -563,7 +712,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven let i = self.active_tab; self.tabs[i].scene.clear(); self.tabs[i].properties = PropertiesPanel::empty(); - self.command_line.push_output(crate::t!("Scene cleared.").as_ref()); + self.command_line + .push_output(crate::t!("Scene cleared.").as_ref()); } ModuleEvent::SetVisualStyle(name) => { use crate::modules::view::visual_style; @@ -572,9 +722,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven None => { // Name the styles that do exist, from the same // list every other caller reads. - self.command_line.push_error( - crate::tf!("Unknown visual style \"{name}\".").as_ref(), - ); + self.command_line + .push_error(crate::tf!("Unknown visual style \"{name}\".").as_ref()); self.command_line .push_info(crate::t!(visual_style::keyword_prompt()).as_ref()); } @@ -591,8 +740,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven } => { return Task::perform( async move { - let exts: Vec<&str> = - extensions.iter().map(|s| s.as_str()).collect(); + let exts: Vec<&str> = extensions.iter().map(|s| s.as_str()).collect(); let path = crate::sys::file_dialog() .set_title(title) .add_filter(filter_name, &exts) @@ -622,8 +770,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven let _ = std::fs::remove_file(self.autosave_target(idx)); if self.tabs.len() == 1 { self.tab_counter += 1; - self.tabs[0] = - crate::app::document::DocumentTab::new_drawing(self.tab_counter); + self.tabs[0] = crate::app::document::DocumentTab::new_drawing(self.tab_counter); self.active_tab = 0; self.apply_display_defaults(0); } else { @@ -1892,7 +2039,8 @@ mod tests { acadrust::types::Color::Index(3), ); let _ = app.on_fill_transparency(acadrust::types::Transparency::from_percent(0.35)); - assert!((app.tabs[i] + assert!( + (app.tabs[i] .scene .document .get_entity(selected_hatch) @@ -1902,7 +2050,8 @@ mod tests { .as_percent() - 0.35) .abs() - < 0.01); + < 0.01 + ); let _ = app.on_fill_transparency(acadrust::types::Transparency::BY_LAYER); assert!(app.tabs[i] .scene @@ -1912,6 +2061,170 @@ mod tests { .common() .transparency .is_by_layer()); + + // Line controls ignore HATCH entities, while fill controls ignore + // ordinary geometry in the same mixed selection. + app.tabs[i].scene.select_entity(line, false); + let _ = app.on_line_color(acadrust::types::Color::Index(4)); + assert_eq!( + app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .color, + acadrust::types::Color::Index(4), + ); + assert_eq!( + app.tabs[i] + .scene + .document + .get_entity(selected_hatch) + .expect("solid hatch") + .common() + .color, + acadrust::types::Color::Index(3), + ); + let _ = app.on_solid_fill_color(acadrust::types::Color::Index(5)); + assert_eq!( + app.tabs[i] + .scene + .document + .get_entity(selected_hatch) + .expect("solid hatch") + .common() + .color, + acadrust::types::Color::Index(5), + ); + assert_eq!( + app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .color, + acadrust::types::Color::Index(4), + ); + let _ = app.on_line_linetype("ByBlock".to_string()); + assert_eq!( + app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .linetype, + "ByBlock", + ); + let _ = app.on_line_lineweight(acadrust::types::LineWeight::Value(50)); + assert_eq!( + app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .line_weight, + acadrust::types::LineWeight::Value(50), + ); + assert_ne!( + app.tabs[i] + .scene + .document + .get_entity(selected_hatch) + .expect("solid hatch") + .common() + .line_weight, + acadrust::types::LineWeight::Value(50), + ); + let _ = app.update(Message::LineLinetypeScaleInput("2.5".to_string())); + assert!( + (app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .linetype_scale + - 2.5) + .abs() + < f64::EPSILON + ); + assert!( + (app.tabs[i] + .scene + .document + .get_entity(selected_hatch) + .expect("solid hatch") + .common() + .linetype_scale + - 1.0) + .abs() + < f64::EPSILON + ); + let _ = app.on_line_transparency(acadrust::types::Transparency::from_percent(0.2)); + assert!( + (app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .transparency + .as_percent() + - 0.2) + .abs() + < 0.01 + ); + assert!(app.tabs[i] + .scene + .document + .get_entity(selected_hatch) + .expect("solid hatch") + .common() + .transparency + .is_by_layer()); + let _ = app.on_fill_transparency(acadrust::types::Transparency::from_percent(0.4)); + assert!( + (app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .transparency + .as_percent() + - 0.2) + .abs() + < 0.01 + ); + let pattern_name = crate::scene::model::hatch_patterns::catalog() + .iter() + .find(|entry| { + matches!( + entry.gpu, + crate::scene::model::hatch_model::HatchPattern::Pattern(_) + ) + }) + .expect("pattern hatch") + .name + .clone(); + let _ = app.update(Message::GraphicHatchPatternChanged(pattern_name.clone())); + let selected_pattern = match app.tabs[i] + .scene + .document + .get_entity(selected_hatch) + .expect("pattern hatch") + { + EntityType::Hatch(hatch) => hatch.pattern.name.as_str(), + _ => panic!("fill target is not a hatch"), + }; + assert_eq!(selected_pattern, pattern_name); + app.tabs[i].scene.deselect_all(); + app.tabs[i].scene.select_entity(selected_hatch, false); + let _ = app.on_graphic_attribute(GraphicAttribute::Hatch); assert_eq!( crate::ui::window::graphic_attributes::current( @@ -1948,6 +2261,38 @@ mod tests { assert_eq!(app.tabs[i].history.undo_stack.len(), undo_before + 1); } + #[test] + fn graphic_line_controls_update_creation_defaults_without_a_selection() { + let mut app = fresh(); + let i = app.active_tab; + app.tabs[i].scene.deselect_all(); + + let _ = app.on_line_color(acadrust::types::Color::Index(2)); + let _ = app.on_line_linetype("Continuous".to_string()); + let _ = app.on_line_lineweight(acadrust::types::LineWeight::Value(35)); + let _ = app.on_line_linetype_scale(2.5); + let _ = app.on_line_transparency(acadrust::types::Transparency::from_percent(0.3)); + + let header = &app.tabs[i].scene.document.header; + assert_eq!( + header.current_entity_color, + acadrust::types::Color::Index(2) + ); + assert_eq!(header.current_linetype_name, "Continuous"); + assert_eq!(header.current_line_weight, 35); + assert!((header.current_entity_linetype_scale - 2.5).abs() < f64::EPSILON); + assert!( + (app.tabs[i] + .scene + .document + .current_entity_transparency() + .as_percent() + - 0.3) + .abs() + < 0.01 + ); + } + /// A foreign document: the fresh scene's document plus one model-space LINE. fn foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument { let mut doc = app.tabs[app.active_tab].scene.document.clone(); @@ -2072,12 +2417,11 @@ mod tests { } /// The nested-INSERT's block name inside a defined block record. - fn nested_insert_target( - doc: &acadrust::CadDocument, - block: &str, - ) -> Option { + fn nested_insert_target(doc: &acadrust::CadDocument, block: &str) -> Option { let br = doc.block_records.get(block)?; - br.entity_handles.iter().find_map(|h| match doc.get_entity(*h)? { + br.entity_handles + .iter() + .find_map(|h| match doc.get_entity(*h)? { EntityType::Insert(ins) => Some(ins.block_name.clone()), _ => None, }) diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 4ab4e54eb..681cd30f3 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1180,19 +1180,23 @@ impl OpenCADStudio { Task::none() } - Message::ImageEmbedPick => { - Task::perform(crate::io::pick_embedded_image_file(), Message::ImageEmbedPickResult) - } + Message::ImageEmbedPick => Task::perform( + crate::io::pick_embedded_image_file(), + Message::ImageEmbedPickResult, + ), Message::ImageEmbedPickResult(Ok(image)) => { use crate::command::CadCommand; use crate::modules::draw::draw::raster_image::ImageCommand; - self.command_line.push_output(crate::tf!( + self.command_line.push_output( + crate::tf!( "IMAGEEMBED \"{name}\": {w}×{h} px (embedded)", name = image.name.as_str(), w = image.pixel_width, h = image.pixel_height, - ).as_ref()); + ) + .as_ref(), + ); let cmd = ImageCommand::new_embedded(image); let i = self.active_tab; self.command_line.push_info(&cmd.prompt()); @@ -1279,10 +1283,13 @@ impl OpenCADStudio { .insert(crate::io::font_repo::font_key(name)); } for (name, path) in &pairs { - self.command_line.push_output(crate::tf!( + self.command_line.push_output( + crate::tf!( "FONT Downloaded {name} → {path}", path = path.display() - ).as_ref()); + ) + .as_ref(), + ); } if !unavailable.is_empty() { self.command_line.push_info(crate::tf!( @@ -3720,6 +3727,10 @@ impl OpenCADStudio { Message::ViewportLeftPress => { self.graphic_attribute_menu_open = false; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; self.solid_fill_color_menu_open = false; self.fill_transparency_menu_open = false; let sweep = self.sync_active_field_if_any(); @@ -3730,6 +3741,10 @@ impl OpenCADStudio { Message::ViewportRightPress => { self.graphic_attribute_menu_open = false; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; self.solid_fill_color_menu_open = false; self.fill_transparency_menu_open = false; let i = self.active_tab; @@ -3835,6 +3850,10 @@ impl OpenCADStudio { Message::ViewportMiddlePress => { self.graphic_attribute_menu_open = false; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; self.solid_fill_color_menu_open = false; self.fill_transparency_menu_open = false; self.on_viewport_middle_press() @@ -4993,13 +5012,21 @@ impl OpenCADStudio { } if !state.objects_specify_onscreen && state.selected_handles.is_empty() { state.error_message = Some( - crate::t!("No objects selected. You must select objects to define a block.") + crate::t!( + "No objects selected. You must select objects to define a block." + ) .into_owned(), ); return Task::none(); } let i = self.active_tab; - if self.tabs[i].scene.document.block_records.get(&name).is_some() { + if self.tabs[i] + .scene + .document + .block_records + .get(&name) + .is_some() + { state.confirm_redefine = Some(name); return Task::none(); } @@ -9511,6 +9538,10 @@ impl OpenCADStudio { Message::BlockPalette(m) => self.on_block_palette(m), Message::ToggleGraphicAttributeDropdown => { self.graphic_attribute_menu_open ^= true; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; self.solid_fill_color_menu_open = false; self.fill_transparency_menu_open = false; Task::none() @@ -9520,8 +9551,90 @@ impl OpenCADStudio { Task::none() } Message::GraphicAttributeChanged(value) => self.on_graphic_attribute(value), + Message::ToggleLineColorDropdown => { + self.line_color_menu_open ^= true; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; + self.graphic_attribute_menu_open = false; + self.solid_fill_color_menu_open = false; + self.fill_transparency_menu_open = false; + Task::none() + } + Message::CloseLineColorDropdown => { + self.line_color_menu_open = false; + Task::none() + } + Message::LineColorChanged(color) => self.on_line_color(color), + Message::ToggleLineLinetypeMenu => { + self.line_linetype_menu_open ^= true; + self.line_color_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; + self.graphic_attribute_menu_open = false; + self.solid_fill_color_menu_open = false; + self.fill_transparency_menu_open = false; + Task::none() + } + Message::CloseLineLinetypeMenu => { + self.line_linetype_menu_open = false; + Task::none() + } + Message::LineLinetypeChanged(value) => self.on_line_linetype(value), + Message::ToggleLineLineweightMenu => { + self.line_lineweight_menu_open ^= true; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_transparency_menu_open = false; + self.graphic_attribute_menu_open = false; + self.solid_fill_color_menu_open = false; + self.fill_transparency_menu_open = false; + Task::none() + } + Message::CloseLineLineweightMenu => { + self.line_lineweight_menu_open = false; + Task::none() + } + Message::LineLineweightChanged(value) => self.on_line_lineweight(value), + Message::LineLinetypeScaleChanged(value) => self.on_line_linetype_scale(value), + Message::LineLinetypeScaleInput(value) => { + let value = value.trim().replace(',', "."); + match value.parse::() { + Ok(value) if value.is_finite() && value > 0.0 => { + self.on_line_linetype_scale(value) + } + _ => Task::none(), + } + } + Message::ToggleLineTransparencyDropdown => { + self.line_transparency_menu_open ^= true; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.graphic_attribute_menu_open = false; + self.solid_fill_color_menu_open = false; + self.fill_transparency_menu_open = false; + Task::none() + } + Message::CloseLineTransparencyDropdown => { + self.line_transparency_menu_open = false; + Task::none() + } + Message::LineTransparencyChanged(value) => self.on_line_transparency( + acadrust::types::Transparency::from_percent(f64::from(value) / 100.0), + ), + Message::LineTransparencyByLayer => { + self.on_line_transparency(acadrust::types::Transparency::BY_LAYER) + } + Message::LineTransparencyByBlock => { + self.on_line_transparency(acadrust::types::Transparency::BY_BLOCK) + } Message::ToggleSolidFillColorDropdown => { self.solid_fill_color_menu_open ^= true; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; self.graphic_attribute_menu_open = false; self.fill_transparency_menu_open = false; Task::none() @@ -9533,6 +9646,10 @@ impl OpenCADStudio { Message::SolidFillColorChanged(color) => self.on_solid_fill_color(color), Message::ToggleFillTransparencyDropdown => { self.fill_transparency_menu_open ^= true; + self.line_color_menu_open = false; + self.line_linetype_menu_open = false; + self.line_lineweight_menu_open = false; + self.line_transparency_menu_open = false; self.graphic_attribute_menu_open = false; self.solid_fill_color_menu_open = false; Task::none() @@ -9552,17 +9669,78 @@ impl OpenCADStudio { } Message::HatchEditorOpen => { self.gradient_editor = None; - self.hatch_editor_handles = Some( - self.tabs[self.active_tab] - .scene - .selected_handles_in_order(), + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + let handles = crate::ui::window::graphic_attributes::fill_handles( + &self.tabs[i].scene.document, + &selected, ); - Task::none() + let current = handles.iter().find_map(|handle| { + let acadrust::EntityType::Hatch(hatch) = + self.tabs[i].scene.document.get_entity(*handle)? + else { + return None; + }; + Some(hatch.pattern.name.as_str()) + }); + self.graphic_hatch_pattern_search.clear(); + self.graphic_hatch_pattern_focus = current + .and_then(|current| { + crate::ui::properties::filtered_hatch_patterns("") + .iter() + .position(|entry| entry.name.eq_ignore_ascii_case(current)) + }) + .unwrap_or(0); + self.hatch_editor_handles = Some(selected); + iced::widget::operation::focus(iced::widget::Id::new( + "graphic-hatch-pattern-search", + )) } Message::HatchEditorClose => { self.hatch_editor_handles = None; + self.graphic_hatch_pattern_search.clear(); + self.graphic_hatch_pattern_focus = 0; Task::none() } + Message::GraphicHatchPatternSearchChanged(search) => { + self.graphic_hatch_pattern_search = search; + self.graphic_hatch_pattern_focus = 0; + Task::none() + } + Message::GraphicHatchPatternFocus(index) => { + let len = crate::ui::properties::filtered_hatch_patterns( + &self.graphic_hatch_pattern_search, + ) + .len(); + if index < len { + self.graphic_hatch_pattern_focus = index; + } + Task::none() + } + Message::GraphicHatchPatternChanged(name) => { + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + let handles = crate::ui::window::graphic_attributes::fill_handles( + &self.tabs[i].scene.document, + &selected, + ); + self.hatch_editor_handles = None; + self.graphic_hatch_pattern_search.clear(); + self.graphic_hatch_pattern_focus = 0; + self.apply_hatch_pattern(i, &handles, name) + } + Message::GraphicHatchPatternConfirm => { + let name = crate::ui::properties::filtered_hatch_patterns( + &self.graphic_hatch_pattern_search, + ) + .get(self.graphic_hatch_pattern_focus) + .map(|entry| entry.name.clone()); + if let Some(name) = name { + self.update(Message::GraphicHatchPatternChanged(name)) + } else { + Task::none() + } + } Message::GradientEditorOpen => { self.hatch_editor_handles = None; self.open_gradient_editor(); @@ -10777,8 +10955,14 @@ impl OpenCADStudio { Task::none() } Message::OpenColorWindow(target, color) => { - if matches!(target, crate::app::ColorPickTarget::GraphicAttributesSolid) { + match target { + crate::app::ColorPickTarget::GraphicAttributesLine => { + self.line_color_menu_open = false; + } + crate::app::ColorPickTarget::GraphicAttributesSolid => { self.solid_fill_color_menu_open = false; + } + _ => {} } self.color_pick_target = Some((target, color)); diff --git a/src/app/update/style.rs b/src/app/update/style.rs index 9d5adf177..94e81be06 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -823,7 +823,8 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { Task::none() } - pub(super) fn on_mleader_style_edit(&mut self, field: &'static str, value: String) -> Task { + pub(super) fn on_mleader_style_edit( + &mut self, field: &'static str, value: String,) -> Task { self.mls_color_open = None; match field { "landing_distance" => self.mls_landing_distance = value, @@ -850,7 +851,8 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { Task::none() } - pub(super) fn on_mleader_style_set_enum(&mut self, field: &'static str, value: String) -> Task { + pub(super) fn on_mleader_style_set_enum( + &mut self, field: &'static str, value: String,) -> Task { use acadrust::objects::{ BlockContentConnectionType, LeaderContentType, LeaderDrawOrderType, MultiLeaderDrawOrderType, MultiLeaderPathType, TextAlignmentType, @@ -870,9 +872,8 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { "BottomOfTopLineUnderlineTopLine" => { TextAttachmentType::BottomOfTopLineUnderlineTopLine } - "BottomOfTopLineUnderlineAll" => { - TextAttachmentType::BottomOfTopLineUnderlineAll - } + "BottomOfTopLineUnderlineAll" => + TextAttachmentType::BottomOfTopLineUnderlineAll, "CenterOfText" => TextAttachmentType::CenterOfText, "CenterOfTextOverline" => TextAttachmentType::CenterOfTextOverline, _ => TextAttachmentType::MiddleOfTopLine, @@ -898,9 +899,8 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { } "text_angle_type" => { s.text_angle_type = match value.as_str() { - "ParallelToLastLeaderLine" => { - TextAngleType::ParallelToLastLeaderLine - } + "ParallelToLastLeaderLine" => + TextAngleType::ParallelToLastLeaderLine, "Optimized" => TextAngleType::Optimized, _ => TextAngleType::Horizontal, }; @@ -946,7 +946,8 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { Task::none() } - pub(super) fn on_mleader_style_set_handle(&mut self, field: &'static str, value: String) -> Task { + pub(super) fn on_mleader_style_set_handle( + &mut self, field: &'static str, value: String,) -> Task { let i = self.active_tab; let doc = &self.tabs[i].scene.document; let handle: Option = if value == "None" @@ -1136,35 +1137,34 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { Some(crate::app::ColorPickTarget::Gradient(index)) => { Some(Message::GradientColorChanged(index, color)) } - Some(crate::app::ColorPickTarget::GraphicAttributesSolid) => { + Some(crate::app::ColorPickTarget::GraphicAttributesLine) => { + Some(Message::LineColorChanged(color)) + } + Some(crate::app::ColorPickTarget::GraphicAttributesSolid) => { Some(Message::SolidFillColorChanged(color)) } Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)), Some(crate::app::ColorPickTarget::MLeader(f)) => { Some(Message::MLeaderStyleEdit { field: f, value: s }) } - Some(crate::app::ColorPickTarget::Table(r, f)) => { + Some(crate::app::ColorPickTarget::Table(r, f)) => Some(Message::TableStyleCellEdit { row: r, field: f, value: s, - }) - } - Some(crate::app::ColorPickTarget::Properties) => { - Some(Message::PropColorChanged(color)) - } + }), + Some(crate::app::ColorPickTarget::Properties) => + Some(Message::PropColorChanged(color)), Some(crate::app::ColorPickTarget::PropertiesBg) => { Some(Message::PropBgColorChanged(color)) } Some(crate::app::ColorPickTarget::PropertiesField(field)) => { Some(Message::PropColorFieldChanged { field, color }) } - Some(crate::app::ColorPickTarget::MText) => { - Some(Message::MTextColorChanged(color)) - } - Some(crate::app::ColorPickTarget::Ribbon) => { - Some(Message::RibbonColorChanged(color)) - } + Some(crate::app::ColorPickTarget::MText) => + Some(Message::MTextColorChanged(color)), + Some(crate::app::ColorPickTarget::Ribbon) => + Some(Message::RibbonColorChanged(color)), Some(crate::app::ColorPickTarget::Layer(idx)) => { self.tabs[self.active_tab].layers.selected = Some(idx); Some(Message::LayerColorSet(color)) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index ae9944d23..3cfddacae 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2943,6 +2943,7 @@ impl OpenCADStudio { let draw_depth = tab.scene.draw_depth_map(); crate::ui::window::graphic_attributes::view( &tab.scene.document, + &tab.properties, &tab.scene .selected_entities() .into_iter() @@ -2953,9 +2954,15 @@ impl OpenCADStudio { auto_collapse, side, self.graphic_attribute_menu_open, + self.line_color_menu_open, + self.line_linetype_menu_open, + self.line_lineweight_menu_open, + self.line_transparency_menu_open, self.solid_fill_color_menu_open, self.fill_transparency_menu_open, self.hatch_editor_handles.is_some(), + &self.graphic_hatch_pattern_search, + self.graphic_hatch_pattern_focus, self.gradient_editor.as_ref(), matches!( self.color_pick_target.as_ref().map(|(target, _)| target), diff --git a/src/scene/entity.rs b/src/scene/entity.rs index a43d3909c..c43ba3cd9 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1269,10 +1269,12 @@ impl Scene { style.0 = crate::scene::view::render::adapt_to_bg(style.0, hatch_bg); m.aci = style.4; m.line_weight_px = style.3; - // A gradient's colour is its first stop (already baked into - // the cached model); only solid / pattern fills take the - // entity's resolved colour. - if !matches!(m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) { + // Gradient RGB comes from its own colour stops, while + // transparency remains an entity property shared by both + // stops. Solid/pattern fills take the complete style. + if matches!(m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) { + m.set_alpha(style.0[3]); + } else { m.color = style.0; } if let EntityType::Hatch(dxf) = e { @@ -2055,7 +2057,14 @@ impl Scene { .colors .get(i) .and_then(|e| e.color.rgb()) - .map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]) + .map(|(r, g, b)| { + [ + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + color[3], + ] + }) }; gradient_color1 = stop(0); let color1 = gradient_color1.unwrap_or(color); diff --git a/src/scene/hatch_boundary/tests.rs b/src/scene/hatch_boundary/tests.rs index e6362d232..0210d6489 100644 --- a/src/scene/hatch_boundary/tests.rs +++ b/src/scene/hatch_boundary/tests.rs @@ -4,6 +4,43 @@ use acadrust::types::{Vector2, Vector3}; use cadkernel::geom2d::{refine_spline_boundary, triangulate_rings, Curve, NurbsCurve}; use cadkernel::tessellation::DEFAULT_ANGLE; +#[test] +fn gradient_stops_keep_entity_transparency() { + let mut hatch = Hatch::new(); + hatch.is_solid = true; + hatch + .paths + .push(rectangle([0.0, 0.0], [10.0, 10.0], [0.0; 2])); + hatch.gradient_color.enabled = true; + hatch.gradient_color.name = "LINEAR".into(); + hatch.gradient_color.colors = vec![ + acadrust::entities::hatch::GradientColorEntry { + value: 0.0, + color: acadrust::types::Color::Rgb { r: 255, g: 0, b: 0 }, + }, + acadrust::entities::hatch::GradientColorEntry { + value: 1.0, + color: acadrust::types::Color::Rgb { r: 0, g: 0, b: 255 }, + }, + ]; + + let mut model = Scene::hatch_model_from_dxf(&hatch, [0.2, 0.3, 0.4, 0.35]).unwrap(); + assert!((model.color[3] - 0.35).abs() < f32::EPSILON); + let crate::scene::model::hatch_model::HatchPattern::Gradient { color2, .. } = &model.pattern + else { + panic!("gradient hatch must produce a gradient render pattern"); + }; + assert!((color2[3] - 0.35).abs() < f32::EPSILON); + + model.set_alpha(0.6); + assert!((model.color[3] - 0.6).abs() < f32::EPSILON); + let crate::scene::model::hatch_model::HatchPattern::Gradient { color2, .. } = &model.pattern + else { + unreachable!(); + }; + assert!((color2[3] - 0.6).abs() < f32::EPSILON); +} + fn narrow(clearance: f64, offset: [f64; 2]) -> NurbsCurve { let line = |a: [f64; 2], b: [f64; 2]| { [ @@ -184,8 +221,7 @@ fn valid_samples_are_unchanged_and_invalid_samples_stay_rejected() { |p| p ) .is_none()); - assert!(refine_spline_boundary( - &vec![[0.0; 2]; 513], + assert!(refine_spline_boundary(&vec![[0.0; 2]; 513], || panic!("size cap ignored"), |p| p ) diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 8165bcdbd..56e300ced 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -272,6 +272,18 @@ pub struct HatchModel { pub draw_depth: f32, } +impl HatchModel { + /// Apply an entity's resolved opacity to every colour stop. Gradient stop + /// colours come from the HATCH payload, but transparency remains a common + /// entity property and must affect both ends equally. + pub(crate) fn set_alpha(&mut self, alpha: f32) { + self.color[3] = alpha; + if let HatchPattern::Gradient { color2, .. } = &mut self.pattern { + color2[3] = alpha; + } + } +} + impl HatchModel { pub(crate) fn gradient_frame( &self, @@ -350,9 +362,8 @@ impl HatchModel { self.pattern_segments_with_dot_length(Some(0.0)) } - fn pattern_segments_with_dot_length( - &self, - plot_dot_length: Option, + fn pattern_segments_with_dot_length(&self, + plot_dot_length: Option ) -> Vec<[[f64; 2]; 2]> { let HatchPattern::Pattern(families) = &self.pattern else { return Vec::new(); diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 43f21a996..4cfecd2d5 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -2,8 +2,7 @@ use super::*; impl Scene { - pub(super) fn paper_viewport_handles( - &self, + pub(super) fn paper_viewport_handles(&self ) -> (Handle, Handle, Arc>) { { let cache = self.paper_viewport_cache.borrow(); @@ -12,7 +11,7 @@ impl Scene { return ( cache.layout_block, cache.sheet, - Arc::clone(&cache.content), + Arc::clone(&cache.content) ); } } @@ -123,7 +122,8 @@ impl Scene { ) -> Vec { if self.current_layout == "Model" { let tiles = self.model_tiles.borrow(); - let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); + let active = self.active_model_tile.get() + .min(tiles.len().saturating_sub(1)); return tiles .iter() .enumerate() @@ -298,9 +298,7 @@ impl Scene { hatches.push(sheet); } hatches.extend( - self.paper_canvas_hatches(tint_selected) - .iter() - .cloned(), + self.paper_canvas_hatches(tint_selected).iter().cloned() ); let hatches = Arc::new(hatches); let wipeouts = self.paper_canvas_wipeouts(); @@ -377,9 +375,8 @@ impl Scene { /// Physical sheet bounds in canvas pixels. Uses the same forced top-down /// paper transform as the GPU sheet viewport, ignoring stored camera twist. - pub fn paper_sheet_screen_rect( - &self, - canvas_px: (f32, f32), + pub fn paper_sheet_screen_rect(&self, + canvas_px: (f32, f32) ) -> Option { let ((x0, y0), (x1, y1)) = self.paper_limits()?; let (canvas_w, canvas_h) = canvas_px; @@ -477,13 +474,17 @@ impl Scene { ) .is_some() => { - Self::hatch_model_from_dxf(dxf, model.color) - .unwrap_or_else(|| model.clone()) + Self::hatch_model_from_dxf(dxf, model.color).unwrap_or_else(|| model.clone()) } _ => model.clone(), }; let style = self.render_style(entity); - m.color = style.0; + if matches!( + m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) { + m.set_alpha(style.0[3]); + } else { + m.color = style.0; + } m.aci = style.4; m.line_weight_px = style.3; if let EntityType::Hatch(dxf) = entity { @@ -600,13 +601,19 @@ impl Scene { ) .is_some() => { - Self::hatch_model_from_dxf(dxf, model.color) - .unwrap_or_else(|| model.clone()) + Self::hatch_model_from_dxf(dxf, model.color).unwrap_or_else(|| model.clone()) } _ => model.clone(), }; let style = self.render_style(entity); - hatch.color = style.0; + if matches!( + hatch.pattern, + model::hatch_model::HatchPattern::Gradient { .. } + ) { + hatch.set_alpha(style.0[3]); + } else { + hatch.color = style.0; + } hatch.aci = style.4; hatch.line_weight_px = style.3; if let EntityType::Hatch(dxf) = entity { @@ -715,9 +722,9 @@ impl Scene { // from a big symbol library) has viewports legitimately aimed at that // second cluster; testing only the dense one wrongly auto-fits them onto // the library. Fall back to the cluster box when no extents are known. - let full_bounds = self.model_space_extents().map(|(mn, mx)| { - (mn.x as f64, mn.y as f64, mx.x as f64, mx.y as f64) - }); + let full_bounds = self.model_space_extents() + .map(|(mn, mx)| + (mn.x as f64, mn.y as f64, mx.x as f64, mx.y as f64)); // Absolute drawing centre. Geometry now reaches the scene at absolute // (UTM) coordinates — the old code centred the overlap test and the // auto-fit on the origin, which was right only while world_offset @@ -734,9 +741,8 @@ impl Scene { (self.local_center[0], self.local_center[1]) } else { self.model_space_extents() - .map(|(mn, mx)| { - (((mn.x + mx.x) * 0.5) as f64, ((mn.y + mx.y) * 0.5) as f64) - }) + .map(|(mn, mx)| + (((mn.x + mx.x) * 0.5) as f64, ((mn.y + mx.y) * 0.5) as f64)) .unwrap_or((0.0, 0.0)) }; diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 20a5cec9e..7e95a2880 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -10,6 +10,7 @@ use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::{fmt, sync::Arc}; +use crate::t; use crate::ui::ROW_H; use acadrust::types::{Color as AcadColor, LineWeight}; use acadrust::Handle; @@ -20,7 +21,6 @@ use iced::widget::{ use iced::{ mouse, Background, Border, Color, Element, Length, Padding, Point, Rectangle, Size, Theme, }; -use crate::t; // ── Row-height-derived constants ───────────────────────────────────────── const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px @@ -234,7 +234,8 @@ fn hatch_pattern_matches( let query = search.trim(); query.is_empty() || entry.name.to_lowercase().contains(&query.to_lowercase()) - || entry.description.to_lowercase().contains(&query.to_lowercase()) + || entry.description.to_lowercase() + .contains(&query.to_lowercase()) } pub(crate) fn filtered_hatch_patterns( @@ -246,6 +247,100 @@ pub(crate) fn filtered_hatch_patterns( .collect() } +pub(crate) fn hatch_pattern_picker_content<'a>( + search_value: &'a str, + focus: usize, + current: &'a str, + search_id: &'static str, + on_search: fn(String) -> Message, + on_confirm: Message, + on_focus: fn(usize) -> Message, + on_changed: fn(String) -> Message, +) -> Element<'a, Message> { + let search = text_input(t!("Search patterns…").as_ref(), search_value) + .id(iced::widget::Id::new(search_id)) + .on_input(on_search) + .on_submit(on_confirm) + .size(FONT_SZ) + .padding([5, 7]) + .width(Length::Fill); + + let mut grid = column![].spacing(6); + let visible = filtered_hatch_patterns(search_value); + for (row_index, pair) in visible.chunks(2).enumerate() { + let mut cards = row![].spacing(6); + for (column_index, entry) in pair.iter().enumerate() { + let index = row_index * 2 + column_index; + let selected = current.eq_ignore_ascii_case(&entry.name); + let focused = focus == index; + let name = entry.name.clone(); + let preview = hatch_pattern_preview(entry.gpu.clone(), PATTERN_PREVIEW_H); + let card = button( + column![ + preview, + container(text(crate::ui::text_util::elide(&entry.name, 20)).size(FONT_SZ)) + .width(Length::Fill) + .align_x(iced::Center), + ] + .spacing(3), + ) + .on_press(on_changed(name)) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed); + let pair = if selected { + palette.primary.weak + } else if hovered || focused { + palette.background.strong + } else { + palette.background.weak + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border { + color: if selected || focused { + palette.primary.base.color + } else { + palette.background.neutral.color + }, + width: if selected || focused { 2.0 } else { 1.0 }, + radius: 4.0.into(), + }, + ..Default::default() + } + }) + .padding(5) + .width(PATTERN_CARD_W); + cards = cards.push(mouse_area(card).on_enter(on_focus(index))); + } + grid = grid.push(cards); + } + + let results: Element<'_, Message> = if visible.is_empty() { + container( + text(t!("No matching patterns")) + .size(FONT_SZ) + .style(hint_text_style), + ) + .padding(12) + .width(Length::Fill) + .center_x(Length::Fill) + .into() + } else { + scrollable(grid) + .height(Length::Fill) + .width(Length::Fill) + .into() + }; + container(column![search, results].spacing(7)) + .style(container::bordered_box) + .padding(8) + .width(PATTERN_PICKER_W) + .height(Length::Fixed(PATTERN_PICKER_H)) + .into() +} + /// All standard CAD lineweight options for the combobox. pub fn lw_options() -> Vec { [ @@ -314,7 +409,7 @@ impl FieldKey { } /// Text-input widget id for one column of a named-parameter row. -pub fn prop_param_field_id(index: usize, field: crate::ui::window::named_parameters::ParamField) -> iced::widget::Id { +pub fn prop_param_field_id(index: usize, field: crate::ui::window::named_parameters::ParamField,) -> iced::widget::Id { use crate::ui::window::named_parameters::ParamField; let suffix = match field { ParamField::Name => "name", @@ -363,7 +458,7 @@ pub fn active_key_focused( /// its field key in O(1) instead of re-scanning the sections, enabling /// select-all-on-focus for both text inputs and edit-choices (e.g. transparency). pub fn build_field_key_map( - sections: &[PropSection], + sections: &[PropSection] ) -> HashMap { let mut map = HashMap::default(); for section in sections { @@ -577,7 +672,7 @@ impl PropertiesPanel { crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0) }; let pin = button(pin_icon) - .on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Properties))) + .on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Properties,))) .style(move |theme: &Theme, status| { let mut style = button::subtle(theme, status); if auto_collapse { @@ -602,9 +697,7 @@ impl PropertiesPanel { let close = tooltip( close, text(t!("Close")).size(10), - tooltip::Position::Bottom, - ) - .gap(4); + tooltip::Position::Bottom).gap(4); let header = mouse_area( container( @@ -619,7 +712,7 @@ impl PropertiesPanel { ) .style(|theme: &Theme| container::Style { background: Some(Background::Color( - theme.palette().background.weak.color, + theme.palette().background.weak.color )), ..Default::default() }) @@ -786,9 +879,7 @@ impl PropertiesPanel { let hdr = container( row![ title, - toggle, - ] - .align_y(iced::Center), + toggle,].align_y(iced::Center) ) .style(|theme: &Theme| { let palette = theme.palette(); @@ -852,9 +943,8 @@ impl PropertiesPanel { label: &'a str, ) -> Element<'a, Message> { match &prop.value { - PropValue::ColorChoice(color) => { - self.render_color_row(label, prop.field, *color, None) - } + PropValue::ColorChoice(color) => + self.render_color_row(label, prop.field, *color, None), PropValue::NamedColorChoice { color, name } => { self.render_color_row(label, prop.field, *color, Some(name)) } @@ -864,9 +954,8 @@ impl PropertiesPanel { PropValue::FieldLwChoice { field, value } => { self.render_field_lw_row(label, field, *value) } - PropValue::FieldLwVaries { field } => { - self.render_field_lw_varies_row(label, field) - } + PropValue::FieldLwVaries { field } => + self.render_field_lw_varies_row(label, field), PropValue::LwVaries => self.render_lw_varies_row(label), PropValue::LinetypeChoice(lt) => self.render_linetype_row(label, lt), PropValue::Choice { selected, options } => { @@ -888,16 +977,13 @@ impl PropertiesPanel { PropValue::ReadOnlyWithTooltip { value, tooltip } => { render_ro_with_tooltip_row(label, value, tooltip) } - PropValue::HatchPatternChoice(current) => { - self.render_hatch_pattern_row(label, current) - } + PropValue::HatchPatternChoice(current) => + self.render_hatch_pattern_row(label, current), PropValue::AttrText { tag, value } => self.render_attr_row(tag, value), - PropValue::EntityLink { id, handles, conflicting } => { - render_entity_link_row(label, *id, handles.clone(), *conflicting) - } - PropValue::ParamRow { index, name, formula, resolved } => { - self.render_param_row(*index, name, formula, resolved) - } + PropValue::EntityLink { id, handles, conflicting, } => + render_entity_link_row(label, *id, handles.clone(), *conflicting), + PropValue::ParamRow { index, name, formula, resolved, } => + self.render_param_row(*index, name, formula, resolved), PropValue::ParamAddRow => render_param_add_row(), PropValue::ParamsVisibilityToggle(value) => render_params_visibility_toggle_row(*value), } @@ -958,7 +1044,7 @@ impl PropertiesPanel { // (#415). Message::OpenColorWindow( crate::app::ColorPickTarget::PropertiesBg, - color, + color ), ); return prop_row_widget(label, selector); @@ -1034,7 +1120,7 @@ impl PropertiesPanel { Message::PropColorPickerToggle, Message::OpenColorWindow( crate::app::ColorPickTarget::Properties, - color, + color ), ); prop_row_widget(label, selector) @@ -1052,7 +1138,7 @@ impl PropertiesPanel { Message::PropColorPickerToggle, Message::OpenColorWindow( crate::app::ColorPickTarget::Properties, - AcadColor::ByLayer, + AcadColor::ByLayer ), ); prop_row_widget(label, selector) @@ -1442,24 +1528,28 @@ impl PropertiesPanel { use crate::ui::window::named_parameters::ParamField; let name_key = FieldKey::Param(index, ParamField::Name); - let name_display = self.edit_buf.get(&name_key).map(|s| s.as_str()).unwrap_or(name); + let name_display = self.edit_buf.get(&name_key) + .map(|s| s.as_str()) + .unwrap_or(name); let name_active = self.active_field.as_ref() == Some(&name_key); let name_input = text_input("name", name_display) .id(name_key.widget_id()) - .on_input(move |v| Message::PropParamInput { index, field: ParamField::Name, value: v }) - .on_submit(Message::PropParamCommit { index, field: ParamField::Name }) + .on_input(move |v| Message::PropParamInput { index, field: ParamField::Name, value: v, }) + .on_submit(Message::PropParamCommit { index, field: ParamField::Name, }) .size(FONT_SZ) .style(text_input_style) .padding([3, 6]) .width(Length::FillPortion(3)); let formula_key = FieldKey::Param(index, ParamField::Formula); - let formula_display = self.edit_buf.get(&formula_key).map(|s| s.as_str()).unwrap_or(formula); + let formula_display = self.edit_buf.get(&formula_key) + .map(|s| s.as_str()) + .unwrap_or(formula); let formula_active = self.active_field.as_ref() == Some(&formula_key); let formula_input = text_input("formula", formula_display) .id(formula_key.widget_id()) - .on_input(move |v| Message::PropParamInput { index, field: ParamField::Formula, value: v }) - .on_submit(Message::PropParamCommit { index, field: ParamField::Formula }) + .on_input(move |v| Message::PropParamInput { index, field: ParamField::Formula, value: v, }) + .on_submit(Message::PropParamCommit { index, field: ParamField::Formula, }) .size(FONT_SZ) .style(text_input_style) .padding([3, 6]) @@ -1470,9 +1560,10 @@ impl PropertiesPanel { Err(_) => ("—".to_string(), true), }; let value_label = container( - text(value_text).size(FONT_SZ).style(move |theme: &Theme| iced::widget::text::Style { + text(value_text).size(FONT_SZ).style(move |theme: &Theme| { iced::widget::text::Style { color: is_error.then_some(theme.palette().danger.base.color), - }), + } + }) ) .width(Length::FillPortion(2)) .align_x(iced::Right); @@ -1490,13 +1581,15 @@ impl PropertiesPanel { Background::Color(theme.palette().background.base.color) } }; - let content = container(row![name_input, formula_input, value_label, delete_btn].spacing(4).align_y(iced::Center)) + let content = container(row![name_input, formula_input, value_label, delete_btn] + .spacing(4) + .align_y(iced::Center),) .style(move |theme: &Theme| container::Style { background: Some(bg(theme)), ..Default::default() }) .padding([2, 6]) .width(Length::Fill); if let Err(err) = resolved { - tooltip(content, text(err.as_str()).size(FONT_SZ), tooltip::Position::Top) + tooltip(content, text(err.as_str()).size(FONT_SZ), tooltip::Position::Top,) .gap(4.0) .padding(6.0) .style(|theme: &Theme| { @@ -1504,7 +1597,7 @@ impl PropertiesPanel { container::Style { background: Some(Background::Color(palette.danger.weak.color)), text_color: Some(palette.danger.weak.text), - border: Border { color: palette.danger.base.color, width: 1.0, radius: 4.0.into() }, + border: Border { color: palette.danger.base.color, width: 1.0, radius: 4.0.into(), }, ..Default::default() } }) @@ -1562,96 +1655,16 @@ impl PropertiesPanel { return prop_row_widget(label, head.into()); } - let search = text_input(t!("Search patterns…").as_ref(), &self.hatch_pattern_search) - .id(iced::widget::Id::new("hatch-pattern-search")) - .on_input(Message::PropHatchPatternSearchChanged) - .on_submit(Message::PropHatchPatternConfirm) - .size(FONT_SZ) - .padding([5, 7]) - .width(Length::Fill); - - let mut grid = column![].spacing(6); - let visible = filtered_hatch_patterns(&self.hatch_pattern_search); - for (row_index, pair) in visible.chunks(2).enumerate() { - let mut cards = row![].spacing(6); - for (column_index, entry) in pair.iter().enumerate() { - let index = row_index * 2 + column_index; - let selected = current.eq_ignore_ascii_case(&entry.name); - let focused = self.hatch_pattern_focus == index; - let name = entry.name.clone(); - let preview = hatch_pattern_preview(entry.gpu.clone(), PATTERN_PREVIEW_H); - let card = button( - column![ - preview, - container(text(crate::ui::text_util::elide(&entry.name, 20)).size(FONT_SZ)) - .width(Length::Fill) - .align_x(iced::Center), - ] - .spacing(3), - ) - .on_press(Message::PropHatchPatternChanged(name)) - .style(move |theme: &Theme, status| { - let palette = theme.palette(); - let hovered = - matches!(status, button::Status::Hovered | button::Status::Pressed); - let pair = if selected { - palette.primary.weak - } else if hovered || focused { - palette.background.strong - } else { - palette.background.weak - }; - button::Style { - background: Some(Background::Color(pair.color)), - text_color: pair.text, - border: Border { - color: if selected || focused { - palette.primary.base.color - } else { - palette.background.neutral.color - }, - width: if selected || focused { 2.0 } else { 1.0 }, - radius: 4.0.into(), - }, - ..Default::default() - } - }) - .padding(5) - .width(PATTERN_CARD_W); - cards = cards.push( - mouse_area(card).on_enter(Message::PropHatchPatternFocus(index)), - ); - } - grid = grid.push(cards); - } - - let results: Element<'_, Message> = if visible.is_empty() { - container( - text(t!("No matching patterns")) - .size(FONT_SZ) - .style(hint_text_style), - ) - .padding(12) - .width(Length::Fill) - .center_x(Length::Fill) - .into() - } else { - scrollable(grid) - .height(Length::Fill) - .width(Length::Fill) - .into() - }; - let popup = container(column![search, results].spacing(7)) - .style(container::bordered_box) - .padding(8) - .width(PATTERN_PICKER_W) - .height(Length::Fixed(PATTERN_PICKER_H)); + let popup = hatch_pattern_picker_content( &self.hatch_pattern_search, + self.hatch_pattern_focus, + current,"hatch-pattern-search",Message::PropHatchPatternSearchChanged,Message::PropHatchPatternConfirm, + Message::PropHatchPatternFocus, + Message::PropHatchPatternChanged, + ); prop_row_widget( - label, - crate::ui::color_select::drop_down_below( - head.into(), - popup.into(), + label,crate::ui::color_select::drop_down_below( + head.into(), popup, Some(Length::Fixed(PATTERN_PICKER_W)), Length::Fixed(PATTERN_PICKER_H), Message::PropHatchPatternPickerToggle(current.to_string()), @@ -1707,11 +1720,10 @@ fn render_bool_row<'a>(label: &'a str, field: &'static str, value: bool) -> Elem }; let btn = button( - text(btn_label) - .size(FONT_SZ) - .style(move |theme: &Theme| iced::widget::text::Style { + text(btn_label).size(FONT_SZ).style(move |theme: &Theme| { iced::widget::text::Style { color: value.then_some(theme.palette().warning.base.color), - }), + } + }) ) .on_press(Message::PropBoolToggle(field)) .style(move |theme: &Theme, status| { @@ -1854,7 +1866,7 @@ fn render_group_row( let label_col = container(label_btn) .style(|theme: &Theme| container::Style { background: Some(Background::Color( - theme.palette().background.weakest.color, + theme.palette().background.weakest.color )), ..Default::default() }) @@ -1867,7 +1879,7 @@ fn render_group_row( let value_col = container(value_field) .style(|theme: &Theme| container::Style { background: Some(Background::Color( - theme.palette().background.base.color, + theme.palette().background.base.color )), ..Default::default() }) @@ -1895,7 +1907,7 @@ fn render_group_row( } fn render_annotative_scale_row<'a>( label: &'a str, - value: &'a str, + value: &'a str ) -> Element<'a, Message> { let field = crate::ui::read_only::field(value, FONT_SZ, Length::Fill); @@ -1941,7 +1953,7 @@ fn render_ro_with_tooltip_row<'a>( tooltip_text: &'a str, ) -> Element<'a, Message> { let field = crate::ui::read_only::field(value, FONT_SZ, Length::Fill); - let wrapped = tooltip(field, text(tooltip_text).size(FONT_SZ), tooltip::Position::Top) + let wrapped = tooltip(field, text(tooltip_text).size(FONT_SZ), tooltip::Position::Top,) .gap(4.0) .padding(6.0) .style(|theme: &Theme| { @@ -1989,7 +2001,7 @@ fn render_entity_link_row<'a>( }; button::Style { background: Some(Background::Color(pair.color)), - border: Border { color: palette.background.neutral.color, width: 1.0, radius: 2.0.into() }, + border: Border { color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, text_color: pair.text, ..Default::default() } @@ -2012,7 +2024,10 @@ fn render_entity_link_row<'a>( /// — avoids a separate "pending new row" concept, since every row always /// reflects a real committed table entry. fn render_param_add_row<'a>() -> Element<'a, Message> { - let btn = button(row![text("+").size(FONT_SZ), text(t!("Add parameter").into_owned()).size(FONT_SZ)].spacing(6).align_y(iced::Center)) + let btn = button(row![text("+").size(FONT_SZ), text(t!("Add parameter").into_owned()).size(FONT_SZ) + ] + .spacing(6) + .align_y(iced::Center),) .on_press(Message::PropParamAddNew) .style(button::text) .padding([4, 8]) @@ -2045,14 +2060,18 @@ fn render_params_visibility_toggle_row<'a>(value: bool) -> Element<'a, Message> }; button::Style { background: Some(Background::Color(pair.color)), - border: Border { color: palette.background.neutral.color, width: 1.0, radius: 2.0.into() }, + border: Border { color: palette.background.neutral.color, width: 1.0, radius: 2.0.into(), }, text_color: pair.text, ..Default::default() } }) .padding([4, 8]) .width(Length::Fill); - container(row![text(t!("Values").into_owned()).size(FONT_SZ).width(Length::Fill), btn].align_y(iced::Center)) + container(row![text(t!("Values").into_owned()) + .size(FONT_SZ) + .width(Length::Fill), btn + ] + .align_y(iced::Center),) .width(Length::Fill) .into() } @@ -2202,6 +2221,8 @@ fn text_input_style(theme: &Theme, status: text_input::Status) -> text_input::St } } +pub(crate) + fn combo_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style { text_input_style(theme, status) } @@ -2226,7 +2247,7 @@ mod tests { }; fn sample_sections() -> Vec { - use crate::scene::model::object::{Property, PropValue}; + use crate::scene::model::object::{PropValue,Property}; vec![ crate::scene::model::object::PropSection { title: "Geometry".into(), diff --git a/src/ui/window/graphic_attributes.rs b/src/ui/window/graphic_attributes.rs index beced95ae..f4e42a60a 100644 --- a/src/ui/window/graphic_attributes.rs +++ b/src/ui/window/graphic_attributes.rs @@ -4,9 +4,10 @@ use crate::app::Message; use crate::ui::dock::{DockMsg, PanelId}; use acadrust::{CadDocument, EntityType, Handle}; use iced::widget::{ - button, column, container, image, mouse_area, row, slider, text, tooltip, Space, + button, column, combo_box, container, image, mouse_area, row, slider, text, text_input, + tooltip, Space, }; -use iced::{Background, Border, Element, Fill, Length, Theme}; +use iced::{Background, Border, Element, Fill, Length, Padding, Theme}; pub(crate) struct SolidFillColorInfo { pub color: acadrust::types::Color, @@ -14,11 +15,49 @@ pub(crate) struct SolidFillColorInfo { pub varies: bool, } -struct FillTransparencyInfo { +struct TransparencyInfo { value: u8, varies: bool, + mode: PropertyMode, } +#[derive(Clone, Copy)] +enum PropertyMode { + ByLayer, + ByBlock, + Custom, +} + +impl PropertyMode { + fn button_label(self) -> &'static str { + match self { + Self::ByLayer => "[L]", + Self::ByBlock => "[B]", + Self::Custom => "[C]", + } + } +} + +struct LinetypeInfo { + value: String, + varies: bool, +} + +struct LineweightInfo { + value: acadrust::types::LineWeight, + varies: bool, +} + +struct LinetypeScaleInfo { + value: f64, + varies: bool, +} + +const PALETTE_CONTROL_ICON_SIZE: f32 = 20.0; +const TRANSPARENCY_ICON: &[u8] = + include_bytes!("../../../assets/icons/attributes/transparency.svg"); +const LINEWEIGHT_ICON: &[u8] = include_bytes!("../../../assets/icons/attributes/lineweight.svg"); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GraphicAttribute { Varies, @@ -116,32 +155,35 @@ pub(crate) fn fill_handles(document: &CadDocument, selected: &[Handle]) -> Vec Vec { - fill_handles(document, selected) - .into_iter() +pub(crate) fn line_handles(document: &CadDocument, selected: &[Handle]) -> Vec { + selected + .iter() + .copied() .filter(|handle| { - matches!( - document.get_entity(*handle), - Some(EntityType::Hatch(hatch)) - if hatch.is_solid && !hatch.gradient_color.enabled - ) + document + .get_entity(*handle) + .is_some_and(|entity| !matches!(entity, EntityType::Hatch(_))) }) .collect() } -fn fill_transparency_info( - document: &CadDocument, - selected: &[Handle], +fn top_handle( + handles: &[Handle], draw_depth: &rustc_hash::FxHashMap, -) -> Option { - let handles = fill_handles(document, selected); - let top = handles.iter().copied().max_by(|left, right| { +) -> Option { + handles.iter().copied().max_by(|left, right| { let left = draw_depth.get(&left.value()).map_or(0.0, |depth| depth[0]); - let right = draw_depth - .get(&right.value()) - .map_or(0.0, |depth| depth[0]); + let right = draw_depth.get(&right.value()).map_or(0.0, |depth| depth[0]); left.total_cmp(&right) - })?; + }) +} + +fn transparency_info( + document: &CadDocument, + handles: &[Handle], + draw_depth: &rustc_hash::FxHashMap, +) -> Option { + let top = top_handle(handles, draw_depth)?; let top_entity = document.get_entity(top)?; let stored = top_entity.common().transparency; let effective = if stored.is_by_layer() { @@ -154,29 +196,29 @@ fn fill_transparency_info( } else { stored }; - Some(FillTransparencyInfo { + Some(TransparencyInfo { value: (effective.as_percent() * 100.0).round().clamp(0.0, 90.0) as u8, varies: handles.iter().any(|handle| { document .get_entity(*handle) .is_some_and(|entity| entity.common().transparency != stored) }), + mode: if stored.is_by_layer() { + PropertyMode::ByLayer + } else if stored.is_by_block() { + PropertyMode::ByBlock + } else { + PropertyMode::Custom + }, }) } -fn solid_fill_color_info( +fn color_info( document: &CadDocument, - selected: &[Handle], + handles: &[Handle], draw_depth: &rustc_hash::FxHashMap, ) -> Option { - let handles = solid_fill_handles(document, selected); - let top = handles.iter().copied().max_by(|left, right| { - let left = draw_depth.get(&left.value()).map_or(0.0, |depth| depth[0]); - let right = draw_depth - .get(&right.value()) - .map_or(0.0, |depth| depth[0]); - left.total_cmp(&right) - })?; + let top = top_handle(handles, draw_depth)?; let top_entity = document.get_entity(top)?; let color = top_entity.common().color; let varies = handles.iter().any(|handle| { @@ -197,6 +239,145 @@ fn solid_fill_color_info( }) } +fn linetype_info( + document: &CadDocument, + handles: &[Handle], + draw_depth: &rustc_hash::FxHashMap, +) -> Option { + let top = top_handle(handles, draw_depth)?; + let value = document.get_entity(top)?.common().linetype.clone(); + let varies = handles.iter().any(|handle| { + document + .get_entity(*handle) + .is_some_and(|entity| entity.common().linetype != value) + }); + Some(LinetypeInfo { + value: if value.is_empty() { + "ByLayer".to_string() + } else { + value + }, + varies, + }) +} + +fn lineweight_info( + document: &CadDocument, + handles: &[Handle], + draw_depth: &rustc_hash::FxHashMap, +) -> Option { + let top = top_handle(handles, draw_depth)?; + let value = document.get_entity(top)?.common().line_weight; + let varies = handles.iter().any(|handle| { + document + .get_entity(*handle) + .is_some_and(|entity| entity.common().line_weight != value) + }); + Some(LineweightInfo { value, varies }) +} + +fn linetype_scale_info( + document: &CadDocument, + handles: &[Handle], + draw_depth: &rustc_hash::FxHashMap, +) -> Option { + let top = top_handle(handles, draw_depth)?; + let value = document.get_entity(top)?.common().linetype_scale; + let varies = handles.iter().any(|handle| { + document + .get_entity(*handle) + .is_some_and(|entity| (entity.common().linetype_scale - value).abs() > f64::EPSILON) + }); + Some(LinetypeScaleInfo { value, varies }) +} + +fn effective_linetype_is_continuous( + document: &CadDocument, + handles: &[Handle], + draw_depth: &rustc_hash::FxHashMap, +) -> bool { + let (name, layer) = top_handle(handles, draw_depth) + .and_then(|handle| document.get_entity(handle)) + .map(|entity| { + ( + entity.common().linetype.as_str(), + entity.common().layer.as_str(), + ) + }) + .unwrap_or_else(|| { + ( + current_linetype_name(document), + document.header.current_layer_name.as_str(), + ) + }); + let name = if name.is_empty() || name.eq_ignore_ascii_case("ByLayer") { + document + .layers + .get(if layer.is_empty() { "0" } else { layer }) + .map_or("Continuous", |layer| layer.line_type.as_str()) + } else if name.eq_ignore_ascii_case("ByBlock") { + "Continuous" + } else { + name + }; + name.eq_ignore_ascii_case("Continuous") || name.eq_ignore_ascii_case("Solid") +} + +fn current_linetype_name(document: &CadDocument) -> &str { + if !document.header.current_linetype_name.is_empty() { + document.header.current_linetype_name.as_str() + } else if !document.header.current_linetype_handle.is_null() { + document + .line_types + .iter() + .find(|line_type| line_type.handle == document.header.current_linetype_handle) + .map_or("ByLayer", |line_type| line_type.name.as_str()) + } else { + "ByLayer" + } +} + +fn linetype_preview_art( + document: &CadDocument, + handles: &[Handle], + draw_depth: &rustc_hash::FxHashMap, + logical_name: &str, +) -> String { + let layer_name = top_handle(handles, draw_depth) + .and_then(|handle| document.get_entity(handle)) + .map(|entity| entity.common().layer.as_str()) + .unwrap_or_else(|| { + if document.header.current_layer_name.is_empty() { + "0" + } else { + document.header.current_layer_name.as_str() + } + }); + let effective_name = if logical_name.is_empty() + || logical_name.eq_ignore_ascii_case("ByLayer") + || logical_name.eq_ignore_ascii_case("ByBlock") + { + document + .layers + .get(layer_name) + .map_or("Continuous", |layer| layer.line_type.as_str()) + } else { + logical_name + }; + document + .line_types + .iter() + .find(|line_type| line_type.name.eq_ignore_ascii_case(effective_name)) + .map(|line_type| { + if line_type.elements.is_empty() { + "____________".to_string() + } else { + crate::io::linetypes::extract_pattern(&line_type.description) + } + }) + .unwrap_or_default() +} + fn top_fill_hatch<'a>( document: &'a CadDocument, selected: &[Handle], @@ -231,25 +412,254 @@ fn top_fill_hatch<'a>( }) .max_by(|(left, _), (right, _)| { let left = draw_depth.get(&left.value()).map_or(0.0, |depth| depth[0]); - let right = draw_depth - .get(&right.value()) - .map_or(0.0, |depth| depth[0]); + let right = draw_depth.get(&right.value()).map_or(0.0, |depth| depth[0]); left.total_cmp(&right) }) .map(|(_, hatch)| hatch) } +fn popup_alignment(side: crate::app::config::DockSide) -> iced_aw::drop_down::Alignment { + match side { + crate::app::config::DockSide::Left => iced_aw::drop_down::Alignment::BottomEnd, + crate::app::config::DockSide::Right => iced_aw::drop_down::Alignment::BottomStart, + } +} + +fn by_mode_menu<'a>( + side: crate::app::config::DockSide, + mode: PropertyMode, + open: bool, + toggle: Message, + close: Message, + by_layer: Message, + by_block: Message, +) -> Element<'a, Message> { + let more = button(text(mode.button_label()).size(crate::ui::ROW_H * 0.42)) + .on_press(toggle) + .style(move |theme: &Theme, status| { + if open { + button::primary(theme, status) + } else { + button::secondary(theme, status) + } + }) + .height(crate::ui::ROW_H) + .padding([2, 7]); + if !open { + return more.into(); + } + let choice = |label, message| { + button(text(label).size(11)) + .on_press(message) + .style(crate::ui::color_select::list_row_style) + .width(Fill) + .padding([2, 4]) + }; + let popup = container(column![ + choice("ByLayer", by_layer), + choice("ByBlock", by_block), + ]) + .style(crate::ui::color_select::popup_panel_style) + .padding(2); + iced_aw::DropDown::new(more, popup, true) + .width(Length::Fixed(110.0)) + .height(Length::Shrink) + .alignment(popup_alignment(side)) + .offset(2.0) + .on_dismiss(close) + .into() +} + +fn color_control<'a>( + info: SolidFillColorInfo, + side: crate::app::config::DockSide, + menu_open: bool, + target: crate::app::ColorPickTarget, + toggle: Message, + close: Message, + changed: fn(acadrust::types::Color) -> Message, +) -> Element<'a, Message> { + let label = if info.varies { + Some("*VARIES*") + } else { + match info.color { + acadrust::types::Color::ByLayer => Some("ByLayer"), + acadrust::types::Color::ByBlock => Some("ByBlock"), + _ => None, + } + } + .map(|label| { + container(text(label).size(10)) + .width(Fill) + .align_x(iced::Left) + .align_y(iced::Center) + .padding([0, 6]) + }); + let color = button(label.unwrap_or_else(|| container(Space::new()).width(Fill).height(Fill))) + .on_press(Message::OpenColorWindow(target.clone(), info.color)) + .width(Fill) + .height(crate::ui::ROW_H) + .padding(0) + .style(move |theme: &Theme, status| button::Style { + background: Some(Background::Color(info.display)), + text_color: if info.display.r * 0.299 + info.display.g * 0.587 + info.display.b * 0.114 + > 0.55 + { + iced::Color::BLACK + } else { + iced::Color::WHITE + }, + border: Border { + color: if matches!(status, button::Status::Hovered) { + theme.palette().primary.base.color + } else { + theme.palette().background.neutral.color + }, + width: 1.0, + radius: 2.0.into(), + }, + ..Default::default() + }); + let mode = if info.varies { + PropertyMode::Custom + } else { + match info.color { + acadrust::types::Color::ByLayer => PropertyMode::ByLayer, + acadrust::types::Color::ByBlock => PropertyMode::ByBlock, + _ => PropertyMode::Custom, + } + }; + let more = button(text(mode.button_label()).size(crate::ui::ROW_H * 0.42)) + .on_press(toggle) + .style(move |theme: &Theme, status| { + if menu_open { + button::primary(theme, status) + } else { + button::secondary(theme, status) + } + }) + .height(crate::ui::ROW_H) + .padding([2, 7]); + let more: Element<'a, Message> = if menu_open { + let choice = |label, color| { + button(text(label).size(11)) + .on_press(changed(color)) + .style(crate::ui::color_select::list_row_style) + .width(Fill) + .padding([2, 4]) + }; + let popup = container(column![ + choice("ByLayer", acadrust::types::Color::ByLayer), + choice("ByBlock", acadrust::types::Color::ByBlock), + button(text("Custom...").size(11)) + .on_press(Message::OpenColorWindow(target, info.color)) + .style(crate::ui::color_select::list_row_style) + .width(Fill) + .padding([2, 4]) + ]) + .style(crate::ui::color_select::popup_panel_style) + .padding(2); + iced_aw::DropDown::new(more, popup, true) + .width(Length::Fixed(110.0)) + .height(Length::Shrink) + .alignment(popup_alignment(side)) + .offset(2.0) + .on_dismiss(close) + .into() + } else { + more.into() + }; + row![color, more].spacing(4).align_y(iced::Center).into() +} + +fn transparency_control<'a>( + info: TransparencyInfo, + side: crate::app::config::DockSide, + menu_open: bool, + toggle: Message, + close: Message, + changed: fn(u8) -> Message, + by_layer: Message, + by_block: Message, +) -> Element<'a, Message> { + let value_label = if info.varies { + "*VARIES*".to_string() + } else { + format!("{}%", info.value) + }; + let control = row![ + crate::ui::icons::semantic(TRANSPARENCY_ICON, PALETTE_CONTROL_ICON_SIZE), + text(value_label).size(10).width(38), + slider(0..=90, i32::from(info.value), move |value| changed( + value as u8 + )) + .width(Fill), + ] + .spacing(5) + .align_y(iced::Center) + .height(crate::ui::ROW_H); + let mode = if info.varies { + PropertyMode::Custom + } else { + info.mode + }; + let more = button(text(mode.button_label()).size(crate::ui::ROW_H * 0.42)) + .on_press(toggle) + .style(move |theme: &Theme, status| { + if menu_open { + button::primary(theme, status) + } else { + button::secondary(theme, status) + } + }) + .height(crate::ui::ROW_H) + .padding([2, 7]); + let more: Element<'a, Message> = if menu_open { + let row_button = |label, message| { + button(text(label).size(11)) + .on_press(message) + .style(crate::ui::color_select::list_row_style) + .width(Fill) + .padding([2, 4]) + }; + let popup = container(column![ + row_button("ByLayer", by_layer), + row_button("ByBlock", by_block), + row_button("Custom", changed(info.value)), + ]) + .style(crate::ui::color_select::popup_panel_style) + .padding(2); + iced_aw::DropDown::new(more, popup, true) + .width(Length::Fixed(110.0)) + .height(Length::Shrink) + .alignment(popup_alignment(side)) + .offset(2.0) + .on_dismiss(close) + .into() + } else { + more.into() + }; + row![control, more].spacing(4).align_y(iced::Center).into() +} + pub fn view<'a>( document: &'a CadDocument, + properties: &'a crate::ui::properties::PropertiesPanel, selected: &[Handle], draw_depth: &rustc_hash::FxHashMap, width: f32, auto_collapse: bool, side: crate::app::config::DockSide, menu_open: bool, + line_color_menu_open: bool, + line_linetype_menu_open: bool, + line_lineweight_menu_open: bool, + line_transparency_menu_open: bool, solid_color_menu_open: bool, transparency_menu_open: bool, hatch_editor_open: bool, + hatch_pattern_search: &'a str, + hatch_pattern_focus: usize, gradient_editor: Option<&'a crate::ui::window::gradient_editor::GradientEditorState>, gradient_color_picker_open: bool, ) -> Element<'a, Message> { @@ -367,38 +777,283 @@ pub fn view<'a>( } else { picker.into() }; - let mut body = column![text(crate::t!("Fill")).size(11), picker].spacing(6); - if current == GraphicAttribute::Solid { - if let Some(info) = solid_fill_color_info(document, selected, draw_depth) { - let label = info.varies.then(|| { - container(text("* VARIES *").size(10)) - .width(Fill) - .align_x(iced::Center) - .align_y(iced::Center) + let line_handles = line_handles(document, selected); + let fill_handles = fill_handles(document, selected); + let mut body = column![text(crate::t!("Line")).size(11)].spacing(6); + let no_selection = selected.is_empty(); + let line_color = color_info(document, &line_handles, draw_depth).or_else(|| { + no_selection.then(|| { + let color = document.header.current_entity_color; + let display = if color == acadrust::types::Color::ByLayer { + document + .layers + .get(if document.header.current_layer_name.is_empty() { + "0" + } else { + &document.header.current_layer_name + }) + .map_or_else( + || crate::ui::properties::acad_color_display(color).0, + |layer| crate::ui::properties::acad_color_display(layer.color).0, + ) + } else { + crate::ui::properties::acad_color_display(color).0 + }; + SolidFillColorInfo { + color, + display, + varies: false, + } + }) }); - let color = button(label.unwrap_or_else(|| { - container(Space::new()).width(Fill).height(Fill) - })) - .on_press(Message::OpenColorWindow( + let linetype = linetype_info(document, &line_handles, draw_depth).or_else(|| { + no_selection.then(|| LinetypeInfo { + value: current_linetype_name(document).to_string(), + varies: false, + }) + }); + let continuous_linetype = linetype.as_ref().is_some_and(|info| { + !info.varies && effective_linetype_is_continuous(document, &line_handles, draw_depth) + }); + let linetype_scale = linetype_scale_info(document, &line_handles, draw_depth).or_else(|| { + no_selection.then(|| LinetypeScaleInfo { + value: document.header.current_entity_linetype_scale, + varies: false, + }) + }); + if let Some(info) = linetype.as_ref() { + let selected = (!info.varies).then(|| crate::ui::properties::LinetypeItem { + name: info.value.clone(), + art: linetype_preview_art(document, &line_handles, draw_depth, &info.value), + }); + let combo = combo_box( + &properties.linetype_combo, + "*VARIES*", + selected.as_ref(), + |item: crate::ui::properties::LinetypeItem| Message::LineLinetypeChanged(item.name), + ) + .size(crate::ui::ROW_H * 0.42) + .padding(Padding { + top: 3.0, + bottom: 3.0, + left: 6.0, + right: 6.0, + }) + .input_style(crate::ui::properties::combo_input_style) + .width(Fill); + let more = by_mode_menu( + side, + if info.varies { + PropertyMode::Custom + } else if info.value.eq_ignore_ascii_case("ByLayer") { + PropertyMode::ByLayer + } else if info.value.eq_ignore_ascii_case("ByBlock") { + PropertyMode::ByBlock + } else { + PropertyMode::Custom + }, + line_linetype_menu_open, + Message::ToggleLineLinetypeMenu, + Message::CloseLineLinetypeMenu, + Message::LineLinetypeChanged("ByLayer".to_string()), + Message::LineLinetypeChanged("ByBlock".to_string()), + ); + body = body.push( + row![crate::ui::wide_menu::wide_menu(combo, 220.0), more] + .spacing(4) + .align_y(iced::Center), + ); + } + let lineweight = lineweight_info(document, &line_handles, draw_depth).or_else(|| { + no_selection.then(|| LineweightInfo { + value: acadrust::types::LineWeight::from_value(document.header.current_line_weight), + varies: false, + }) + }); + if let Some(color) = line_color { + body = body.push(color_control( + color, + side, + line_color_menu_open, + crate::app::ColorPickTarget::GraphicAttributesLine, + Message::ToggleLineColorDropdown, + Message::CloseLineColorDropdown, + Message::LineColorChanged, + )); + } + if let (Some(scale), Some(lineweight)) = (linetype_scale, lineweight) { + let scale_text = if scale.varies { + String::new() + } else { + format!("{:.2}", scale.value) + }; + let mut scale_input = text_input("*VARIES*", &scale_text) + .size(crate::ui::ROW_H * 0.42) + .padding([3, 5]) + .style(crate::ui::properties::combo_input_style) + .on_submit(Message::Noop) + .width(Fill); + if !continuous_linetype { + scale_input = scale_input.on_input(Message::LineLinetypeScaleInput); + } + let step = |up: bool| { + let icon: Element<'_, Message> = if up { + crate::ui::icons::themed_arrow_up(7.0) + } else { + crate::ui::icons::themed_arrow_down(7.0) + }; + let button = button(icon).style(button::secondary).padding([0, 3]); + if continuous_linetype { + button + } else { + let delta = if up { 0.1 } else { -0.1 }; + button.on_press(Message::LineLinetypeScaleChanged( + (scale.value + delta).clamp(0.01, 1000.0), + )) + } + }; + let scale = row![ + crate::ui::icons::semantic(GraphicAttribute::Solid.icon(), PALETTE_CONTROL_ICON_SIZE), + scale_input, + column![step(true), step(false)].spacing(0), + ] + .spacing(3) + .align_y(iced::Center) + .width(Length::FillPortion(1)); + let selected = + (!lineweight.varies).then_some(crate::ui::properties::LwItem(lineweight.value)); + let lineweight_mode = if lineweight.varies { + PropertyMode::Custom + } else { + match lineweight.value { + acadrust::types::LineWeight::ByLayer => PropertyMode::ByLayer, + acadrust::types::LineWeight::ByBlock => PropertyMode::ByBlock, + _ => PropertyMode::Custom, + } + }; + let lineweight_picker = combo_box( + &properties.lineweight_combo, + "*VARIES*", + selected.as_ref(), + |item: crate::ui::properties::LwItem| Message::LineLineweightChanged(item.0), + ) + .size(crate::ui::ROW_H * 0.42) + .padding(Padding { + top: 3.0, + bottom: 3.0, + left: 6.0, + right: 6.0, + }) + .input_style(crate::ui::properties::combo_input_style) + .width(Fill); + let more = by_mode_menu( + side, + lineweight_mode, + line_lineweight_menu_open, + Message::ToggleLineLineweightMenu, + Message::CloseLineLineweightMenu, + Message::LineLineweightChanged(acadrust::types::LineWeight::ByLayer), + Message::LineLineweightChanged(acadrust::types::LineWeight::ByBlock), + ); + let lineweight = row![ + crate::ui::icons::semantic(LINEWEIGHT_ICON, PALETTE_CONTROL_ICON_SIZE), + lineweight_picker, + more, + ] + .spacing(3) + .align_y(iced::Center) + .width(Length::FillPortion(1)); + body = body.push( + row![scale, lineweight] + .spacing(12) + .align_y(iced::Center) + .height(crate::ui::ROW_H), + ); + } + let line_transparency = transparency_info(document, &line_handles, draw_depth).or_else(|| { + no_selection.then(|| { + let stored = document.current_entity_transparency(); + let effective = if stored.is_by_layer() { + document + .layers + .get(if document.header.current_layer_name.is_empty() { + "0" + } else { + &document.header.current_layer_name + }) + .map_or(stored, |layer| layer.transparency) + } else if stored.is_by_block() { + acadrust::types::Transparency::from_percent(0.0) + } else { + stored + }; + TransparencyInfo { + value: (effective.as_percent() * 100.0).round().clamp(0.0, 90.0) as u8, + varies: false, + mode: if stored.is_by_layer() { + PropertyMode::ByLayer + } else if stored.is_by_block() { + PropertyMode::ByBlock + } else { + PropertyMode::Custom + }, + } + }) + }); + if let Some(info) = line_transparency { + body = body.push(transparency_control( + info, + side, + line_transparency_menu_open, + Message::ToggleLineTransparencyDropdown, + Message::CloseLineTransparencyDropdown, + Message::LineTransparencyChanged, + Message::LineTransparencyByLayer, + Message::LineTransparencyByBlock, + )); + } + body = body + .push(Space::new().height(2)) + .push(text(crate::t!("Fill")).size(11)) + .push(picker); + if !matches!(current, GraphicAttribute::None | GraphicAttribute::Gradient) { + if let Some(info) = color_info(document, &fill_handles, draw_depth) { + body = body.push(color_control( + info, + side, + solid_color_menu_open, crate::app::ColorPickTarget::GraphicAttributesSolid, - info.color, - )) + Message::ToggleSolidFillColorDropdown, + Message::CloseSolidFillColorDropdown, + Message::SolidFillColorChanged, + )); + } + } + if current == GraphicAttribute::Gradient { + if let Some(hatch) = + top_fill_hatch(document, selected, draw_depth, GraphicAttribute::Gradient) + { + let toggle = if gradient_editor.is_some() { + Message::GradientCancel + } else { + Message::GradientEditorOpen + }; + let preview = button( + image(crate::ui::window::gradient_editor::compact_preview(hatch)) + .width(Fill) + .height(Fill) + .content_fit(iced::ContentFit::Fill), + ) + .on_press(toggle.clone()) .width(Fill) .height(crate::ui::ROW_H) .padding(0) .style(move |theme: &Theme, status| button::Style { - background: Some(Background::Color(info.display)), - text_color: if info.display.r * 0.299 - + info.display.g * 0.587 - + info.display.b * 0.114 - > 0.55 - { - iced::Color::BLACK - } else { - iced::Color::WHITE - }, + background: Some(Background::Color(theme.palette().background.base.color)), border: Border { - color: if matches!(status, button::Status::Hovered) { + color: if gradient_editor.is_some() + || matches!(status, button::Status::Hovered | button::Status::Pressed) + { theme.palette().primary.base.color } else { theme.palette().background.neutral.color @@ -408,87 +1063,8 @@ pub fn view<'a>( }, ..Default::default() }); - let more = button(text("...").size(crate::ui::ROW_H * 0.42)) - .on_press(Message::ToggleSolidFillColorDropdown) - .style(move |theme: &Theme, status| { - if solid_color_menu_open { - button::primary(theme, status) - } else { - button::secondary(theme, status) - } - }) - .height(crate::ui::ROW_H) - .padding([2, 7]); - let more: Element<'_, Message> = if solid_color_menu_open { - let choice = |label, color| { - button(text(label).size(11)) - .on_press(Message::SolidFillColorChanged(color)) - .style(crate::ui::color_select::list_row_style) - .width(Fill) - .padding([2, 4]) - }; - let popup = container(column![ - choice("ByLayer", acadrust::types::Color::ByLayer), - choice("ByBlock", acadrust::types::Color::ByBlock), - button(text("Custom...").size(11)) - .on_press(Message::OpenColorWindow( - crate::app::ColorPickTarget::GraphicAttributesSolid, - info.color, - )) - .style(crate::ui::color_select::list_row_style) - .width(Fill) - .padding([2, 4]) - ]) - .style(crate::ui::color_select::popup_panel_style) - .padding(2); - let alignment = match side { - crate::app::config::DockSide::Left => { - iced_aw::drop_down::Alignment::BottomEnd - } - crate::app::config::DockSide::Right => { - iced_aw::drop_down::Alignment::BottomStart - } - }; - iced_aw::DropDown::new(more, popup, true) - .width(Length::Fixed(110.0)) - .height(Length::Shrink) - .alignment(alignment) - .offset(2.0) - .on_dismiss(Message::CloseSolidFillColorDropdown) - .into() - } else { - more.into() - }; - body = body.push(row![color, more].spacing(4).align_y(iced::Center)); - } - } else if current == GraphicAttribute::Gradient { - if let Some(hatch) = top_fill_hatch( - document, - selected, - draw_depth, - GraphicAttribute::Gradient, - ) { - let preview = container( - image(crate::ui::window::gradient_editor::compact_preview(hatch)) - .width(Fill) - .height(crate::ui::ROW_H), - ) - .width(Fill) - .height(crate::ui::ROW_H) - .style(|theme: &Theme| container::Style { - border: Border { - color: theme.palette().background.neutral.color, - width: 1.0, - radius: 2.0.into(), - }, - ..Default::default() - }); let edit = button(text("...").size(crate::ui::ROW_H * 0.42)) - .on_press(if gradient_editor.is_some() { - Message::GradientCancel - } else { - Message::GradientEditorOpen - }) + .on_press(toggle) .style(move |theme: &Theme, status| { if gradient_editor.is_some() { button::primary(theme, status) @@ -505,19 +1081,14 @@ pub fn view<'a>( ); let edit: Element<'_, Message> = if let Some(editor) = gradient_editor { let alignment = match side { - crate::app::config::DockSide::Left => { - iced_aw::drop_down::Alignment::BottomEnd - } + crate::app::config::DockSide::Left => iced_aw::drop_down::Alignment::BottomEnd, crate::app::config::DockSide::Right => { iced_aw::drop_down::Alignment::BottomStart } }; iced_aw::DropDown::new( edit, - crate::ui::window::gradient_editor::view( - editor, - gradient_color_picker_open, - ), + crate::ui::window::gradient_editor::view(editor, gradient_color_picker_open), true, ) .width(Length::Fixed(390.0)) @@ -530,12 +1101,9 @@ pub fn view<'a>( body = body.push(row![preview, edit].spacing(4).align_y(iced::Center)); } } else if current == GraphicAttribute::Hatch { - let preview: Element<'_, Message> = top_fill_hatch( - document, - selected, - draw_depth, - GraphicAttribute::Hatch, - ) + let hatch = top_fill_hatch(document, selected, draw_depth, GraphicAttribute::Hatch); + let current_pattern = hatch.map_or("", |hatch| hatch.pattern.name.as_str()); + let preview_content: Element<'_, Message> = hatch .and_then(|hatch| { crate::scene::Scene::hatch_model_from_dxf(hatch, [1.0; 4]) .map(|model| model.pattern) @@ -545,17 +1113,53 @@ pub fn view<'a>( container(Space::new()) .width(Fill) .height(crate::ui::ROW_H) - .style(|theme: &Theme| container::Style { + .into() + }); + let preview = button(preview_content) + .on_press(if hatch_editor_open { + Message::HatchEditorClose + } else { + Message::HatchEditorOpen + }) + .width(Fill) + .height(crate::ui::ROW_H) + .padding(0) + .style(move |theme: &Theme, status| button::Style { background: Some(Background::Color(theme.palette().background.base.color)), border: Border { - color: theme.palette().background.neutral.color, + color: if hatch_editor_open + || matches!(status, button::Status::Hovered | button::Status::Pressed) + { + theme.palette().primary.base.color + } else { + theme.palette().background.neutral.color + }, width: 1.0, radius: 2.0.into(), }, ..Default::default() - }) - .into() }); + let preview: Element<'_, Message> = if hatch_editor_open { + let popup = crate::ui::properties::hatch_pattern_picker_content( + hatch_pattern_search, + hatch_pattern_focus, + current_pattern, + "graphic-hatch-pattern-search", + Message::GraphicHatchPatternSearchChanged, + Message::GraphicHatchPatternConfirm, + Message::GraphicHatchPatternFocus, + Message::GraphicHatchPatternChanged, + ); + iced_aw::DropDown::new(preview, popup, true) + .width(Length::Fixed(348.0)) + .height(Length::Fixed(720.0)) + .alignment(popup_alignment(side)) + .offset(4.0) + .on_dismiss(Message::HatchEditorClose) + .into() + } else { + preview.into() + }; let edit = button(text("...").size(crate::ui::ROW_H * 0.42)) .on_press(if hatch_editor_open { Message::HatchEditorClose @@ -572,87 +1176,20 @@ pub fn view<'a>( .height(crate::ui::ROW_H) .padding([2, 7]); let edit = tooltip(edit, text("Edit hatch").size(10), tooltip::Position::Bottom); - let edit: Element<'_, Message> = if hatch_editor_open { - let alignment = match side { - crate::app::config::DockSide::Left => iced_aw::drop_down::Alignment::BottomEnd, - crate::app::config::DockSide::Right => iced_aw::drop_down::Alignment::BottomStart, - }; - iced_aw::DropDown::new(edit, crate::ui::window::hatch_editor::view(), true) - .width(Length::Fixed(390.0)) - .alignment(alignment) - .offset(4.0) - .into() - } else { - edit.into() - }; body = body.push(row![preview, edit].spacing(4).align_y(iced::Center)); } if current != GraphicAttribute::None { - if let Some(info) = fill_transparency_info(document, selected, draw_depth) { - let value_label = if info.varies { - "*VARIES*".to_string() - } else { - format!("{}%", info.value) - }; - let control = row![ - text(crate::t!("Transparency")).size(11).width(75), - slider(0..=90, i32::from(info.value), |value| { - Message::FillTransparencyChanged(value as u8) - }) - .width(Fill), - text(value_label).size(10).width(45), - ] - .spacing(5) - .align_y(iced::Center) - .height(crate::ui::ROW_H); - let more = button(text("...").size(crate::ui::ROW_H * 0.42)) - .on_press(Message::ToggleFillTransparencyDropdown) - .style(move |theme: &Theme, status| { - if transparency_menu_open { - button::primary(theme, status) - } else { - button::secondary(theme, status) - } - }) - .height(crate::ui::ROW_H) - .padding([2, 7]); - let more: Element<'_, Message> = if transparency_menu_open { - let row_button = |label, message| { - button(text(label).size(11)) - .on_press(message) - .style(crate::ui::color_select::list_row_style) - .width(Fill) - .padding([2, 4]) - }; - let popup = container(column![ - row_button("ByLayer", Message::FillTransparencyByLayer), - row_button("ByBlock", Message::FillTransparencyByBlock), - row_button( - "Custom", - Message::FillTransparencyChanged(info.value), - ), - ]) - .style(crate::ui::color_select::popup_panel_style) - .padding(2); - let alignment = match side { - crate::app::config::DockSide::Left => { - iced_aw::drop_down::Alignment::BottomEnd - } - crate::app::config::DockSide::Right => { - iced_aw::drop_down::Alignment::BottomStart - } - }; - iced_aw::DropDown::new(more, popup, true) - .width(Length::Fixed(110.0)) - .height(Length::Shrink) - .alignment(alignment) - .offset(2.0) - .on_dismiss(Message::CloseFillTransparencyDropdown) - .into() - } else { - more.into() - }; - body = body.push(row![control, more].spacing(4).align_y(iced::Center)); + if let Some(info) = transparency_info(document, &fill_handles, draw_depth) { + body = body.push(transparency_control( + info, + side, + transparency_menu_open, + Message::ToggleFillTransparencyDropdown, + Message::CloseFillTransparencyDropdown, + Message::FillTransparencyChanged, + Message::FillTransparencyByLayer, + Message::FillTransparencyByBlock, + )); } } let body = body.padding(8); diff --git a/src/ui/window/hatch_editor.rs b/src/ui/window/hatch_editor.rs deleted file mode 100644 index 677bff862..000000000 --- a/src/ui/window/hatch_editor.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::app::Message; -use iced::widget::{button, column, container, mouse_area, row, text, Space}; -use iced::{Background, Border, Element, Fill, Length, Theme}; - -pub fn view() -> Element<'static, Message> { - let content = column![ - text("Hatch settings").size(12), - text("Additional hatch settings will be available here.").size(11), - Space::new().height(12), - container( - row![button(text(crate::t!("Close")).size(11)) - .on_press(Message::HatchEditorClose) - .style(button::secondary) - .padding([5, 14])] - .width(Fill), - ) - .width(Fill) - .align_x(iced::alignment::Horizontal::Right), - ] - .spacing(8) - .padding(10); - let panel = container(content) - .width(Length::Fixed(390.0)) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.base.color)), - border: Border { - color: theme.palette().primary.base.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); - mouse_area(panel) - .interaction(iced::mouse::Interaction::Pointer) - .into() -} diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index 6f2de35a1..0944cb779 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -1,33 +1,32 @@ pub mod about; +pub mod alias_editor; +pub mod annotation_data; +pub mod attribute_editor; +pub mod auto_constrain_settings; pub mod block_definition; pub mod block_palette; pub mod browser; -pub mod graphic_attributes; -pub mod gradient_editor; -pub mod hatch_editor; -pub mod layout_manager; -pub mod layer_state_manager; -pub mod drawing_units; -pub mod geometric_tolerance; pub mod drafting_settings; -pub mod auto_constrain_settings; -pub mod layer_translator; -pub mod plot; -pub mod print_all; -pub mod plugin_manager; -pub mod shortcuts; -pub mod layers; -pub mod update_notice; -pub mod open_progress; -pub mod missing_fonts; -pub mod recovery; -pub mod options; -pub mod attribute_editor; -pub mod annotation_data; -pub mod alias_editor; +pub mod drawing_units; pub mod find_replace; +pub mod geometric_tolerance; +pub mod gradient_editor; +pub mod graphic_attributes; +pub mod layer_state_manager; +pub mod layer_translator; +pub mod layers; +pub mod layout_manager; +pub mod missing_fonts; pub mod named_parameters; +pub mod open_progress; +pub mod options; +pub mod plot; +pub mod plugin_manager; +pub mod print_all; +pub mod recovery; +pub mod shortcuts; +pub mod update_notice; +pub mod wblock; pub mod xref_attach; pub mod xref_help; pub mod xref_manager; -pub mod wblock;