parent
a717572d65
commit
6686141d3b
6 changed files with 448 additions and 51 deletions
|
|
@ -1926,6 +1926,16 @@ pub enum Message {
|
||||||
PropVertexStep(i8),
|
PropVertexStep(i8),
|
||||||
/// User selected a hatch pattern from the pattern pick_list in Properties.
|
/// User selected a hatch pattern from the pattern pick_list in Properties.
|
||||||
PropHatchPatternChanged(String),
|
PropHatchPatternChanged(String),
|
||||||
|
/// Open or close the visual hatch-pattern picker.
|
||||||
|
PropHatchPatternPickerToggle(String),
|
||||||
|
/// Filter the visual hatch-pattern picker.
|
||||||
|
PropHatchPatternSearchChanged(String),
|
||||||
|
/// Move the visual pattern grid focus to a hovered card.
|
||||||
|
PropHatchPatternFocus(usize),
|
||||||
|
/// Move keyboard focus by one card or one two-column row.
|
||||||
|
PropHatchPatternNavigate(i8),
|
||||||
|
/// Select the keyboard-focused visual pattern card.
|
||||||
|
PropHatchPatternConfirm,
|
||||||
/// User selected a generic choice field in the Properties panel.
|
/// User selected a generic choice field in the Properties panel.
|
||||||
PropGeomChoiceChanged {
|
PropGeomChoiceChanged {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
|
|
|
||||||
|
|
@ -342,9 +342,6 @@ impl OpenCADStudio {
|
||||||
lineweight_combo: iced::widget::combo_box::State::new(
|
lineweight_combo: iced::widget::combo_box::State::new(
|
||||||
ui::properties::lw_options(),
|
ui::properties::lw_options(),
|
||||||
),
|
),
|
||||||
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|
||||||
crate::scene::model::hatch_patterns::names(),
|
|
||||||
),
|
|
||||||
linetype_items,
|
linetype_items,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
@ -1464,9 +1461,6 @@ impl OpenCADStudio {
|
||||||
title,
|
title,
|
||||||
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
||||||
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
||||||
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|
||||||
crate::scene::model::hatch_patterns::names(),
|
|
||||||
),
|
|
||||||
lineweight_combo: iced::widget::combo_box::State::new(
|
lineweight_combo: iced::widget::combo_box::State::new(
|
||||||
ui::properties::lw_options(),
|
ui::properties::lw_options(),
|
||||||
),
|
),
|
||||||
|
|
@ -1485,9 +1479,6 @@ impl OpenCADStudio {
|
||||||
lineweight_combo: iced::widget::combo_box::State::new(
|
lineweight_combo: iced::widget::combo_box::State::new(
|
||||||
ui::properties::lw_options(),
|
ui::properties::lw_options(),
|
||||||
),
|
),
|
||||||
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|
||||||
crate::scene::model::hatch_patterns::names(),
|
|
||||||
),
|
|
||||||
linetype_items,
|
linetype_items,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|
@ -1528,9 +1519,6 @@ impl OpenCADStudio {
|
||||||
selected_group: active_group,
|
selected_group: active_group,
|
||||||
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
||||||
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
||||||
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|
||||||
crate::scene::model::hatch_patterns::names(),
|
|
||||||
),
|
|
||||||
lineweight_combo: iced::widget::combo_box::State::new(
|
lineweight_combo: iced::widget::combo_box::State::new(
|
||||||
ui::properties::lw_options(),
|
ui::properties::lw_options(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1213,6 +1213,12 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandHistoryPrev => {
|
Message::CommandHistoryPrev => {
|
||||||
|
if self.tabs[self.active_tab]
|
||||||
|
.properties
|
||||||
|
.hatch_pattern_picker_open
|
||||||
|
{
|
||||||
|
return self.update(Message::PropHatchPatternNavigate(-2));
|
||||||
|
}
|
||||||
// Grip popup wins first — arrow keys walk its items.
|
// Grip popup wins first — arrow keys walk its items.
|
||||||
if let Some(popup) = self.grip_popup.as_mut() {
|
if let Some(popup) = self.grip_popup.as_mut() {
|
||||||
if !popup.items.is_empty() {
|
if !popup.items.is_empty() {
|
||||||
|
|
@ -1235,6 +1241,12 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandHistoryNext => {
|
Message::CommandHistoryNext => {
|
||||||
|
if self.tabs[self.active_tab]
|
||||||
|
.properties
|
||||||
|
.hatch_pattern_picker_open
|
||||||
|
{
|
||||||
|
return self.update(Message::PropHatchPatternNavigate(2));
|
||||||
|
}
|
||||||
if let Some(popup) = self.grip_popup.as_mut() {
|
if let Some(popup) = self.grip_popup.as_mut() {
|
||||||
if !popup.items.is_empty() {
|
if !popup.items.is_empty() {
|
||||||
popup.selected = (popup.selected + 1) % popup.items.len();
|
popup.selected = (popup.selected + 1) % popup.items.len();
|
||||||
|
|
@ -1349,7 +1361,17 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
Message::CommandFinalize => self.on_command_finalize(),
|
Message::CommandFinalize => self.on_command_finalize(),
|
||||||
|
|
||||||
Message::CommandEscape => self.on_command_escape(),
|
Message::CommandEscape => {
|
||||||
|
let panel = &mut self.tabs[self.active_tab].properties;
|
||||||
|
if panel.hatch_pattern_picker_open {
|
||||||
|
panel.hatch_pattern_picker_open = false;
|
||||||
|
panel.hatch_pattern_search.clear();
|
||||||
|
panel.hatch_pattern_focus = 0;
|
||||||
|
Task::none()
|
||||||
|
} else {
|
||||||
|
self.on_command_escape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Message::Command(cmd) => {
|
Message::Command(cmd) => {
|
||||||
// Close viewport context menu if open.
|
// Close viewport context menu if open.
|
||||||
|
|
@ -2764,6 +2786,12 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MTextCaretMove(d) => {
|
Message::MTextCaretMove(d) => {
|
||||||
|
if self.tabs[self.active_tab]
|
||||||
|
.properties
|
||||||
|
.hatch_pattern_picker_open
|
||||||
|
{
|
||||||
|
return self.update(Message::PropHatchPatternNavigate(d as i8));
|
||||||
|
}
|
||||||
self.mtext_caret_move(d);
|
self.mtext_caret_move(d);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3088,7 +3116,79 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::PropHatchPatternChanged(name) => self.on_prop_hatch_pattern_changed(name),
|
Message::PropHatchPatternChanged(name) => {
|
||||||
|
let panel = &mut self.tabs[self.active_tab].properties;
|
||||||
|
panel.hatch_pattern_picker_open = false;
|
||||||
|
panel.hatch_pattern_search.clear();
|
||||||
|
self.on_prop_hatch_pattern_changed(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PropHatchPatternPickerToggle(current) => {
|
||||||
|
let panel = &mut self.tabs[self.active_tab].properties;
|
||||||
|
panel.hatch_pattern_picker_open = !panel.hatch_pattern_picker_open;
|
||||||
|
if panel.hatch_pattern_picker_open {
|
||||||
|
panel.color_picker_open = false;
|
||||||
|
panel.color_palette_open = false;
|
||||||
|
panel.open_color_field = None;
|
||||||
|
panel.edit_choice_open = false;
|
||||||
|
panel.hatch_pattern_focus =
|
||||||
|
crate::ui::properties::filtered_hatch_patterns("")
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.name.eq_ignore_ascii_case(¤t))
|
||||||
|
.unwrap_or(0);
|
||||||
|
return iced::widget::operation::focus(iced::widget::Id::new(
|
||||||
|
"hatch-pattern-search",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
panel.hatch_pattern_search.clear();
|
||||||
|
panel.hatch_pattern_focus = 0;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PropHatchPatternSearchChanged(search) => {
|
||||||
|
let panel = &mut self.tabs[self.active_tab].properties;
|
||||||
|
panel.hatch_pattern_search = search;
|
||||||
|
panel.hatch_pattern_focus = 0;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PropHatchPatternFocus(index) => {
|
||||||
|
let panel = &mut self.tabs[self.active_tab].properties;
|
||||||
|
let len =
|
||||||
|
crate::ui::properties::filtered_hatch_patterns(&panel.hatch_pattern_search)
|
||||||
|
.len();
|
||||||
|
if index < len {
|
||||||
|
panel.hatch_pattern_focus = index;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PropHatchPatternNavigate(delta) => {
|
||||||
|
let panel = &mut self.tabs[self.active_tab].properties;
|
||||||
|
let len =
|
||||||
|
crate::ui::properties::filtered_hatch_patterns(&panel.hatch_pattern_search)
|
||||||
|
.len();
|
||||||
|
if len > 0 {
|
||||||
|
panel.hatch_pattern_focus =
|
||||||
|
(panel.hatch_pattern_focus as isize + delta as isize)
|
||||||
|
.rem_euclid(len as isize) as usize;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PropHatchPatternConfirm => {
|
||||||
|
let panel = &self.tabs[self.active_tab].properties;
|
||||||
|
let name =
|
||||||
|
crate::ui::properties::filtered_hatch_patterns(&panel.hatch_pattern_search)
|
||||||
|
.get(panel.hatch_pattern_focus)
|
||||||
|
.map(|entry| entry.name.clone());
|
||||||
|
if let Some(name) = name {
|
||||||
|
self.update(Message::PropHatchPatternChanged(name))
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Message::PropBoolToggle(field) => {
|
Message::PropBoolToggle(field) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|
@ -3236,6 +3336,10 @@ impl OpenCADStudio {
|
||||||
Message::PropEditChoiceToggle => {
|
Message::PropEditChoiceToggle => {
|
||||||
let panel = &mut self.tabs[self.active_tab].properties;
|
let panel = &mut self.tabs[self.active_tab].properties;
|
||||||
panel.edit_choice_open = !panel.edit_choice_open;
|
panel.edit_choice_open = !panel.edit_choice_open;
|
||||||
|
if panel.edit_choice_open {
|
||||||
|
panel.hatch_pattern_picker_open = false;
|
||||||
|
panel.hatch_pattern_search.clear();
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3255,6 +3359,8 @@ impl OpenCADStudio {
|
||||||
!self.tabs[i].properties.color_picker_open;
|
!self.tabs[i].properties.color_picker_open;
|
||||||
if self.tabs[i].properties.color_picker_open {
|
if self.tabs[i].properties.color_picker_open {
|
||||||
self.tabs[i].properties.color_palette_open = false;
|
self.tabs[i].properties.color_palette_open = false;
|
||||||
|
self.tabs[i].properties.hatch_pattern_picker_open = false;
|
||||||
|
self.tabs[i].properties.hatch_pattern_search.clear();
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3382,6 +3488,8 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].properties.color_picker_open = false;
|
self.tabs[i].properties.color_picker_open = false;
|
||||||
self.tabs[i].properties.color_palette_open = false;
|
self.tabs[i].properties.color_palette_open = false;
|
||||||
|
self.tabs[i].properties.hatch_pattern_picker_open = false;
|
||||||
|
self.tabs[i].properties.hatch_pattern_search.clear();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,34 @@ fn viewcube_has_room(bar_w: f32, tile_w: f32) -> bool {
|
||||||
bar_w + VIEWCUBE_GAP + VIEWCUBE_REGION_PX + VIEWCUBE_PAD <= tile_w
|
bar_w + VIEWCUBE_GAP + VIEWCUBE_REGION_PX + VIEWCUBE_PAD <= tile_w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hatch_pattern_key_event(
|
||||||
|
event: iced::Event,
|
||||||
|
status: iced::event::Status,
|
||||||
|
_window: window::Id,
|
||||||
|
) -> Option<Message> {
|
||||||
|
if !matches!(status, iced::event::Status::Captured) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let iced::Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) = event else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match key {
|
||||||
|
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
|
||||||
|
Some(Message::PropHatchPatternNavigate(-1))
|
||||||
|
}
|
||||||
|
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
|
||||||
|
Some(Message::PropHatchPatternNavigate(1))
|
||||||
|
}
|
||||||
|
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
|
||||||
|
Some(Message::PropHatchPatternNavigate(-2))
|
||||||
|
}
|
||||||
|
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
|
||||||
|
Some(Message::PropHatchPatternNavigate(2))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
|
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
|
||||||
/// so wrap it locally with a friendly name renderer.
|
/// so wrap it locally with a friendly name renderer.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -1658,6 +1686,14 @@ impl OpenCADStudio {
|
||||||
let single_instance = crate::io::single_instance::subscribe().map(Message::OpenExternal);
|
let single_instance = crate::io::single_instance::subscribe().map(Message::OpenExternal);
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let single_instance = Subscription::none();
|
let single_instance = Subscription::none();
|
||||||
|
let hatch_pattern_keys = if self.tabs[self.active_tab]
|
||||||
|
.properties
|
||||||
|
.hatch_pattern_picker_open
|
||||||
|
{
|
||||||
|
event::listen_with(hatch_pattern_key_event)
|
||||||
|
} else {
|
||||||
|
Subscription::none()
|
||||||
|
};
|
||||||
iced::Subscription::batch([
|
iced::Subscription::batch([
|
||||||
frames,
|
frames,
|
||||||
history_tick,
|
history_tick,
|
||||||
|
|
@ -1668,6 +1704,7 @@ impl OpenCADStudio {
|
||||||
web_fonts,
|
web_fonts,
|
||||||
autosave,
|
autosave,
|
||||||
single_instance,
|
single_instance,
|
||||||
|
hatch_pattern_keys,
|
||||||
event::listen_with(|ev, status, win_id| {
|
event::listen_with(|ev, status, win_id| {
|
||||||
use iced::event::Status;
|
use iced::event::Status;
|
||||||
match ev {
|
match ev {
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,6 @@ pub fn find(name: &str) -> Option<&'static PatternEntry> {
|
||||||
catalog().iter().find(|e| e.name.eq_ignore_ascii_case(name))
|
catalog().iter().find(|e| e.name.eq_ignore_ascii_case(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn names() -> Vec<String> {
|
|
||||||
catalog().iter().map(|e| e.name.clone()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── DXF export ────────────────────────────────────────────────────────────
|
// ── DXF export ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn build_dxf_pattern(entry: &PatternEntry) -> DxfPattern {
|
pub fn build_dxf_pattern(entry: &PatternEntry) -> DxfPattern {
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,24 @@
|
||||||
//! • Geometry → text_input per coordinate / dimension field
|
//! • Geometry → text_input per coordinate / dimension field
|
||||||
|
|
||||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||||
use std::fmt;
|
use std::{fmt, sync::Arc};
|
||||||
|
|
||||||
use crate::ui::ROW_H;
|
use crate::ui::ROW_H;
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use iced::widget::{button, column, combo_box, container, row, scrollable, text, text_input};
|
use iced::widget::{
|
||||||
use iced::{Background, Border, Color, Element, Length, Padding, Theme};
|
button, canvas, column, combo_box, container, mouse_area, row, scrollable, text, text_input,
|
||||||
|
};
|
||||||
|
use iced::{
|
||||||
|
mouse, Background, Border, Color, Element, Length, Padding, Point, Rectangle, Size, Theme,
|
||||||
|
};
|
||||||
|
|
||||||
// ── Row-height-derived constants ─────────────────────────────────────────
|
// ── Row-height-derived constants ─────────────────────────────────────────
|
||||||
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px
|
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px
|
||||||
const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0; // fills combo to ROW_H
|
const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0; // fills combo to ROW_H
|
||||||
const SWATCH_SZ: f32 = ROW_H * 0.54; // ≈14 px color swatch
|
const SWATCH_SZ: f32 = ROW_H * 0.54; // ≈14 px color swatch
|
||||||
|
const PATTERN_CARD_W: f32 = 158.0;
|
||||||
|
const PATTERN_PREVIEW_H: f32 = 58.0;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::scene::model::object::{PropSection, PropValue};
|
use crate::scene::model::object::{PropSection, PropValue};
|
||||||
|
|
@ -72,6 +78,124 @@ impl fmt::Display for SelectionGroup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct HatchPatternPreview {
|
||||||
|
pattern: crate::scene::model::hatch_model::HatchPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl canvas::Program<Message> for HatchPatternPreview {
|
||||||
|
type State = ();
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_state: &(),
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
bounds: Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> Vec<canvas::Geometry> {
|
||||||
|
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
|
||||||
|
|
||||||
|
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
let pad = 4.0;
|
||||||
|
let sample = canvas::Path::rectangle(
|
||||||
|
Point::new(pad, pad),
|
||||||
|
Size::new(
|
||||||
|
(bounds.width - pad * 2.0).max(0.0),
|
||||||
|
(bounds.height - pad * 2.0).max(0.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
frame.fill(&sample, palette.background.base.color);
|
||||||
|
|
||||||
|
match &self.pattern {
|
||||||
|
HatchPattern::Solid => {
|
||||||
|
frame.fill(&sample, palette.background.base.text.scale_alpha(0.72));
|
||||||
|
}
|
||||||
|
HatchPattern::Gradient { .. } => {
|
||||||
|
frame.fill(&sample, palette.primary.weak.color);
|
||||||
|
}
|
||||||
|
HatchPattern::Pattern(_) => {
|
||||||
|
let model = HatchModel {
|
||||||
|
world_origin: [0.0, 0.0],
|
||||||
|
boundary: Arc::new(vec![
|
||||||
|
[pad, pad],
|
||||||
|
[bounds.width - pad, pad],
|
||||||
|
[bounds.width - pad, bounds.height - pad],
|
||||||
|
[pad, bounds.height - pad],
|
||||||
|
]),
|
||||||
|
boundary_wcs: None,
|
||||||
|
pattern: self.pattern.clone(),
|
||||||
|
name: String::new(),
|
||||||
|
color: [1.0; 4],
|
||||||
|
angle_offset: 0.0,
|
||||||
|
scale: hatch_preview_scale(&self.pattern),
|
||||||
|
draw_depth: 0.0,
|
||||||
|
};
|
||||||
|
let stroke = canvas::Stroke::default()
|
||||||
|
.with_color(palette.background.base.text)
|
||||||
|
.with_width(1.0);
|
||||||
|
for segment in model.pattern_segments() {
|
||||||
|
frame.stroke(
|
||||||
|
&canvas::Path::line(
|
||||||
|
Point::new(segment[0][0] as f32, bounds.height - segment[0][1] as f32),
|
||||||
|
Point::new(segment[1][0] as f32, bounds.height - segment[1][1] as f32),
|
||||||
|
),
|
||||||
|
stroke.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.stroke(
|
||||||
|
&sample,
|
||||||
|
canvas::Stroke::default()
|
||||||
|
.with_color(palette.background.neutral.color)
|
||||||
|
.with_width(1.0),
|
||||||
|
);
|
||||||
|
vec![frame.into_geometry()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hatch_preview_scale(pattern: &crate::scene::model::hatch_model::HatchPattern) -> f32 {
|
||||||
|
use crate::scene::model::hatch_model::HatchPattern;
|
||||||
|
|
||||||
|
let HatchPattern::Pattern(families) = pattern else {
|
||||||
|
return 1.0;
|
||||||
|
};
|
||||||
|
let spacing = families
|
||||||
|
.iter()
|
||||||
|
.filter_map(|family| {
|
||||||
|
let spacing = family.dy.abs();
|
||||||
|
(spacing > 1.0e-4).then_some(spacing)
|
||||||
|
})
|
||||||
|
.fold(f32::INFINITY, f32::min);
|
||||||
|
if spacing.is_finite() {
|
||||||
|
(8.0 / spacing).clamp(0.01, 100.0)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hatch_pattern_matches(
|
||||||
|
entry: &crate::scene::model::hatch_patterns::PatternEntry,
|
||||||
|
search: &str,
|
||||||
|
) -> bool {
|
||||||
|
let query = search.trim();
|
||||||
|
query.is_empty()
|
||||||
|
|| entry.name.to_lowercase().contains(&query.to_lowercase())
|
||||||
|
|| entry.description.to_lowercase().contains(&query.to_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn filtered_hatch_patterns(
|
||||||
|
search: &str,
|
||||||
|
) -> Vec<&'static crate::scene::model::hatch_patterns::PatternEntry> {
|
||||||
|
crate::scene::model::hatch_patterns::catalog()
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| hatch_pattern_matches(entry, search))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// All standard CAD lineweight options for the combobox.
|
/// All standard CAD lineweight options for the combobox.
|
||||||
pub fn lw_options() -> Vec<LwItem> {
|
pub fn lw_options() -> Vec<LwItem> {
|
||||||
[
|
[
|
||||||
|
|
@ -131,7 +255,12 @@ pub struct PropertiesPanel {
|
||||||
pub layer_combo: combo_box::State<String>,
|
pub layer_combo: combo_box::State<String>,
|
||||||
pub lineweight_combo: combo_box::State<LwItem>,
|
pub lineweight_combo: combo_box::State<LwItem>,
|
||||||
pub linetype_combo: combo_box::State<LinetypeItem>,
|
pub linetype_combo: combo_box::State<LinetypeItem>,
|
||||||
pub hatch_pattern_combo: combo_box::State<String>,
|
/// Whether the visual hatch-pattern picker is open.
|
||||||
|
pub hatch_pattern_picker_open: bool,
|
||||||
|
/// Case-insensitive filter for the visual hatch-pattern picker.
|
||||||
|
pub hatch_pattern_search: String,
|
||||||
|
/// Keyboard/hover focus inside the filtered visual pattern grid.
|
||||||
|
pub hatch_pattern_focus: usize,
|
||||||
/// In-progress text edits keyed by `field` name.
|
/// In-progress text edits keyed by `field` name.
|
||||||
pub edit_buf: HashMap<String, String>,
|
pub edit_buf: HashMap<String, String>,
|
||||||
/// Entity handles this panel was built for. `refresh_properties` compares
|
/// Entity handles this panel was built for. `refresh_properties` compares
|
||||||
|
|
@ -177,7 +306,9 @@ impl Default for PropertiesPanel {
|
||||||
layer_combo: combo_box::State::new(vec![]),
|
layer_combo: combo_box::State::new(vec![]),
|
||||||
lineweight_combo: combo_box::State::new(lw_options()),
|
lineweight_combo: combo_box::State::new(lw_options()),
|
||||||
linetype_combo: combo_box::State::new(vec![]),
|
linetype_combo: combo_box::State::new(vec![]),
|
||||||
hatch_pattern_combo: combo_box::State::new(crate::scene::model::hatch_patterns::names()),
|
hatch_pattern_picker_open: false,
|
||||||
|
hatch_pattern_search: String::new(),
|
||||||
|
hatch_pattern_focus: 0,
|
||||||
edit_buf: HashMap::default(),
|
edit_buf: HashMap::default(),
|
||||||
source_handles: vec![],
|
source_handles: vec![],
|
||||||
color_picker_open: false,
|
color_picker_open: false,
|
||||||
|
|
@ -865,38 +996,142 @@ impl PropertiesPanel {
|
||||||
label: &'a str,
|
label: &'a str,
|
||||||
current: &'a str,
|
current: &'a str,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let selected = if current == VARIES_LABEL {
|
let head = button(
|
||||||
None
|
row![
|
||||||
} else {
|
text(crate::ui::text_util::elide(current, 16))
|
||||||
Some(current.to_string())
|
.size(FONT_SZ)
|
||||||
};
|
.width(Length::Fill),
|
||||||
// iced hides `selected` while the combo has focus and shows its
|
if self.hatch_pattern_picker_open {
|
||||||
// placeholder instead. Use the real pattern name for a single hatch;
|
crate::ui::icons::themed_arrow_up(FONT_SZ)
|
||||||
// a fixed VARIES placeholder made a correct ANSI31 value look mixed as
|
} else {
|
||||||
// soon as the dropdown opened (#476).
|
crate::ui::icons::themed_arrow_down(FONT_SZ)
|
||||||
let placeholder = if current == VARIES_LABEL {
|
},
|
||||||
VARIES_LABEL
|
]
|
||||||
} else {
|
.align_y(iced::Center),
|
||||||
current
|
|
||||||
};
|
|
||||||
let combo = combo_box(
|
|
||||||
&self.hatch_pattern_combo,
|
|
||||||
placeholder,
|
|
||||||
selected.as_ref(),
|
|
||||||
Message::PropHatchPatternChanged,
|
|
||||||
)
|
)
|
||||||
.size(FONT_SZ)
|
.on_press(Message::PropHatchPatternPickerToggle(current.to_string()))
|
||||||
.padding(Padding {
|
.style(move |theme: &Theme, status| {
|
||||||
top: COMBO_PAD_V,
|
let palette = theme.extended_palette();
|
||||||
bottom: COMBO_PAD_V,
|
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||||
left: 6.0,
|
button::Style {
|
||||||
right: 6.0,
|
background: Some(Background::Color(if hovered {
|
||||||
|
palette.background.weak.color
|
||||||
|
} else {
|
||||||
|
palette.background.base.color
|
||||||
|
})),
|
||||||
|
text_color: palette.background.base.text,
|
||||||
|
border: Border {
|
||||||
|
color: if self.hatch_pattern_picker_open {
|
||||||
|
palette.primary.base.color
|
||||||
|
} else {
|
||||||
|
palette.background.neutral.color
|
||||||
|
},
|
||||||
|
width: 1.0,
|
||||||
|
radius: 2.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.input_style(combo_input_style)
|
.padding([COMBO_PAD_V, 6.0])
|
||||||
.on_open(Message::PropColorPickerClose)
|
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
prop_row_widget(label, combo.into())
|
if !self.hatch_pattern_picker_open {
|
||||||
|
return prop_row_widget(label, head.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let search = text_input("Search patterns…", &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 = canvas(HatchPatternPreview {
|
||||||
|
pattern: entry.gpu.clone(),
|
||||||
|
})
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(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.extended_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("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::Fixed(300.0))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
let popup = container(column![search, results].spacing(7))
|
||||||
|
.style(container::bordered_box)
|
||||||
|
.padding(8)
|
||||||
|
.width(348)
|
||||||
|
.max_height(360.0);
|
||||||
|
|
||||||
|
prop_row_widget(
|
||||||
|
label,
|
||||||
|
crate::ui::color_select::floating_below(head.into(), popup.into()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1491,3 +1726,26 @@ fn hint_text_style(theme: &Theme) -> iced::widget::text::Style {
|
||||||
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.48)),
|
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.48)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{hatch_pattern_matches, hatch_preview_scale};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatch_picker_filters_names_and_descriptions() {
|
||||||
|
let ansi31 = crate::scene::model::hatch_patterns::find("ANSI31").unwrap();
|
||||||
|
|
||||||
|
assert!(hatch_pattern_matches(ansi31, "ansi"));
|
||||||
|
assert!(hatch_pattern_matches(ansi31, &ansi31.description));
|
||||||
|
assert!(!hatch_pattern_matches(ansi31, "definitely-not-a-pattern"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatch_preview_scale_is_finite_and_visible() {
|
||||||
|
let ansi31 = crate::scene::model::hatch_patterns::find("ANSI31").unwrap();
|
||||||
|
let scale = hatch_preview_scale(&ansi31.gpu);
|
||||||
|
|
||||||
|
assert!(scale.is_finite());
|
||||||
|
assert!((0.01..=100.0).contains(&scale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue