* 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:
4 files changed
+104
-2
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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", ¶ms)
|
||||
}
|
||||
|
||||
#[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
@@ -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) |
|
||||
|
||||
Reference in new issue
Block a user