From fec902b7d554f9d764115dc397ff40d0851d6fc7 Mon Sep 17 00:00:00 2001 From: phonedu <135960865+phonedu@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:00:25 +0800 Subject: [PATCH] M1: refuse negative vector sizes A negative width or height was clamped to zero by the scale helper, producing a layer smaller than asked for; vector.create now says so. Regression test covers width -10 with height 8. --- crates/engine/src/cmd/create.rs | 7 ++++++- crates/engine/src/vector_behaviour_tests.rs | 4 +++- docs/mcp.md | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 84fa419a..d9e64203 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -170,8 +170,13 @@ pub fn parse_path_data(d: &str) -> Option { /// independently (as resizing a vector does); with one, the other keeps its proportion. A size /// asked for on an axis the geometry has none of is an error, not silently ignored. fn vector_scale(width: Option, height: Option, bw: f64, bh: f64, what: &str) -> std::result::Result<(f64, f64), String> { + for (axis, asked) in [("width", width), ("height", height)] { + if asked.is_some_and(|a| a < 0.0) { + return Err(format!("`{axis}` can't be negative")); + } + } let fit = |asked: Option, have: f64, axis: &str| match asked { - Some(a) if have > 1e-9 => Ok(a.max(0.0) / have), + Some(a) if have > 1e-9 => Ok(a / have), Some(_) => Err(format!("`{axis}` can't size {what} with no {} extent", if axis == "width" { "horizontal" } else { "vertical" })), None => Ok(1.0), }; diff --git a/crates/engine/src/vector_behaviour_tests.rs b/crates/engine/src/vector_behaviour_tests.rs index 35d61074..7410bc04 100644 --- a/crates/engine/src/vector_behaviour_tests.rs +++ b/crates/engine/src/vector_behaviour_tests.rs @@ -468,7 +468,9 @@ fn vector_paths_take_the_requested_size_and_refuse_empty_geometry() { assert!(e.contains("horizontal extent"), "{e}"); let e = s.execute("vector.create", &json!({"path": "M0 0 L24 0", "height": 16})).unwrap_err().to_string(); assert!(e.contains("vertical extent"), "{e}"); - // A size that would collapse both axes is refused too. + // A size that would collapse both axes is refused too, and so is a negative one. let e = s.execute("vector.create", &json!({"path": path, "width": 0})).unwrap_err().to_string(); assert!(e.contains("0×0"), "{e}"); + let e = s.execute("vector.create", &json!({"path": path, "width": -10, "height": 8})).unwrap_err().to_string(); + assert!(e.contains("can't be negative"), "{e}"); } diff --git a/docs/mcp.md b/docs/mcp.md index 864bc1f1..cb2cb870 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -429,8 +429,8 @@ on; `key` resolves keys the way the canvas does. Commands behind keys: `vector.create {path | points}` makes the layer's box from the geometry; `width`/`height` scale the geometry to the size asked for (with both, each side independently; with one, the other keeps its proportion). A path or point set that draws nothing is refused instead of making an invisible -0×0 layer, as is a size on an axis the geometry has none of and a size that would collapse both -axes. +0×0 layer, as is a size on an axis the geometry has none of, a negative size, and a size that +would collapse both axes. Vector edit mode is driven with `run_command` (or `pointer`/`key` for gestures): `vector.edit.begin {id? | ids?}` (default the selection; converts shapes to vectors; several layers