layer.vectorMask.edit stored `feather` as f.max(0.0) as f32, so 1e39 became inf (hard edge, null in info, .pcraft save refused) and any huge value was accepted. It now takes the same finite 0..=1000 px range and error as layer.layerMask.edit, checked before the edit so a bad value leaves the mask and history untouched. The params doc says so. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
63a0e43e74
commit
e4f15eecdf
|
|
@ -1415,6 +1415,16 @@ fn vector_mask_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
const CMD: &str = "layer.vectorMask.edit";
|
||||
let id = layer_id(s, p)?;
|
||||
let path = p.get("path").map(parse_path).transpose().map_err(|e| bad(CMD, e))?;
|
||||
// Same range as a pixel mask's feather (`layer.layerMask.edit`), checked before any edit (#4201).
|
||||
let feather = p
|
||||
.get("feather")
|
||||
.map(|v| {
|
||||
v.as_f64()
|
||||
.filter(|n| n.is_finite() && (0.0..=1000.0).contains(n))
|
||||
.map(|n| n as f32)
|
||||
.ok_or_else(|| bad(CMD, "`feather` must be a finite number within 0..1000"))
|
||||
})
|
||||
.transpose()?;
|
||||
s.edit("Edit Vector Mask", |doc, _| {
|
||||
let l = doc.layer_mut(id).ok_or(EngineError::NoLayer(id))?;
|
||||
let vm = l.vector_mask.as_mut().ok_or_else(|| EngineError::Other("layer has no vector mask".into()))?;
|
||||
|
|
@ -1430,8 +1440,8 @@ fn vector_mask_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if let Some(d) = f64p(p, "density") {
|
||||
vm.density = (d / 100.0).clamp(0.0, 1.0) as f32;
|
||||
}
|
||||
if let Some(f) = f64p(p, "feather") {
|
||||
vm.feather = f.max(0.0) as f32;
|
||||
if let Some(f) = feather {
|
||||
vm.feather = f;
|
||||
}
|
||||
if let Some(b) = p.get("invert").and_then(Value::as_bool)
|
||||
&& b
|
||||
|
|
@ -1821,7 +1831,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"layer.vectorMask.edit",
|
||||
"Edit Vector Mask",
|
||||
[],
|
||||
leak(format!(r##"{{"layer":id?,"path":{{…}}?,"enabled":bool?,"linked":bool?,"density":0..100?,"feather":px?,"invert":true?}}. {PATH_FORM}"##)),
|
||||
leak(format!(
|
||||
r##"{{"layer":id?,"path":{{…}}?,"enabled":bool?,"linked":bool?,"density":0..100?,"feather":0..1000? (pixels),"invert":true?}}. {PATH_FORM}"##
|
||||
)),
|
||||
has_layer,
|
||||
vector_mask_edit
|
||||
),
|
||||
|
|
|
|||
|
|
@ -701,6 +701,28 @@ fn vector_mask_commands_and_compositing() {
|
|||
assert!(s.execute("layer.vectorMask.revealAll", json!({})).is_err());
|
||||
}
|
||||
|
||||
/// #4201: the vector mask's feather takes the pixel mask's range (finite 0..=1000 px); anything
|
||||
/// else is refused before the edit, so no infinite feather can block a .pcraft save.
|
||||
#[test]
|
||||
fn vector_mask_edit_rejects_out_of_range_feather() {
|
||||
let mut s = session(32, 32, 8);
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
let id = s.active().unwrap().active_layer.unwrap();
|
||||
s.execute("layer.vectorMask.revealAll", json!({})).unwrap();
|
||||
let feather = |s: &Session| doc(s).layer(id).unwrap().vector_mask.as_ref().unwrap().feather;
|
||||
let steps = s.active().unwrap().history.past_len();
|
||||
for bad in [json!(1e39), json!(-1), json!(1001), json!("x"), json!(null)] {
|
||||
assert!(s.execute("layer.vectorMask.edit", json!({"feather": bad})).is_err(), "feather {bad}");
|
||||
assert_eq!(feather(&s), 0.0, "feather {bad}");
|
||||
assert_eq!(s.active().unwrap().history.past_len(), steps, "feather {bad}");
|
||||
}
|
||||
let r = s.execute("layer.vectorMask.edit", json!({"feather": 12.5})).unwrap();
|
||||
assert_eq!(r["feather"], 12.5);
|
||||
assert_eq!(feather(&s), 12.5);
|
||||
s.execute("layer.vectorMask.edit", json!({"feather": 1000})).unwrap();
|
||||
assert_eq!(feather(&s), 1000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_vectors_moves_shapes_and_masks() {
|
||||
let mut s = session(100, 100, 8);
|
||||
|
|
|
|||
Loading…
Reference in New Issue