mirror of
https://github.com/aphrody-labs/cadcraft.git
synced 2026-10-13 19:26:54 +00:00
Squash of PR #679 by @aldozar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2f8782cc2
commit
0675648de8
19 files changed
+324
-15
No files matched your search
@@ -1,6 +1,7 @@
|
||||
//! HATCH, GRADIENT, BOUNDARY, REGION: boundaries from pick points or selected closed objects.
|
||||
|
||||
mod patfile;
|
||||
pub use patfile::{PatternChoice, pattern_choices};
|
||||
pub(super) use patfile::{lines_for, run_patterns};
|
||||
|
||||
use cadcraft_color::Color;
|
||||
@@ -10,7 +11,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use super::helpers::lwpoly;
|
||||
use super::*;
|
||||
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
||||
use crate::{Accept, EngineError, Input, Interactive, Picker, Prompt, Result, Session, Step};
|
||||
|
||||
mod assoc;
|
||||
mod edit;
|
||||
@@ -582,7 +583,11 @@ impl Interactive for HatchM {
|
||||
fn prompt(&self, s: &Session) -> Prompt {
|
||||
// The sub-prompts offer the current value: Enter keeps it.
|
||||
match self.asking {
|
||||
Some(HatchAsk::Pattern) => return Prompt::new("Enter a pattern name", Accept::TEXT).default(self.settings.pattern.clone()),
|
||||
Some(HatchAsk::Pattern) => {
|
||||
return Prompt::new("Enter a pattern name", Accept::TEXT)
|
||||
.default(self.settings.pattern.clone())
|
||||
.picker(Picker::HatchPattern(self.settings.pattern.clone()));
|
||||
}
|
||||
Some(HatchAsk::Scale) => {
|
||||
return Prompt::new("Specify a scale for the hatch pattern", Accept::NUMBER).default(format!("{:.4}", self.settings.scale));
|
||||
}
|
||||
@@ -596,8 +601,10 @@ impl Interactive for HatchM {
|
||||
Some(HatchAsk::Origin) => return Prompt::new("Specify the pattern origin", Accept::POINT).kw(&ORIGIN_KW).default("Use current"),
|
||||
None => {}
|
||||
}
|
||||
// The pattern picker stays offered while boundaries are picked: a choice sets the pattern.
|
||||
let picker = |p: Prompt| if self.gradient { p } else { p.picker(Picker::HatchPattern(self.settings.pattern.clone())) };
|
||||
if self.selecting {
|
||||
return Prompt::new("Select objects", Accept::SELECT).kw(&[
|
||||
return picker(Prompt::new("Select objects", Accept::SELECT).kw(&[
|
||||
"picK internal point",
|
||||
"Undo",
|
||||
"seTtings",
|
||||
@@ -605,9 +612,9 @@ impl Interactive for HatchM {
|
||||
"Angle",
|
||||
"Origin",
|
||||
"sEparate",
|
||||
]);
|
||||
]));
|
||||
}
|
||||
Prompt::new("Pick internal point", Accept::POINT).kw(&[
|
||||
picker(Prompt::new("Pick internal point", Accept::POINT).kw(&[
|
||||
"Select objects",
|
||||
"Undo",
|
||||
"seTtings",
|
||||
@@ -616,7 +623,7 @@ impl Interactive for HatchM {
|
||||
"Angle",
|
||||
"Origin",
|
||||
"sEparate",
|
||||
])
|
||||
]))
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if let Some(a) = self.asking.take() {
|
||||
@@ -670,6 +677,11 @@ impl Interactive for HatchM {
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
// A pattern chosen in the picker while boundaries are picked.
|
||||
Input::Text(t) if !self.gradient => {
|
||||
self.answer(s, HatchAsk::Pattern, &t)?;
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
// Enter ends boundary picking in either mode and creates the hatch.
|
||||
Input::Enter => self.finish(s),
|
||||
_ => Ok(Step::Continue),
|
||||
@@ -738,6 +750,43 @@ mod tests {
|
||||
assert!(hatch.gradient.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pattern_picker_is_offered_and_its_choice_sets_the_pattern() {
|
||||
let mut s = Session::new();
|
||||
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
|
||||
s.start("hatch").unwrap();
|
||||
let picker = s.current_prompt().and_then(|p| p.picker);
|
||||
assert_eq!(picker, Some(Picker::HatchPattern(HatchSettings::from(&s, &Value::Null, false).pattern)));
|
||||
// A choice made in the picker while boundaries are picked.
|
||||
s.input(Input::Text("AR-CONC".into())).unwrap();
|
||||
assert_eq!(s.current_prompt().and_then(|p| p.picker), Some(Picker::HatchPattern("AR-CONC".into())));
|
||||
// And at the pattern prompt.
|
||||
s.input(Input::Keyword("Pattern".into())).unwrap();
|
||||
assert!(s.current_prompt().unwrap().picker.is_some());
|
||||
s.input(Input::Text("BRICK".into())).unwrap();
|
||||
s.input(Input::Point(Vec2::new(5.0, 5.0))).unwrap();
|
||||
s.input(Input::Enter).unwrap();
|
||||
assert_eq!(only_hatch(&s).1.pattern, "BRICK");
|
||||
// An unknown name keeps the pattern.
|
||||
s.start("hatch").unwrap();
|
||||
s.input(Input::Text("NO-SUCH".into())).unwrap();
|
||||
assert_eq!(s.current_prompt().and_then(|p| p.picker), Some(Picker::HatchPattern("BRICK".into())));
|
||||
// GRADIENT offers no pattern picker.
|
||||
s.cancel();
|
||||
s.start("gradient").unwrap();
|
||||
assert!(s.current_prompt().unwrap().picker.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_choices_list_the_library_with_its_lines() {
|
||||
let all = pattern_choices();
|
||||
assert_eq!(all.first().map(|c| c.name.as_str()), Some("SOLID"));
|
||||
assert!(all[0].lines.is_empty());
|
||||
let ansi31 = all.iter().find(|c| c.name == "ANSI31").unwrap();
|
||||
assert_eq!(ansi31.lines.len(), 1);
|
||||
assert!(!ansi31.description.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_picks_and_handles_in_one_region_do_not_produce_duplicate_loops() {
|
||||
// One internal point
|
||||
|
||||
@@ -10,7 +10,7 @@ use super::assoc::{detach, same_shape};
|
||||
use super::{ORIGIN_KW, OriginMode, closed_loop, island_style};
|
||||
use crate::cmd::helpers::lwpoly;
|
||||
use crate::cmd::{bad, bool_or, point_param, str_param, targets};
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
use crate::{Accept, Input, Interactive, Picker, Prompt, Result, Session, Step};
|
||||
|
||||
/// Upper bound on hatches edited by one call.
|
||||
const MAX_EDITED: usize = 10_000;
|
||||
@@ -328,7 +328,10 @@ impl Interactive for HatchEditM {
|
||||
};
|
||||
Prompt::new("Island detection style", Accept::TEXT).kw(&["Ignore", "Outer", "Normal"]).default(cur)
|
||||
}
|
||||
Ask::Pattern => Prompt::new("Enter a pattern name", Accept::TEXT).default(h.map(|h| h.pattern).unwrap_or_default()),
|
||||
Ask::Pattern => {
|
||||
let current = h.map(|h| h.pattern).unwrap_or_default();
|
||||
Prompt::new("Enter a pattern name", Accept::TEXT).default(current.clone()).picker(Picker::HatchPattern(current))
|
||||
}
|
||||
Ask::Scale => Prompt::new("Specify a scale for the hatch pattern", Accept::NUMBER).default(format!("{:.4}", h.map_or(1.0, |h| h.scale))),
|
||||
Ask::Angle => Prompt::new("Specify an angle for the hatch pattern", Accept::NUMBER)
|
||||
.default(format!("{:.0}", h.map_or(0.0, |h| h.angle.to_degrees()))),
|
||||
|
||||
@@ -44,6 +44,25 @@ pub(crate) fn names() -> Vec<String> {
|
||||
v
|
||||
}
|
||||
|
||||
/// A pattern offered for new hatches: a library pattern or one a `.pat` file loaded.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PatternChoice {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
/// The line families at unit scale (none for SOLID).
|
||||
pub lines: Vec<PatternLine>,
|
||||
}
|
||||
|
||||
/// Every pattern offered for new hatches, in the order of [`names`], for a pattern picker.
|
||||
pub fn pattern_choices() -> Vec<PatternChoice> {
|
||||
let mut v: Vec<PatternChoice> = standard_patterns()
|
||||
.into_iter()
|
||||
.map(|p| PatternChoice { name: p.name.to_string(), description: p.description.to_string(), lines: p.lines })
|
||||
.collect();
|
||||
v.extend(store().iter().map(|p| PatternChoice { name: p.name.clone(), description: p.description.clone(), lines: p.lines.clone() }));
|
||||
v
|
||||
}
|
||||
|
||||
/// Add the patterns of a `.pat` file's text (a pattern loaded before under the same name is
|
||||
/// replaced). Returns the names added and the file's problems.
|
||||
pub(crate) fn load(text: &str) -> (Vec<String>, Vec<String>) {
|
||||
|
||||
@@ -37,8 +37,9 @@ use serde_json::Value;
|
||||
pub use cadcraft_doc as doc;
|
||||
pub use cadcraft_geom as geom;
|
||||
pub use cadcraft_render as render;
|
||||
pub use cmd::hatch::{PatternChoice, pattern_choices};
|
||||
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
|
||||
pub use prompt::{Accept, Drag, Input, Interactive, Prompt, Step};
|
||||
pub use prompt::{Accept, Drag, Input, Interactive, Picker, Prompt, Step};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EngineError {
|
||||
|
||||
@@ -45,17 +45,34 @@ pub struct Prompt {
|
||||
/// index): Dynamic Input shows its dimensions ([`crate::grip_dims`]).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grip: Option<(Handle, usize)>,
|
||||
/// A visual chooser the UI can show for this prompt; a choice made in it is entered as
|
||||
/// [`Input::Text`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub picker: Option<Picker>,
|
||||
}
|
||||
|
||||
/// A visual chooser for a prompt ([`Prompt::picker`]).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Picker {
|
||||
/// A hatch pattern by name (see [`crate::pattern_choices`]); holds the current pattern.
|
||||
HatchPattern(String),
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
pub fn new(message: impl Into<String>, accept: Accept) -> Self {
|
||||
Prompt { message: message.into(), keywords: Vec::new(), default: None, accept, base: None, deferred: false, grip: None }
|
||||
Prompt { message: message.into(), keywords: Vec::new(), default: None, accept, base: None, deferred: false, grip: None, picker: None }
|
||||
}
|
||||
/// The grip being stretched (the `grip` field).
|
||||
pub fn grip(mut self, g: (Handle, usize)) -> Self {
|
||||
self.grip = Some(g);
|
||||
self
|
||||
}
|
||||
/// The visual chooser the UI can show (the `picker` field).
|
||||
pub fn picker(mut self, p: Picker) -> Self {
|
||||
self.picker = Some(p);
|
||||
self
|
||||
}
|
||||
pub fn kw(mut self, k: &[&str]) -> Self {
|
||||
self.keywords = k.iter().map(|s| s.to_string()).collect();
|
||||
self
|
||||
|
||||
@@ -349,6 +349,41 @@ pub fn pattern_lines(d: &Drawing, h: &Hatch, loops: &[Vec<Vec2>], px: f64, view:
|
||||
out
|
||||
}
|
||||
|
||||
/// Repeats of a pattern across a swatch ([`pattern_swatch`]).
|
||||
const SWATCH_REPEATS: f64 = 6.0;
|
||||
/// Most runs a swatch holds.
|
||||
const MAX_SWATCH_RUNS: usize = 4000;
|
||||
|
||||
/// A preview of a pattern for a pattern picker: the runs of its line families `lines` inside the
|
||||
/// `side` × `side` square with a corner at the origin, at a scale that shows a few repeats of the
|
||||
/// pattern (each family's line spacing or dash repeat, whichever is longer). Empty for a pattern
|
||||
/// with no lines (SOLID).
|
||||
pub fn pattern_swatch(lines: &[PatternLine], side: f64) -> Vec<Vec<Vec2>> {
|
||||
let repeat =
|
||||
lines.iter().map(|l| l.delta.1.abs().max(l.dashes.iter().map(|d| d.abs()).sum::<f64>())).filter(|r| r.is_finite()).fold(0.0, f64::max);
|
||||
if !side.is_finite() || side <= 0.0 || repeat < 1e-9 {
|
||||
return Vec::new();
|
||||
}
|
||||
let h = Hatch {
|
||||
pattern: String::new(),
|
||||
solid: false,
|
||||
loops: Vec::new(),
|
||||
scale: side / (SWATCH_REPEATS * repeat),
|
||||
angle: 0.0,
|
||||
associative: false,
|
||||
style: 0,
|
||||
elevation: 0.0,
|
||||
gradient: None,
|
||||
origin: Vec2::ZERO,
|
||||
background: None,
|
||||
pattern_lines: lines.to_vec(),
|
||||
};
|
||||
let square = vec![Vec2::new(0.0, 0.0), Vec2::new(side, 0.0), Vec2::new(side, side), Vec2::new(0.0, side)];
|
||||
let mut runs = pattern_lines(&Drawing::default(), &h, &[square], 0.0, None);
|
||||
runs.truncate(MAX_SWATCH_RUNS);
|
||||
runs
|
||||
}
|
||||
|
||||
/// One pattern line family, scaled and placed: parallel lines `spacing` apart through `origin`
|
||||
/// at angle `ang`, each shifted along itself by `delta`'s along-line part from the last.
|
||||
struct Family<'a> {
|
||||
|
||||
@@ -31,6 +31,7 @@ use pen::Pen;
|
||||
|
||||
pub use dim::{ArrowGeom, Arrowhead, DimGeometry, DimText, LineRole, arrowhead, dimension_geometry, dimension_geometry_with, format_linear_value};
|
||||
pub use fill::triangulate_evenodd;
|
||||
pub use hatch::pattern_swatch;
|
||||
pub use paper::{PAPER_SIZES, PaperSize, Sheet, paper_size, sheet};
|
||||
pub use tolerance::{gdt_points, tolerance_in};
|
||||
|
||||
|
||||
@@ -165,6 +165,23 @@ fn pattern_lines_from_a_file_are_drawn_for_unknown_names() {
|
||||
assert!(((p[1] - p[0]).angle().to_degrees().rem_euclid(180.0) - 45.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_swatches_stay_in_their_square_and_show_a_few_repeats() {
|
||||
for p in cadcraft_doc::library::standard_patterns() {
|
||||
let runs = cadcraft_render::pattern_swatch(&p.lines, 1.0);
|
||||
if p.lines.is_empty() {
|
||||
assert!(runs.is_empty(), "{}", p.name);
|
||||
continue;
|
||||
}
|
||||
assert!(runs.len() >= 3, "{} has {} runs", p.name, runs.len());
|
||||
assert!(runs.len() <= 4000, "{} has {} runs", p.name, runs.len());
|
||||
for v in runs.iter().flatten() {
|
||||
assert!((-1e-9..=1.0 + 1e-9).contains(&v.x) && (-1e-9..=1.0 + 1e-9).contains(&v.y), "{} at {v:?}", p.name);
|
||||
}
|
||||
}
|
||||
assert!(cadcraft_render::pattern_swatch(&[], 1.0).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brick_joints_are_drawn_once_half_a_brick_apart_course_to_course() {
|
||||
let l = draw(hatch("BRICK", vec![square(0.0, 2.0)]), "0");
|
||||
|
||||
@@ -1206,3 +1206,5 @@ menu View Zobrazení
|
||||
menu Draw Kreslení
|
||||
menu Dimension Kótování
|
||||
menu Modify Modifikace
|
||||
Hatch Pattern Vzor šrafy
|
||||
Search for pattern Hledat vzor
|
||||
|
@@ -1013,3 +1013,5 @@
|
||||
Calibrate Kalibrieren
|
||||
Move sideways to pan; push/pull or twist to zoom. Seitlich bewegen zum Verschieben; drücken, ziehen oder drehen zum Zoomen.
|
||||
Left device button: zoom extents. Right device button: calibrate. Linke Gerätetaste: auf Grenzen zoomen. Rechte Gerätetaste: kalibrieren.
|
||||
Hatch Pattern Schraffurmuster
|
||||
Search for pattern Muster suchen
|
||||
|
@@ -1232,3 +1232,5 @@
|
||||
Calibrate Calibrar
|
||||
Move sideways to pan; push/pull or twist to zoom. Mueve el mando lateralmente para encuadrar; empuja, tira o gira para hacer zoom.
|
||||
Left device button: zoom extents. Right device button: calibrate. Botón izquierdo del dispositivo: zoom a la extensión. Botón derecho: calibrar.
|
||||
Hatch Pattern Patrón de sombreado
|
||||
Search for pattern Buscar patrón
|
||||
|
@@ -996,3 +996,5 @@
|
||||
Import... Importer...
|
||||
Export... Exporter...
|
||||
Batch Publish... Publication par lots...
|
||||
Hatch Pattern Motif de hachure
|
||||
Search for pattern Rechercher un motif
|
||||
|
@@ -995,3 +995,5 @@
|
||||
Hide Others Ocultar outros
|
||||
Accessibility Acessibilidade
|
||||
Show System Cursor Exibir cursor do sistema
|
||||
Hatch Pattern Padrão de hachura
|
||||
Search for pattern Pesquisar padrão
|
||||
|
@@ -996,3 +996,5 @@
|
||||
Import... Импорт...
|
||||
Export... Экспорт...
|
||||
Batch Publish... Пакетная публикация...
|
||||
Hatch Pattern Образец штриховки
|
||||
Search for pattern Поиск образца
|
||||
|
@@ -94,6 +94,7 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
|
||||
include_str!("../cmdline.rs"),
|
||||
include_str!("../credits.rs"),
|
||||
include_str!("../palettes/quickcalc.rs"),
|
||||
include_str!("../palettes/patterns.rs"),
|
||||
include_str!("../dialogs.rs"),
|
||||
include_str!("../layers.rs"),
|
||||
include_str!("../canvas/compare.rs"),
|
||||
|
||||
@@ -1230,4 +1230,6 @@
|
||||
Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off. Креслення, що ще не мають файлу, зберігаються в типовій версії. 0 хвилин вимикає автозбереження.
|
||||
Time-sensitive right-click: a quick click is Enter, holding opens the menu Клік правою кнопкою з урахуванням часу: швидкий клік — Enter, утримання відкриває меню
|
||||
Turned off, a right-click while a command runs always opens the shortcut menu. Якщо вимкнено, клік правою кнопкою під час команди завжди відкриває контекстне меню.
|
||||
Hatch Pattern Зразок штрихування
|
||||
Search for pattern Пошук зразка
|
||||
Run Script... Виконати сценарій...
|
||||
|
@@ -498,6 +498,7 @@ impl CadApp {
|
||||
}
|
||||
});
|
||||
dialogs::show(self, ui.ctx());
|
||||
palettes::patterns::prompt_picker(self, ui.ctx());
|
||||
closing::prompt(self, ui.ctx());
|
||||
autosave::show(self, ui.ctx());
|
||||
autosave::gpu_notice(self, ui.ctx());
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::CadApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// The hatch pattern picker.
|
||||
pub mod patterns;
|
||||
/// The QuickCalc palette.
|
||||
pub mod quickcalc;
|
||||
|
||||
@@ -1014,11 +1016,9 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
EntityKind::Hatch(hh) => {
|
||||
prop_row(ui, "Pattern", |ui| {
|
||||
ui.menu_button(hh.pattern.clone(), |ui| {
|
||||
for pat in cadcraft_doc::library::standard_patterns() {
|
||||
if ui.button(pat.name).clicked() {
|
||||
set = Some(json!({ "handles": ids, "pattern": pat.name }));
|
||||
ui.close();
|
||||
}
|
||||
if let Some(name) = patterns::menu(ui, &hh.pattern) {
|
||||
set = Some(json!({ "handles": ids, "pattern": name }));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! The hatch pattern picker: a swatch of every pattern offered for new hatches, beside HATCH's
|
||||
//! and HATCHEDIT's prompts and in the Properties palette, so a pattern is chosen by its look
|
||||
//! instead of by typing its name.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_engine::geom::Vec2;
|
||||
use cadcraft_engine::{Input, PatternChoice, Picker, pattern_choices};
|
||||
use egui::{Align2, Rect, Sense, Stroke, pos2, vec2};
|
||||
|
||||
use crate::CadApp;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// A swatch's side, in points.
|
||||
const CELL: f32 = 56.0;
|
||||
/// Room under a swatch for the pattern's name.
|
||||
const LABEL_H: f32 = 16.0;
|
||||
/// Space between swatches.
|
||||
const GAP: f32 = 6.0;
|
||||
/// Swatches per row in the Properties palette's menu.
|
||||
const MENU_COLUMNS: f32 = 6.0;
|
||||
|
||||
/// A pattern as the picker draws it: its runs in the unit square (y up), or a solid fill.
|
||||
struct Swatch {
|
||||
name: String,
|
||||
description: String,
|
||||
runs: Vec<Vec<Vec2>>,
|
||||
solid: bool,
|
||||
}
|
||||
|
||||
/// The patterns' names and line family counts, and their swatches.
|
||||
type Swatches = (Vec<(String, usize)>, Arc<Vec<Swatch>>);
|
||||
|
||||
thread_local! {
|
||||
static SWATCHES: RefCell<Swatches> = RefCell::new((Vec::new(), Arc::new(Vec::new())));
|
||||
}
|
||||
|
||||
/// The swatches of the patterns offered now, made again only when the list changes (a `.pat`
|
||||
/// file loaded more patterns).
|
||||
fn swatches() -> Arc<Vec<Swatch>> {
|
||||
let choices = pattern_choices();
|
||||
let key: Vec<(String, usize)> = choices.iter().map(|c| (c.name.clone(), c.lines.len())).collect();
|
||||
SWATCHES.with(|cell| {
|
||||
let mut cache = cell.borrow_mut();
|
||||
if cache.0 != key {
|
||||
*cache = (key, Arc::new(choices.into_iter().map(swatch).collect()));
|
||||
}
|
||||
cache.1.clone()
|
||||
})
|
||||
}
|
||||
|
||||
fn swatch(c: PatternChoice) -> Swatch {
|
||||
let solid = c.lines.is_empty();
|
||||
Swatch { runs: cadcraft_engine::render::pattern_swatch(&c.lines, 1.0), name: c.name, description: c.description, solid }
|
||||
}
|
||||
|
||||
/// Draw one swatch in `r` (the square and the name under it).
|
||||
fn paint(p: &egui::Painter, r: Rect, s: &Swatch, selected: bool, hovered: bool) {
|
||||
let t = Tokens::get();
|
||||
let square = Rect::from_min_size(r.min, vec2(CELL, CELL));
|
||||
p.rect_filled(square, 3.0, t.field);
|
||||
let inner = square.shrink(3.0);
|
||||
if s.solid {
|
||||
p.rect_filled(inner, 1.0, t.text);
|
||||
} else {
|
||||
let at = |v: &Vec2| pos2(inner.min.x + v.x as f32 * inner.width(), inner.max.y - v.y as f32 * inner.height());
|
||||
let stroke = Stroke::new(1.0, t.text);
|
||||
for run in &s.runs {
|
||||
match run.as_slice() {
|
||||
[] => {}
|
||||
[dot] => {
|
||||
p.circle_filled(at(dot), 0.7, t.text);
|
||||
}
|
||||
pts => {
|
||||
p.add(egui::Shape::line(pts.iter().map(at).collect(), stroke));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let border = match (selected, hovered) {
|
||||
(true, _) => Stroke::new(2.0, t.accent),
|
||||
(false, true) => Stroke::new(1.0, t.accent),
|
||||
_ => Stroke::new(1.0, t.border),
|
||||
};
|
||||
p.rect_stroke(square, 3.0, border, egui::StrokeKind::Inside);
|
||||
let label = Rect::from_min_max(pos2(r.min.x, square.max.y), r.max);
|
||||
let color = if selected { t.accent } else { t.text_dim };
|
||||
p.with_clip_rect(label).text(label.center_top() + vec2(0.0, 2.0), Align2::CENTER_TOP, &s.name, crate::theme::small(), color);
|
||||
}
|
||||
|
||||
/// The swatches whose name or description holds `filter`, `current` marked; returns the
|
||||
/// pattern clicked.
|
||||
pub fn grid(ui: &mut egui::Ui, current: &str, filter: &str) -> Option<String> {
|
||||
let filter = filter.trim().to_lowercase();
|
||||
let mut chosen = None;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(GAP, GAP);
|
||||
let shown = swatches();
|
||||
for s in
|
||||
shown.iter().filter(|s| filter.is_empty() || s.name.to_lowercase().contains(&filter) || s.description.to_lowercase().contains(&filter))
|
||||
{
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(CELL, CELL + LABEL_H), Sense::click());
|
||||
if ui.is_rect_visible(r) {
|
||||
paint(ui.painter(), r, s, s.name.eq_ignore_ascii_case(current), resp.hovered());
|
||||
}
|
||||
let tip = if s.description.is_empty() { s.name.clone() } else { format!("{}\n{}", s.name, s.description) };
|
||||
if resp.on_hover_text(tip).clicked() {
|
||||
chosen = Some(s.name.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
chosen
|
||||
}
|
||||
|
||||
/// The Properties palette's pattern menu: the swatch grid, scrolling; returns the pattern
|
||||
/// clicked.
|
||||
pub fn menu(ui: &mut egui::Ui, current: &str) -> Option<String> {
|
||||
ui.set_width(MENU_COLUMNS * (CELL + GAP));
|
||||
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| grid(ui, current, "")).inner
|
||||
}
|
||||
|
||||
/// The picker window while the prompt offers one (HATCH's boundary and pattern prompts,
|
||||
/// HATCHEDIT's pattern prompt): a click enters the pattern's name at the prompt.
|
||||
pub fn prompt_picker(app: &mut CadApp, ctx: &egui::Context) {
|
||||
let Some(Picker::HatchPattern(current)) = app.session.current_prompt().and_then(|p| p.picker) else { return };
|
||||
let filter_id = egui::Id::new("hatch_pattern_filter");
|
||||
let mut filter: String = ctx.data_mut(|d| d.get_temp(filter_id)).unwrap_or_default();
|
||||
let mut chosen = None;
|
||||
let area = ctx.content_rect();
|
||||
egui::Window::new(crate::tl!("Hatch Pattern"))
|
||||
.id(egui::Id::new("Hatch Pattern"))
|
||||
.collapsible(true)
|
||||
.resizable(true)
|
||||
.default_size(vec2(5.0 * (CELL + GAP) + 12.0, 5.0 * (CELL + LABEL_H + GAP) + 40.0))
|
||||
.default_pos(pos2(area.max.x - 5.0 * (CELL + GAP) - 330.0, area.min.y + 120.0))
|
||||
.show(ctx, |ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut filter).hint_text(crate::tl!("Search for pattern")).desired_width(f32::INFINITY));
|
||||
ui.add_space(4.0);
|
||||
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||
chosen = grid(ui, ¤t, &filter);
|
||||
});
|
||||
});
|
||||
ctx.data_mut(|d| d.insert_temp(filter_id, filter));
|
||||
if let Some(name) = chosen {
|
||||
app.cmd.buffer.clear();
|
||||
if let Err(e) = app.session.input(Input::Text(name)) {
|
||||
app.session.echo(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user