HATCHEDIT: refuse an explicit bad scale or angle before editing anything (#724) (#777)

* HATCHEDIT: refuse an explicit bad scale or angle before editing anything (#724)

run_hatchedit read `scale` and `angle` leniently (a non-positive scale or a
non-numeric value was skipped), so such a request reported success, added an
undo step and cleared the redo history, applied its valid companion property
or recreated the boundary. HATCH already refuses a non-positive scale. The
numbers are now checked first with the same size_param rule; `angle` must be
a number; absent and null still mean "leave it".

* Docs: update the progress docs for this change

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 05:37:45 -04:00
1 parent a2b6c9ac4a
commit e773d93164
4 files changed
+104 -2

No files matched your search

+2
View File
@@ -119,6 +119,7 @@ Ranked; detail and the full beta checklist in [docs/roadmap.md](docs/roadmap.md)
| Date | Entry |
|---|---|
| 2026-10-12 | TRIM: a pick after a closed polyline's last cut keeps only the complementary run instead of an extra circuit of the outline (#726) |
| 2026-10-12 | HATCHEDIT refuses an explicit bad `scale` or `angle` before it edits anything, so a rejected request leaves the drawing, undo and redo as they were (#724) |
| 2026-10-10 | Merged main: Ukrainian interface (#36, first string catalog), Dynamic Input pointer boxes with Tab and relative entry (#60), press-drag selection windows (#63), WIPEOUT and hatch gradients kept through DXF/DWG save (#147, #162), SETVAR DIM* as style overrides (#129), UNITS dialog (#38), close-window save prompt (#47), DIMANGULAR on circles (#20), cargo fmt fixed on main. UI/UX 40% → 43% (dynamic input 15% → 45%), localization 3% → 5%; ready for real work ≈ 37% → 38% (weighted 37.2% → 37.7%), mainstream 27% → 28%, essentials 45% → 46%. Beta is now ~37 points away; hours unchanged within rounding. |
| 2026-10-10 | Added mainstream practitioner (≈ 27%) and essentials user (≈ 45%) numbers; ready for real work unchanged at ≈ 37%. User evidence: 2 praise comments, 0 "switched from AutoCAD" reports, 27 of 37 open issues on the core path. |
| 2026-10-10 | Stage re-normalized from alpha to **pre-alpha** under the craftrules core-workflow gate: drafting with tracking, blocks, plotting and lossless DWG save fail; dimensioning passes. Alpha is ≈ 125–190 h away. |
@@ -133,6 +134,7 @@ Ranked; detail and the full beta checklist in [docs/roadmap.md](docs/roadmap.md)
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | trivial | Progress log: closed-polyline TRIM (#726) |
| 2026-10-12 | trivial | Progress log: HATCHEDIT numeric validation (#724) |
| 2026-10-10 | minor | Merged main's changes (Ukrainian catalog, dynamic input pointer boxes, DXF fixes); UI/UX and localization numbers updated |
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% for each number |
| 2026-10-10 | minor | Mainstream practitioner and essentials user headline numbers |
+13 -1
View File
@@ -10,7 +10,7 @@ use serde_json::{Value, json};
use super::assoc::{detach, same_shape};
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::cmd::{bad, bool_or, point_param, size_param, str_param, targets};
use crate::{Accept, Input, Interactive, Picker, Prompt, Result, Session, Step};
/// Upper bound on hatches edited by one call.
@@ -248,7 +248,19 @@ fn gradient_edit(p: &Value) -> Result<Option<GradientEdit>> {
Ok(Some(GradientEdit { name, color1: color("color1")?, color2: color("color2")?, angle, centered }))
}
/// An explicit `scale` must be positive and an explicit `angle` a number, checked before anything
/// is edited so a refused request changes nothing (absent and null leave them as they are).
fn check_numbers(p: &Value) -> Result<()> {
size_param("hatchedit", p, "scale", false)?;
match p.get("angle") {
None | Some(Value::Null) => Ok(()),
Some(v) if v.as_f64().is_some_and(f64::is_finite) => Ok(()),
Some(_) => Err(bad("hatchedit", "`angle` must be a number of degrees")),
}
}
pub(super) fn run_hatchedit(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p)?;
let hs = hatches(s, p)?;
let style = match str_param(p, "style") {
Some(t) => Some(island_style(t).ok_or_else(|| bad("hatchedit", "`style` is \"normal\", \"outer\" or \"ignore\""))?),
@@ -0,0 +1,86 @@
//! HATCHEDIT refuses an explicit bad `scale` or `angle` before changing anything: no partial
//! edit, no undo step, redo history kept.
use cadcraft_engine::Session;
use cadcraft_engine::doc::EntityKind;
use serde_json::{Value, json};
fn fixture() -> (Session, String) {
let mut s = Session::new();
s.execute("rectang", &json!({"p1": [0, 0], "p2": [10, 10]})).unwrap();
let handle = s.execute("hatch", &json!({"points": [[5, 5]]})).unwrap()["handle"].as_str().unwrap().to_owned();
// One redo entry, which a rejected edit must leave alone.
s.execute("circle", &json!({"center": [20, 20], "radius": 1})).unwrap();
s.undo().unwrap();
assert_eq!(s.state().unwrap().redo.len(), 1);
(s, handle)
}
fn hatch_props(s: &Session) -> (f64, f64) {
let hatches: Vec<(f64, f64)> = s
.doc()
.unwrap()
.model
.iter()
.filter_map(|e| if let EntityKind::Hatch(h) = &e.kind { Some((h.scale, h.angle.to_degrees())) } else { None })
.collect();
assert_eq!(hatches.len(), 1);
hatches[0]
}
fn assert_props(s: &Session, scale: f64, angle: f64) {
let (sc, an) = hatch_props(s);
assert!((sc - scale).abs() < 1e-9 && (an - angle).abs() < 1e-9, "scale {sc}, angle {an}");
}
fn edit(s: &mut Session, handle: &str, extra: Value) -> cadcraft_engine::Result<Value> {
let mut params = json!({ "handles": [handle] });
for (k, v) in extra.as_object().unwrap() {
params[k] = v.clone();
}
s.execute("hatchedit", &params)
}
#[test]
fn bad_scale_or_angle_is_refused_and_changes_nothing() {
let cases = [
(json!({"scale": 0}), "scale"),
(json!({"scale": -1}), "scale"),
(json!({"scale": "x"}), "scale"),
(json!({"angle": "x"}), "angle"),
(json!({"scale": 0, "angle": 45}), "scale"),
(json!({"scale": 2, "angle": "x"}), "angle"),
(json!({"scale": 0, "recreate": "polyline"}), "scale"),
(json!({"angle": "x", "recreate": "polyline"}), "angle"),
];
for (extra, key) in cases {
let (mut s, handle) = fixture();
let before = s.doc().unwrap().clone();
let (undo, redo, revision) = {
let st = s.state().unwrap();
(st.undo.len(), st.redo.len(), st.revision)
};
let err = edit(&mut s, &handle, extra.clone()).expect_err(&format!("{extra} should be refused"));
assert!(err.to_string().contains(&format!("`{key}`")), "{extra}: {err}");
assert_eq!(s.doc().unwrap(), &before, "{extra}: the drawing is untouched");
let st = s.state().unwrap();
assert_eq!((st.undo.len(), st.redo.len(), st.revision), (undo, redo, revision), "{extra}: history is untouched");
}
}
#[test]
fn valid_omitted_and_null_values_still_edit() {
let (mut s, handle) = fixture();
edit(&mut s, &handle, json!({"scale": 2, "angle": 45})).unwrap();
assert_props(&s, 2.0, 45.0);
// Omitted or null numeric fields keep what the hatch has while another property changes.
edit(&mut s, &handle, json!({"style": "outer"})).unwrap();
assert_props(&s, 2.0, 45.0);
edit(&mut s, &handle, json!({"scale": null, "angle": null, "style": "normal"})).unwrap();
assert_props(&s, 2.0, 45.0);
// Zero and negative angles are angles.
edit(&mut s, &handle, json!({"angle": 0})).unwrap();
assert_props(&s, 2.0, 0.0);
edit(&mut s, &handle, json!({"angle": -30})).unwrap();
assert_props(&s, 2.0, -30.0);
}
+3 -1
View File
@@ -1,6 +1,6 @@
# Where CADCraft falls short of AutoCAD
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (property-based and hostile-input tests for geometry landed; S6 removed) · **Target:** Autodesk AutoCAD 2027
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (F13: HATCHEDIT refuses a bad scale or angle before editing (#724); previously minor: SCRIPT command reading `.scr` files landed; E2 removed) · **Target:** Autodesk AutoCAD 2027
Every known shortfall, one row each, ranked within each section by user impact. This is the work
list: agents pick from here (top of a section first), and remove or update a row in the same PR
@@ -36,6 +36,7 @@ do them first.
| F11 | Tables (cell styles, formulas, TABLEDIT, TABLEEXPORT, table breaking and CSV data links landed, #534): formulas aren't read from or written as AutoCAD FIELD objects (other programs see values); number formats apply to formula results only; cell text doesn't wrap; custom cell styles, breaks and data links travel as CADCraft xdata (not through DWG); data links read CSV only (no Excel ranges, no write-back, not on the web); no cell-style tab in the Table Style dialog | `cmd/table/*`, `io/dxf_table.rs` | Low-medium | 6–10 |
| F12 | Annotative scaling done (#468): scale list (SCALELISTEDIT), CANNOSCALE, OBJECTSCALE, ANNOALLVISIBLE/ANNOAUTOSCALE, viewport scale, DXF, status bar list. Left: per-scale positions (ANNORESET is a no-op), a viewport annotation scale apart from its viewport scale, annotative objects inside blocks and old LEADERs, AutoCAD files' per-object scale contexts (read as not annotative) | `cmd/annoscale.rs`, `doc/src/anno.rs` | Low-medium | 4–8 |
| F13 | Hatch: 23 patterns (AutoCAD ~85; ours must stay original); the Hatch and Gradient and Hatch Edit dialogs have no Preview or Inherit Properties and can't add or remove boundaries of an existing hatch (HPDLGMODE 0 keeps HATCH on the command line), no MPolygon; HATCHEDIT can't add or remove boundaries; a hatch copied with its boundary isn't linked to the copy; HPGAPTOL closes gaps between curve ends only | `doc/src/library.rs`, `cmd/hatch.rs`, `cmd/hatch/edit.rs` | Medium | 8–12 |
| F13 | Hatch: 23 patterns (AutoCAD ~85; ours must stay original); no hatch dialog (HATCH and HATCHEDIT options work at the command line and in JSON; HATCHEDIT refuses a bad `scale` or `angle` before editing, #724), no MPolygon; HATCHEDIT can't add or remove boundaries; a hatch copied with its boundary isn't linked to the copy; HPGAPTOL closes gaps between curve ends only | `doc/src/library.rs`, `cmd/hatch.rs`, `cmd/hatch/edit.rs` | Medium | 8–12 |
| F14 | **[alpha blocker]** Plotting leftovers: binary `.ctb`/`.stb`/`.pc3` files can't be read; model-space PDF plots keep the default margins (a device paper's margins apply to layouts and PNG plots); no system printers (H3). The Plot dialog (#160), Plot Preview and PUBLISH (#186), plot styles (CTB and STB, [plot-styles.md](plot-styles.md), #410) with their end, join and fill styles, and plotter devices with paper sizes and printable areas ([plotters.md](plotters.md), #564) landed | `io/src/plotter.rs`, `cmd/plotters.rs` | Medium | 4–8 |
| F15 | Viewports: MVIEW Restore prints "not available yet" (named viewport configurations into a layout, see U11); a clipped viewport and its clip boundary move and scale separately. Polygonal, object and clipped viewports, VPCLIP, MVIEW ON/OFF/Lock/2/3/4 and the status-bar scale list landed (#453) | `cmd/layout/viewports.rs` | Low | 2–4 |
| F16 | Groups: no group bounding box or single grip when a group is selected (GROUPDISPLAYMODE), copying a whole group does not make a new group, no Object Grouping dialog (GROUP, GROUPEDIT, UNGROUP, -GROUP, PICKSTYLE group selection and the DXF/DWG round trip landed, #417); PICKSTYLE's associative-hatch bit is stored but unused | `cmd/groups.rs` | Low | 2–4 |
@@ -149,6 +150,7 @@ Detail: [localization-parity.md](localization-parity.md).
| 2026-10-12 | minor | A bare system variable name typed at the command line (ORTHOMODE, GRIDMODE…) runs SETVAR on that variable, as in AutoCAD; U10 narrowed |
| 2026-10-13 | minor | STRETCH takes crossing and window polygons (CP, WP), BOX and a fence (F), typed and as JSON (`polygon`, `fence`): F1 narrowed |
| 2026-10-12 | minor | Property-based and hostile-input tests for geometry numerics landed (deterministic xorshift generator, no new dependency; known answers at 1e6–1e7 coordinates): S6 removed |
| 2026-10-12 | trivial | F13: HATCHEDIT refuses a bad scale or angle before editing (#724) |
| 2026-10-12 | minor | SCRIPT command reading `.scr` files landed (runs a file through the same script engine as CLI/MCP; nested scripts refused): E2 removed |
| 2026-10-11 | minor | Refreshed for merged features: code pages (#505), tracking leftovers (#441), dynamic input/grip input (#519, #425) and SPLINEDIT Join/Add/Elevate (#559) closed their top-10 rows (top 10 now has 6 rows until the next review re-ranks); MULTILEADER (#459) and xrefs (#525) narrowed rows 1 and 4; F1 narrowed to the remaining stubs (#558, #559, #547); F5, F7, F9, F12, F14, F15, F17, F20, F22, U5, U7, U10, FF2, FF4, FF10 narrowed; F18, U2, U8, U9 removed (done); U13, S3, S4, P1 updated for merged fixes |
| 2026-10-11 | minor | German interface catalog landed: L1 row count 936 → 989, L2 narrowed (de done; 40–60 → 35–55 h) |