mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-31 22:36:51 -06:00
Hatch and Gradient dialog (create and edit) (#753)
* HATCH/GRADIENT: points and objects in one call; GRADIENT name and centered Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * HATCHEDIT: gradient parameter (make or change a gradient fill) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * HATCHPICK: pick hatch boundaries, an origin or hatches for the dialog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hatch dialog: state, defaults and the OK command call Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hatch dialog: pick points, objects, origin or hatch and come back Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hatch and Gradient dialog window (pattern swatches, gradient, origin, boundaries, options) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * HATCH, GRADIENT and HATCHEDIT open the hatch dialog; double-click a hatch to edit; HPDLGMODE Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hatch dialog: keep the window to its content (OK and Cancel stayed off screen); double-click test A vertical separator between the two columns took the height available, so the window grew every frame until its buttons left the screen. Found in the running app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: hatch dialogs in gaps, target-app parity and UI parity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hatch dialog review fixes - Double-clicking a hatch at its centre opened the dialog and also started a grip edit. - Edit mode no longer clamps an out-of-range scale or angle into a change nobody made. - HPORIGINMODE outside 1-5 means the current origin, as HATCH reads it. - Picks made in one space are dropped when the dialog comes back in another (layout tab). - A gradient hatch's Pattern tab offers the current pattern (HPNAME), not SOLID. - Ukrainian: "Default to boundary extents" said "outside the boundary". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: lloyd <lloyd@stackworx.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
lloyd
parent
bcd6322ee9
commit
5183f63bc5
@@ -15,15 +15,22 @@ use crate::{Accept, EngineError, Input, Interactive, Picker, Prompt, Result, Ses
|
||||
|
||||
mod assoc;
|
||||
mod edit;
|
||||
mod pick;
|
||||
pub(crate) use assoc::update as refill_associative;
|
||||
pub use pick::{HatchPicks, PickMode};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("hatch", "Hatch...", run_hatch).menu(&["Draw", "Hatch..."]).alias(&["h", "bh", "-hatch"]).params("{points?: [[x,y]] (internal points) | handles?: [hex] (closed objects), pattern?: \"ANSI31\", scale?, angle? (degrees), associative? (HPASSOC), separate? (HPSEPARATE: a hatch per point/object), origin?: [x,y] (HPORIGIN) | originMode?: \"current\"|\"bottomLeft\"|\"bottomRight\"|\"topLeft\"|\"topRight\"|\"center\" (HPORIGINMODE), style?: \"normal\"|\"outer\"|\"ignore\" (HPISLANDDETECTION)} — gaps up to HPGAPTOL close").interactive(|s| Ok(Box::new(HatchM::new(s, false)))),
|
||||
CommandSpec::new("gradient", "Gradient...", run_gradient).menu(&["Draw", "Gradient..."]).alias(&["gd"]).params("{points? | handles?, color1?, color2?, angle?}").interactive(|s| Ok(Box::new(HatchM::new(s, true)))),
|
||||
CommandSpec::new("hatch", "Hatch...", run_hatch).menu(&["Draw", "Hatch..."]).alias(&["h", "bh", "-hatch"]).params("{points?: [[x,y]] (internal points) and/or handles?: [hex] (closed objects), pattern?: \"ANSI31\", scale?, angle? (degrees), associative? (HPASSOC), separate? (HPSEPARATE: a hatch per point/object), origin?: [x,y] (HPORIGIN) | originMode?: \"current\"|\"bottomLeft\"|\"bottomRight\"|\"topLeft\"|\"topRight\"|\"center\" (HPORIGINMODE), style?: \"normal\"|\"outer\"|\"ignore\" (HPISLANDDETECTION)} — gaps up to HPGAPTOL close").interactive(|s| Ok(Box::new(HatchM::new(s, false)))),
|
||||
CommandSpec::new("gradient", "Gradient...", run_gradient).menu(&["Draw", "Gradient..."]).alias(&["gd"]).params("{points? and/or handles?, name?: \"LINEAR\"|\"CYLINDER\"|\"CURVED\"|\"SPHERICAL\"|\"HEMISPHERICAL\" or INV… (colours swapped), color1?, color2?, angle? (degrees), centered? (default true)}").interactive(|s| Ok(Box::new(HatchM::new(s, true)))),
|
||||
CommandSpec::new("boundary", "Boundary...", run_boundary).menu(&["Draw", "Boundary..."]).alias(&["bo", "bpoly"]).params("{points: [[x,y]]}").interactive(|_| Ok(Box::new(BoundaryM { region: false }))),
|
||||
CommandSpec::new("region", "Region", run_region).menu(&["Draw", "Region"]).alias(&["reg"]).params("{handles?}"),
|
||||
CommandSpec::new("hatchedit", "Hatch Edit", edit::run_hatchedit).menu(&["Modify", "Object", "Hatch..."]).alias(&["he", "-hatchedit"]).params("{handles?, pattern?, scale?, angle?, associative?: bool (false unlinks; true links each loop to a closed object of the same shape, or to `boundary` objects), boundary?: [hex], style?, origin? | originMode?, separate?: true, recreate?: \"polyline\"|\"region\", associate?: bool (link to the recreated boundary)}").interactive(|s| Ok(Box::new(edit::HatchEditM::new(s)))),
|
||||
CommandSpec::new("hatchedit", "Hatch Edit", edit::run_hatchedit).menu(&["Modify", "Object", "Hatch..."]).alias(&["he", "-hatchedit"]).params("{handles?, pattern?, scale?, angle?, gradient?: {name?, color1?, color2?, angle? (degrees), centered?} (makes or changes a gradient fill; `pattern` turns it back), associative?: bool (false unlinks; true links each loop to a closed object of the same shape, or to `boundary` objects), boundary?: [hex], style?, origin? | originMode?, separate?: true, recreate?: \"polyline\"|\"region\", associate?: bool (link to the recreated boundary)}").interactive(|s| Ok(Box::new(edit::HatchEditM::new(s)))),
|
||||
CommandSpec::new("hatchpick", "Hatch Pick", pick::run_hatchpick)
|
||||
.params("{points?: [[x,y]] (internal points), handles?: [hex] (closed objects)} (typed: Points, Objects, oRigin or Hatch, then the picks): the Hatch and Gradient dialog's picks, kept in the session; changes nothing")
|
||||
.enabled(has_doc)
|
||||
.noundo()
|
||||
.interactive(|_| Ok(Box::new(pick::HatchPickM::default()))),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -250,6 +257,17 @@ pub(crate) fn loops_from_handles(s: &Session, hs: &[Handle]) -> Result<Vec<Hatch
|
||||
Ok(loops)
|
||||
}
|
||||
|
||||
/// Gradient fills by name: the shapes the renderer draws (`render/src/hatch.rs`); the INV forms
|
||||
/// swap the two colours.
|
||||
pub(crate) const GRADIENT_NAMES: [&str; 10] =
|
||||
["LINEAR", "CYLINDER", "CURVED", "SPHERICAL", "HEMISPHERICAL", "INVLINEAR", "INVCYLINDER", "INVCURVED", "INVSPHERICAL", "INVHEMISPHERICAL"];
|
||||
|
||||
/// A gradient name as stored (upper case), or None when it isn't one of [`GRADIENT_NAMES`].
|
||||
pub(crate) fn gradient_name(t: &str) -> Option<&'static str> {
|
||||
let up = t.trim().to_ascii_uppercase();
|
||||
GRADIENT_NAMES.iter().copied().find(|n| *n == up)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HatchSettings {
|
||||
pattern: String,
|
||||
@@ -273,11 +291,11 @@ impl HatchSettings {
|
||||
));
|
||||
let pattern = str_param(p, "pattern").map(str::to_ascii_uppercase).unwrap_or(hdr.0);
|
||||
let g = gradient.then(|| Gradient {
|
||||
name: str_param(p, "name").unwrap_or("LINEAR").to_string(),
|
||||
name: str_param(p, "name").and_then(gradient_name).unwrap_or("LINEAR").to_string(),
|
||||
color1: str_param(p, "color1").and_then(Color::parse).unwrap_or(Color::Index(5)),
|
||||
color2: str_param(p, "color2").and_then(Color::parse).unwrap_or(Color::Index(7)),
|
||||
angle: f64_or(p, "angle", 0.0).to_radians(),
|
||||
centered: true,
|
||||
centered: bool_or(p, "centered", true),
|
||||
});
|
||||
HatchSettings {
|
||||
pattern,
|
||||
@@ -333,14 +351,21 @@ fn make(s: &mut Session, p: &Value, gradient: bool) -> Result<Value> {
|
||||
if str_param(p, "originMode").is_some_and(|t| OriginMode::parse(t).is_none()) {
|
||||
return Err(bad(cmd, "unknown `originMode`"));
|
||||
}
|
||||
if gradient && str_param(p, "name").is_some_and(|t| gradient_name(t).is_none()) {
|
||||
return Err(bad(cmd, format!("unknown gradient `name`; use one of {}", GRADIENT_NAMES.join(", "))));
|
||||
}
|
||||
let st = HatchSettings::from(s, p, gradient);
|
||||
// Loops by picked point or selected object: one hatch each when separate.
|
||||
let mut groups: Vec<Vec<HatchLoop>> = Vec::new();
|
||||
if let Some(pts) = points_param(p, "points") {
|
||||
for q in pts {
|
||||
groups.push(loops_at(s, q)?);
|
||||
}
|
||||
} else {
|
||||
// Internal points and boundary objects can come together (the dialog's Pick points and
|
||||
// Select objects); points alone don't fall back to the selection.
|
||||
let pts = points_param(p, "points");
|
||||
let given = |k: &str| p.get(k).is_some_and(|v| !v.is_null());
|
||||
let objects = pts.is_none() || given("handles") || given("handle");
|
||||
for q in pts.into_iter().flatten() {
|
||||
groups.push(loops_at(s, q)?);
|
||||
}
|
||||
if objects {
|
||||
let hs = targets(s, p)?;
|
||||
if st.separate {
|
||||
groups.extend(loops_from_handles(s, &hs)?.into_iter().map(|l| vec![HatchLoop { outer: true, ..l }]));
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
//! associativity (unlink, or link to boundary objects), separate hatches and recreated
|
||||
//! boundaries of existing hatches.
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, Handle, Hatch, HatchLoop, entity_bounds};
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{Drawing, EntityKind, Gradient, Handle, Hatch, HatchLoop, entity_bounds};
|
||||
use cadcraft_geom::{Bounds2, Polyline, Vec2, point_in_polygon};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::assoc::{detach, same_shape};
|
||||
use super::{ORIGIN_KW, OriginMode, closed_loop, island_style};
|
||||
use super::{GRADIENT_NAMES, ORIGIN_KW, OriginMode, closed_loop, gradient_name, island_style};
|
||||
use crate::cmd::helpers::lwpoly;
|
||||
use crate::cmd::{bad, bool_or, point_param, str_param, targets};
|
||||
use crate::{Accept, Input, Interactive, Picker, Prompt, Result, Session, Step};
|
||||
@@ -165,6 +166,87 @@ fn properties(h: &mut Hatch, p: &Value) {
|
||||
}
|
||||
}
|
||||
|
||||
/// HATCHEDIT's `gradient`: the fields to change, all checked before any hatch is touched.
|
||||
struct GradientEdit {
|
||||
name: Option<&'static str>,
|
||||
color1: Option<Color>,
|
||||
color2: Option<Color>,
|
||||
/// Radians.
|
||||
angle: Option<f64>,
|
||||
centered: Option<bool>,
|
||||
}
|
||||
|
||||
impl GradientEdit {
|
||||
/// Make `h` a gradient fill (from the default LINEAR blue-to-white one when it has none) and
|
||||
/// set the given fields.
|
||||
fn apply(&self, h: &mut Hatch) {
|
||||
let mut g = h.gradient.clone().unwrap_or(Gradient {
|
||||
name: "LINEAR".into(),
|
||||
color1: Color::Index(5),
|
||||
color2: Color::Index(7),
|
||||
angle: 0.0,
|
||||
centered: true,
|
||||
});
|
||||
if let Some(n) = self.name {
|
||||
g.name = n.into();
|
||||
}
|
||||
if let Some(c) = self.color1 {
|
||||
g.color1 = c;
|
||||
}
|
||||
if let Some(c) = self.color2 {
|
||||
g.color2 = c;
|
||||
}
|
||||
if let Some(a) = self.angle {
|
||||
g.angle = a;
|
||||
}
|
||||
if let Some(c) = self.centered {
|
||||
g.centered = c;
|
||||
}
|
||||
h.gradient = Some(g);
|
||||
h.pattern = "SOLID".into();
|
||||
h.solid = true;
|
||||
h.pattern_lines.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn gradient_edit(p: &Value) -> Result<Option<GradientEdit>> {
|
||||
let g = match p.get("gradient") {
|
||||
None | Some(Value::Null) => return Ok(None),
|
||||
Some(Value::Object(g)) => g,
|
||||
Some(_) => return Err(bad("hatchedit", "`gradient` is an object {name?, color1?, color2?, angle?, centered?}")),
|
||||
};
|
||||
let field = |k: &str| g.get(k).filter(|v| !v.is_null());
|
||||
let name = match field("name") {
|
||||
None => None,
|
||||
Some(v) => Some(
|
||||
v.as_str()
|
||||
.and_then(gradient_name)
|
||||
.ok_or_else(|| bad("hatchedit", format!("unknown gradient `name`; use one of {}", GRADIENT_NAMES.join(", "))))?,
|
||||
),
|
||||
};
|
||||
let color = |k: &str| -> Result<Option<Color>> {
|
||||
match field(k) {
|
||||
None => Ok(None),
|
||||
Some(v) => v
|
||||
.as_str()
|
||||
.and_then(Color::parse)
|
||||
.map(Some)
|
||||
.ok_or_else(|| bad("hatchedit", format!("`gradient.{k}` is a colour such as \"5\", \"red\" or \"255,128,0\""))),
|
||||
}
|
||||
};
|
||||
let angle = match field("angle") {
|
||||
None => None,
|
||||
Some(v) => Some(
|
||||
v.as_f64().map(f64::to_radians).filter(|a| a.is_finite()).ok_or_else(|| bad("hatchedit", "`gradient.angle` is a number of degrees"))?,
|
||||
),
|
||||
};
|
||||
let centered = match field("centered") {
|
||||
None => None,
|
||||
Some(v) => Some(v.as_bool().ok_or_else(|| bad("hatchedit", "`gradient.centered` is true or false"))?),
|
||||
};
|
||||
Ok(Some(GradientEdit { name, color1: color("color1")?, color2: color("color2")?, angle, centered }))
|
||||
}
|
||||
|
||||
pub(super) fn run_hatchedit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = hatches(s, p)?;
|
||||
let style = match str_param(p, "style") {
|
||||
@@ -185,6 +267,10 @@ pub(super) fn run_hatchedit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Some(_) => return Err(bad("hatchedit", "`recreate` is \"polyline\"")),
|
||||
};
|
||||
let grad = gradient_edit(p)?;
|
||||
if grad.is_some() && str_param(p, "pattern").map(|v| v.trim().to_ascii_uppercase()).is_some_and(|v| !v.is_empty() && v != "SOLID") {
|
||||
return Err(bad("hatchedit", "give `pattern` or `gradient`, not both"));
|
||||
}
|
||||
// Like the prompt: a pattern must be one CADCraft defines, SOLID, or the hatches' own (a
|
||||
// pattern read from a file keeps its definition).
|
||||
if let Some(up) = str_param(p, "pattern").map(|v| v.trim().to_ascii_uppercase()).filter(|v| !v.is_empty())
|
||||
@@ -210,6 +296,9 @@ pub(super) fn run_hatchedit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for handle in &hs {
|
||||
let Some(EntityKind::Hatch(mut h)) = s.doc()?.entity(*handle).map(|e| e.kind.clone()) else { continue };
|
||||
properties(&mut h, p);
|
||||
if let Some(g) = &grad {
|
||||
g.apply(&mut h);
|
||||
}
|
||||
if let Some(st) = style {
|
||||
h.style = st;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
//! HATCHPICK: the Hatch and Gradient dialog's picks (internal points, boundary objects, a pattern
|
||||
//! origin, or the hatches to edit), kept in the session for the dialog to take. It never changes
|
||||
//! the drawing.
|
||||
|
||||
use cadcraft_doc::{EntityKind, Handle, HatchLoop};
|
||||
use cadcraft_geom::Vec2;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{HatchSettings, closed_loop, loops_at, loops_from_handles};
|
||||
use crate::cmd::{bad, points_param, targets};
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
/// Upper bound on the points and objects one HATCHPICK keeps.
|
||||
pub(crate) const MAX_PICKS: usize = 1000;
|
||||
|
||||
/// The first prompt's keywords: what to pick.
|
||||
const MODES: [&str; 4] = ["Points", "Objects", "oRigin", "Hatch"];
|
||||
|
||||
/// What HATCHPICK picks.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum PickMode {
|
||||
/// Internal points, each inside a closed boundary.
|
||||
#[default]
|
||||
Points,
|
||||
/// Closed boundary objects.
|
||||
Objects,
|
||||
/// One point: the pattern origin.
|
||||
Origin,
|
||||
/// Hatches to edit.
|
||||
Hatch,
|
||||
}
|
||||
|
||||
/// What the last HATCHPICK kept ([`Session::hatch_picks`]).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct HatchPicks {
|
||||
pub mode: PickMode,
|
||||
pub points: Vec<Vec2>,
|
||||
/// Closed boundary objects (Objects) or hatches (Hatch).
|
||||
pub handles: Vec<Handle>,
|
||||
/// The pattern origin (Origin).
|
||||
pub origin: Option<Vec2>,
|
||||
}
|
||||
|
||||
fn mode_named(t: &str) -> Option<PickMode> {
|
||||
Some(match t.trim().to_ascii_lowercase().as_str() {
|
||||
"points" | "p" => PickMode::Points,
|
||||
"objects" | "o" => PickMode::Objects,
|
||||
"origin" | "r" => PickMode::Origin,
|
||||
"hatch" | "h" => PickMode::Hatch,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// JSON form: `{points?, handles?}`, checked as the prompts check them.
|
||||
pub(super) fn run_hatchpick(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let given = |k: &str| p.get(k).is_some_and(|v| !v.is_null());
|
||||
if !given("points") && !given("handles") {
|
||||
return Err(bad("hatchpick", "give internal `points` or boundary `handles`"));
|
||||
}
|
||||
let mut picks = HatchPicks::default();
|
||||
if given("points") {
|
||||
let pts = points_param(p, "points").ok_or_else(|| bad("hatchpick", "`points` is an array of [x, y]"))?;
|
||||
if pts.len() > MAX_PICKS {
|
||||
return Err(bad("hatchpick", format!("at most {MAX_PICKS} points")));
|
||||
}
|
||||
for q in pts {
|
||||
loops_at(s, q).map_err(|_| bad("hatchpick", format!("no closed boundary around [{}, {}]", q.x, q.y)))?;
|
||||
picks.points.push(q);
|
||||
}
|
||||
}
|
||||
if given("handles") {
|
||||
let hs = targets(s, p)?;
|
||||
if hs.len() > MAX_PICKS {
|
||||
return Err(bad("hatchpick", format!("at most {MAX_PICKS} objects")));
|
||||
}
|
||||
let d = s.doc()?;
|
||||
for h in hs {
|
||||
match d.entity(h) {
|
||||
Some(e) if closed_loop(d, e).is_some() => picks.handles.push(h),
|
||||
_ => return Err(bad("hatchpick", format!("{} is not a closed object", h.hex()))),
|
||||
}
|
||||
}
|
||||
if picks.points.is_empty() {
|
||||
picks.mode = PickMode::Objects;
|
||||
}
|
||||
}
|
||||
let out = json!({
|
||||
"points": picks.points.iter().map(|q| [q.x, q.y]).collect::<Vec<_>>(),
|
||||
"handles": picks.handles.iter().map(|h| h.hex()).collect::<Vec<_>>(),
|
||||
});
|
||||
s.hatch_picks = Some(picks);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// One pick, so Undo can drop it.
|
||||
enum Pick {
|
||||
Point(Vec2, Vec<HatchLoop>),
|
||||
Objects(Vec<Handle>, Vec<HatchLoop>),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HatchPickM {
|
||||
mode: Option<PickMode>,
|
||||
picks: Vec<Pick>,
|
||||
}
|
||||
|
||||
impl HatchPickM {
|
||||
fn count(&self) -> usize {
|
||||
self.picks
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
Pick::Point(..) => 1,
|
||||
Pick::Objects(hs, _) => hs.len(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
fn keep(&self, s: &mut Session, mode: PickMode) {
|
||||
let mut out = HatchPicks { mode, ..HatchPicks::default() };
|
||||
for p in &self.picks {
|
||||
match p {
|
||||
Pick::Point(q, _) => out.points.push(*q),
|
||||
Pick::Objects(hs, _) => out.handles.extend(hs.iter().copied()),
|
||||
}
|
||||
}
|
||||
s.hatch_picks = Some(out);
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for HatchPickM {
|
||||
fn name(&self) -> &'static str {
|
||||
"HATCHPICK"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match self.mode {
|
||||
None => Prompt::new("Enter what to pick", Accept::TEXT).kw(&MODES),
|
||||
Some(PickMode::Points) => Prompt::new("Pick internal point", Accept::POINT).kw(&["Undo"]),
|
||||
Some(PickMode::Objects) => Prompt::new("Select objects", Accept::SELECT).kw(&["Undo"]),
|
||||
Some(PickMode::Origin) => Prompt::new("Specify the pattern origin", Accept::POINT),
|
||||
Some(PickMode::Hatch) => Prompt::new("Select hatch object", Accept::SELECT),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
let Some(mode) = self.mode else {
|
||||
match &i {
|
||||
Input::Keyword(t) | Input::Text(t) => self.mode = mode_named(t),
|
||||
Input::Enter | Input::Cancel => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
return Ok(Step::Continue);
|
||||
};
|
||||
match (mode, i) {
|
||||
(_, Input::Cancel) => Ok(Step::Cancel),
|
||||
(PickMode::Points | PickMode::Objects, Input::Keyword(k)) if k == "Undo" => {
|
||||
if self.picks.pop().is_none() {
|
||||
s.echo("Nothing to undo.");
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(PickMode::Points | PickMode::Objects, Input::Enter) => {
|
||||
self.keep(s, mode);
|
||||
Ok(Step::Done)
|
||||
}
|
||||
(PickMode::Points, Input::Point(q)) => {
|
||||
if self.count() >= MAX_PICKS {
|
||||
s.echo(format!("At most {MAX_PICKS} picks."));
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
match loops_at(s, q) {
|
||||
Ok(l) => {
|
||||
s.echo(format!("Analyzing the selected data... {} loop(s)", l.len()));
|
||||
self.picks.push(Pick::Point(q, l));
|
||||
}
|
||||
Err(e) => s.echo(e.to_string()),
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(PickMode::Objects, Input::Pick(hs)) => {
|
||||
let room = MAX_PICKS.saturating_sub(self.count());
|
||||
let closed: Vec<Handle> = {
|
||||
let d = s.doc()?;
|
||||
hs.iter().copied().filter(|h| d.entity(*h).is_some_and(|e| closed_loop(d, e).is_some())).take(room).collect()
|
||||
};
|
||||
let others = hs.len().saturating_sub(closed.len());
|
||||
if !closed.is_empty() {
|
||||
let loops = loops_from_handles(s, &closed)?;
|
||||
self.picks.push(Pick::Objects(closed.clone(), loops));
|
||||
}
|
||||
s.echo(if others == 0 { format!("{} found", closed.len()) } else { format!("{} found, {others} not closed", closed.len()) });
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(PickMode::Origin, Input::Point(q)) => {
|
||||
s.hatch_picks = Some(HatchPicks { mode, origin: Some(q), ..HatchPicks::default() });
|
||||
Ok(Step::Done)
|
||||
}
|
||||
(PickMode::Hatch, Input::Pick(hs)) => {
|
||||
let hatches: Vec<Handle> = {
|
||||
let d = s.doc()?;
|
||||
hs.into_iter().filter(|h| matches!(d.entity(*h).map(|e| &e.kind), Some(EntityKind::Hatch(_)))).take(MAX_PICKS).collect()
|
||||
};
|
||||
if hatches.is_empty() {
|
||||
s.echo("No hatch selected.");
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
s.hatch_picks = Some(HatchPicks { mode, handles: hatches, ..HatchPicks::default() });
|
||||
Ok(Step::Done)
|
||||
}
|
||||
(PickMode::Origin | PickMode::Hatch, Input::Enter) => Ok(Step::Done),
|
||||
_ => Ok(Step::Continue),
|
||||
}
|
||||
}
|
||||
fn preview(&self, s: &Session, _c: Vec2) -> Vec<EntityKind> {
|
||||
let loops: Vec<HatchLoop> = self
|
||||
.picks
|
||||
.iter()
|
||||
.flat_map(|p| match p {
|
||||
Pick::Point(_, l) | Pick::Objects(_, l) => l.iter().cloned(),
|
||||
})
|
||||
.collect();
|
||||
if loops.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![HatchSettings::from(s, &Value::Null, false).hatch(loops)]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user