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;