mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
* HATCH/GRADIENT: points and objects in one call; GRADIENT name and centered Co-Authored-By: Claude Opus 5.5 <[email protected]> * HATCHEDIT: gradient parameter (make or change a gradient fill) Co-Authored-By: Claude Opus 5.5 <[email protected]> * HATCHPICK: pick hatch boundaries, an origin or hatches for the dialog Co-Authored-By: Claude Opus 5.5 <[email protected]> * Hatch dialog: state, defaults and the OK command call Co-Authored-By: Claude Opus 5.5 <[email protected]> * Hatch dialog: pick points, objects, origin or hatch and come back Co-Authored-By: Claude Opus 5.5 <[email protected]> * Hatch and Gradient dialog window (pattern swatches, gradient, origin, boundaries, options) Co-Authored-By: Claude Opus 5.5 <[email protected]> * HATCH, GRADIENT and HATCHEDIT open the hatch dialog; double-click a hatch to edit; HPDLGMODE Co-Authored-By: Claude Opus 5.5 <[email protected]> * 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 <[email protected]> * Docs: hatch dialogs in gaps, target-app parity and UI parity Co-Authored-By: Claude Opus 5.5 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: lloyd <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
250 lines
9.8 KiB
Rust
250 lines
9.8 KiB
Rust
//! The engine side of the Hatch and Gradient dialog: GRADIENT's name and centred, points and
|
|
//! objects in one HATCH call, HATCHEDIT's gradient, HATCHPICK and HPDLGMODE.
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::doc::{EntityKind, Handle, Hatch};
|
|
use serde_json::{Value, json};
|
|
|
|
fn rect(s: &mut Session, a: [f64; 2], b: [f64; 2]) -> String {
|
|
s.execute("rectang", &json!({ "p1": a, "p2": b })).unwrap()["handle"].as_str().unwrap().to_string()
|
|
}
|
|
|
|
/// The handle of the object added last.
|
|
fn last(s: &Session) -> String {
|
|
s.doc().unwrap().model.iter().last().unwrap().handle.hex()
|
|
}
|
|
|
|
fn hatch(s: &Session, h: &str) -> Hatch {
|
|
match &s.doc().unwrap().entity(Handle::parse_hex(h).unwrap()).unwrap().kind {
|
|
EntityKind::Hatch(x) => x.clone(),
|
|
k => panic!("not a hatch: {k:?}"),
|
|
}
|
|
}
|
|
|
|
fn hatches(s: &Session) -> Vec<(String, Hatch)> {
|
|
s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::Hatch(h) = &e.kind { Some((e.handle.hex(), h.clone())) } else { None }).collect()
|
|
}
|
|
|
|
fn count(s: &Session) -> usize {
|
|
s.doc().unwrap().model.len()
|
|
}
|
|
|
|
fn undo_depth(s: &Session) -> usize {
|
|
s.state().unwrap().undo.len()
|
|
}
|
|
|
|
fn handle(v: &Value) -> String {
|
|
v["handle"].as_str().unwrap().to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn gradient_takes_a_name_and_centred() {
|
|
let mut s = Session::new();
|
|
rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
let h = handle(
|
|
&s.execute("gradient", &json!({ "points": [[5, 5]], "name": "invspherical", "centered": false, "color1": "1", "color2": "3", "angle": 45 }))
|
|
.unwrap(),
|
|
);
|
|
let g = hatch(&s, &h).gradient.unwrap();
|
|
assert_eq!(g.name, "INVSPHERICAL");
|
|
assert!(!g.centered);
|
|
assert!((g.angle - 45f64.to_radians()).abs() < 1e-12);
|
|
// Linear and centred unless told otherwise.
|
|
let h = handle(&s.execute("gradient", &json!({ "points": [[5, 5]] })).unwrap());
|
|
let g = hatch(&s, &h).gradient.unwrap();
|
|
assert_eq!((g.name.as_str(), g.centered), ("LINEAR", true));
|
|
// An unknown name is refused and adds nothing.
|
|
let n = count(&s);
|
|
assert!(s.execute("gradient", &json!({ "points": [[5, 5]], "name": "RAINBOW" })).is_err());
|
|
assert_eq!(count(&s), n);
|
|
}
|
|
|
|
#[test]
|
|
fn hatch_takes_points_and_objects_in_one_call() {
|
|
let mut s = Session::new();
|
|
rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
s.execute("circle", &json!({ "center": [30, 5], "radius": 3 })).unwrap();
|
|
let c = last(&s);
|
|
let depth = undo_depth(&s);
|
|
s.execute("hatch", &json!({ "points": [[5, 5]], "handles": [c], "pattern": "SOLID" })).unwrap();
|
|
let hs = hatches(&s);
|
|
assert_eq!(hs.len(), 1, "one hatch for both picks");
|
|
assert_eq!(hs[0].1.loops.len(), 2);
|
|
assert_eq!(undo_depth(&s), depth + 1, "one undo step");
|
|
// Separate: one hatch per point and one per object.
|
|
s.execute("hatch", &json!({ "points": [[5, 5]], "handles": [c], "separate": true })).unwrap();
|
|
assert_eq!(hatches(&s).len(), 3);
|
|
s.undo().unwrap();
|
|
assert_eq!(hatches(&s).len(), 1, "one undo removes both");
|
|
// Points alone still ignore the selection.
|
|
s.set_selection(vec![Handle::parse_hex(&c).unwrap()]);
|
|
let h = handle(&s.execute("hatch", &json!({ "points": [[5, 5]] })).unwrap());
|
|
assert_eq!(hatch(&s, &h).loops.len(), 1);
|
|
}
|
|
|
|
use cadcraft_engine::doc::color::{Color, Rgb};
|
|
|
|
#[test]
|
|
fn hatchedit_turns_a_pattern_into_a_gradient_and_back() {
|
|
let mut s = Session::new();
|
|
rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
let h = handle(&s.execute("hatch", &json!({ "points": [[5, 5]], "pattern": "ANSI31" })).unwrap());
|
|
let depth = undo_depth(&s);
|
|
s.execute(
|
|
"hatchedit",
|
|
&json!({ "handles": [h], "gradient": { "name": "cylinder", "color1": "red", "color2": "255,128,0", "angle": 30, "centered": false } }),
|
|
)
|
|
.unwrap();
|
|
let x = hatch(&s, &h);
|
|
let g = x.gradient.clone().unwrap();
|
|
assert_eq!((g.name.as_str(), g.color1, g.color2, g.centered), ("CYLINDER", Color::Index(1), Color::True(Rgb(255, 128, 0)), false));
|
|
assert!((g.angle - 30f64.to_radians()).abs() < 1e-12);
|
|
assert!(x.solid);
|
|
assert_eq!(x.pattern, "SOLID");
|
|
assert_eq!(undo_depth(&s), depth + 1, "one undo step");
|
|
// Only the fields given change.
|
|
s.execute("hatchedit", &json!({ "handles": [h], "gradient": { "color2": "3" } })).unwrap();
|
|
let g = hatch(&s, &h).gradient.unwrap();
|
|
assert_eq!((g.name.as_str(), g.color1, g.color2), ("CYLINDER", Color::Index(1), Color::Index(3)));
|
|
// A pattern turns it back.
|
|
s.execute("hatchedit", &json!({ "handles": [h], "pattern": "ANSI31" })).unwrap();
|
|
let x = hatch(&s, &h);
|
|
assert!(x.gradient.is_none());
|
|
assert!(!x.solid);
|
|
s.undo().unwrap();
|
|
assert!(hatch(&s, &h).gradient.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn hatchedit_refuses_a_bad_gradient_and_changes_nothing() {
|
|
let mut s = Session::new();
|
|
rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
let h = handle(&s.execute("hatch", &json!({ "points": [[5, 5]], "pattern": "ANSI31" })).unwrap());
|
|
let before = hatch(&s, &h);
|
|
for bad in [
|
|
json!("x"),
|
|
json!({ "name": "RAINBOW" }),
|
|
json!({ "name": 5 }),
|
|
json!({ "color1": "nope" }),
|
|
json!({ "angle": "x" }),
|
|
json!({ "centered": 1 }),
|
|
] {
|
|
assert!(s.execute("hatchedit", &json!({ "handles": [h], "gradient": bad })).is_err(), "{bad}");
|
|
assert_eq!(hatch(&s, &h), before, "{bad}");
|
|
}
|
|
assert!(s.execute("hatchedit", &json!({ "handles": [h], "pattern": "ANSI31", "gradient": { "name": "LINEAR" } })).is_err());
|
|
assert_eq!(hatch(&s, &h), before);
|
|
}
|
|
|
|
use cadcraft_engine::geom::Vec2;
|
|
use cadcraft_engine::{Input, PickMode};
|
|
|
|
fn h(hex: &str) -> Handle {
|
|
Handle::parse_hex(hex).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn hatchpick_keeps_points_inside_boundaries_and_changes_nothing() {
|
|
let mut s = Session::new();
|
|
rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
let (n, depth) = (count(&s), undo_depth(&s));
|
|
s.start("hatchpick").unwrap();
|
|
s.input(Input::Keyword("Points".into())).unwrap();
|
|
assert!(s.prompt_text().contains("Pick internal point"), "{}", s.prompt_text());
|
|
s.input(Input::Point(Vec2::new(5.0, 5.0))).unwrap();
|
|
assert!(!s.preview(Vec2::new(6.0, 6.0)).is_empty(), "the found boundary is previewed");
|
|
// Outside every boundary: reported, not kept.
|
|
s.input(Input::Point(Vec2::new(50.0, 50.0))).unwrap();
|
|
assert!(s.log.iter().any(|l| l.contains("Valid hatch boundary not found")), "{:?}", s.log);
|
|
s.input(Input::Point(Vec2::new(2.0, 2.0))).unwrap();
|
|
s.input(Input::Keyword("Undo".into())).unwrap();
|
|
s.input(Input::Enter).unwrap();
|
|
assert!(s.running.is_none());
|
|
let p = s.hatch_picks.clone().unwrap();
|
|
assert_eq!(p.mode, PickMode::Points);
|
|
assert_eq!(p.points, vec![Vec2::new(5.0, 5.0)]);
|
|
assert_eq!((count(&s), undo_depth(&s)), (n, depth), "nothing added, no undo step");
|
|
}
|
|
|
|
#[test]
|
|
fn hatchpick_keeps_closed_objects_only() {
|
|
let mut s = Session::new();
|
|
let r = rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
s.execute("line", &json!({ "points": [[20, 0], [30, 0]] })).unwrap();
|
|
let l = last(&s);
|
|
s.start("hatchpick").unwrap();
|
|
s.input(Input::Keyword("Objects".into())).unwrap();
|
|
s.input(Input::Pick(vec![h(&r), h(&l)])).unwrap();
|
|
s.input(Input::Enter).unwrap();
|
|
let p = s.hatch_picks.clone().unwrap();
|
|
assert_eq!((p.mode, p.handles), (PickMode::Objects, vec![h(&r)]));
|
|
}
|
|
|
|
#[test]
|
|
fn hatchpick_origin_and_hatch_modes() {
|
|
let mut s = Session::new();
|
|
let r = rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
let hx = handle(&s.execute("hatch", &json!({ "points": [[5, 5]] })).unwrap());
|
|
s.start("hatchpick").unwrap();
|
|
s.input(Input::Keyword("oRigin".into())).unwrap();
|
|
s.input(Input::Point(Vec2::new(1.0, 2.0))).unwrap();
|
|
assert!(s.running.is_none());
|
|
assert_eq!(s.hatch_picks.clone().unwrap().origin, Some(Vec2::new(1.0, 2.0)));
|
|
s.hatch_picks = None;
|
|
s.start("hatchpick").unwrap();
|
|
s.input(Input::Keyword("Hatch".into())).unwrap();
|
|
// Not a hatch: asked again.
|
|
s.input(Input::Pick(vec![h(&r)])).unwrap();
|
|
assert!(s.running.is_some());
|
|
s.input(Input::Pick(vec![h(&r), h(&hx)])).unwrap();
|
|
assert!(s.running.is_none());
|
|
let p = s.hatch_picks.clone().unwrap();
|
|
assert_eq!((p.mode, p.handles), (PickMode::Hatch, vec![h(&hx)]));
|
|
}
|
|
|
|
#[test]
|
|
fn hatchpick_typed_mode_and_escape_keeps_nothing() {
|
|
let mut s = Session::new();
|
|
rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
s.cmdline("hatchpick points").unwrap();
|
|
assert!(s.prompt_text().contains("Pick internal point"), "{}", s.prompt_text());
|
|
s.input(Input::Point(Vec2::new(5.0, 5.0))).unwrap();
|
|
s.cancel();
|
|
assert!(s.running.is_none());
|
|
assert!(s.hatch_picks.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn hatchpick_json_form_validates_like_the_prompts() {
|
|
let mut s = Session::new();
|
|
let r = rect(&mut s, [0.0, 0.0], [10.0, 10.0]);
|
|
s.execute("line", &json!({ "points": [[20, 0], [30, 0]] })).unwrap();
|
|
let l = last(&s);
|
|
s.execute("hatchpick", &json!({ "points": [[5, 5]], "handles": [r] })).unwrap();
|
|
let p = s.hatch_picks.clone().unwrap();
|
|
assert_eq!((p.points, p.handles), (vec![Vec2::new(5.0, 5.0)], vec![h(&r)]));
|
|
let too_many: Vec<[f64; 2]> = vec![[5.0, 5.0]; 1001];
|
|
for bad in [
|
|
json!({}),
|
|
json!({ "points": [[50, 50]] }),
|
|
json!({ "points": [["a"]] }),
|
|
json!({ "handles": [l] }),
|
|
json!({ "handles": ["zz"] }),
|
|
json!({ "handles": "x" }),
|
|
json!({ "points": too_many }),
|
|
] {
|
|
s.hatch_picks = None;
|
|
assert!(s.execute("hatchpick", &bad).is_err(), "{bad}");
|
|
assert!(s.hatch_picks.is_none(), "{bad}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hpdlgmode_defaults_to_1_and_takes_0_or_1() {
|
|
let mut s = Session::new();
|
|
assert_eq!(cadcraft_engine::sysvars::get(&s, "HPDLGMODE"), Some(json!(1)));
|
|
s.execute("setvar", &json!({ "name": "HPDLGMODE", "value": 0 })).unwrap();
|
|
assert_eq!(cadcraft_engine::sysvars::get(&s, "HPDLGMODE"), Some(json!(0)));
|
|
assert!(s.execute("setvar", &json!({ "name": "HPDLGMODE", "value": 2 })).is_err());
|
|
}
|