Merge #525: Tint and Apply To in the Properties Fill and Stroke menus

Conflict: ROADMAP.md: both progress-log lines
Conflict: crates/engine/src/cmd/object.rs: object.fill takes the PR's swatch?/tint? form with next's known_swatch check (#520) on a given swatch; params doc names the swatch format; both test modules
Conflict: crates/ui-egui/src/i18n/it.rs: both sets of rows
Conflict: crates/ui-egui/src/panels/properties.rs: swatch_menu keeps the PR's popup (Apply To, Tint) with next's swatch_list for the swatches (its own width); tests keep next's Pathfinder test and the PR's three
This commit is contained in:
aiongg
2026-10-12 00:15:19 +00:00
5 changed files with 216 additions and 21 deletions
+61 -6
View File
@@ -180,12 +180,34 @@ pub fn specs() -> Vec<CommandSpec> {
)
}
),
cmd!("object.fill", "Fill", [], None, "{swatch (a swatch's name, e.g. [Paper]), tint?: 0..1, ids?}", has_selection, |s, p| {
let sw = str_param(p, "swatch").ok_or_else(|| bad("object.fill", "missing swatch"))?.to_string();
known_swatch(&s.doc()?.doc, &sw, "object.fill", "swatch")?;
let tint = f64_or(p, "tint", 1.0) as f32;
set_flag(s, p, move |i| i.fill = Fill { swatch: sw.clone(), tint, ..i.fill.clone() }, false)
}),
cmd!(
"object.fill",
"Fill",
[],
None,
"{swatch? (a swatch's name, e.g. [Paper]), tint?: 0..1, ids?} — a swatch (tint 100% unless given), or a tint alone that keeps each object's swatch",
has_selection,
|s, p| {
let sw = str_param(p, "swatch").map(str::to_string);
let tint = p.get("tint").and_then(Value::as_f64).filter(|t| t.is_finite()).map(|t| t.clamp(0.0, 1.0) as f32);
if sw.is_none() && tint.is_none() {
return Err(bad("object.fill", "missing swatch"));
}
if let Some(n) = &sw {
known_swatch(&s.doc()?.doc, n, "object.fill", "swatch")?;
}
set_flag(
s,
p,
move |i| {
let swatch = sw.clone().unwrap_or_else(|| i.fill.swatch.clone());
let tint = tint.unwrap_or(if sw.is_some() { 1.0 } else { i.fill.tint });
i.fill = Fill { swatch, tint, ..i.fill.clone() };
},
false,
)
}
),
cmd!(
"object.stroke",
"Stroke",
@@ -200,10 +222,15 @@ pub fn specs() -> Vec<CommandSpec> {
}
}
let p2 = p.clone();
// A tint alone changes no stroke into one (it would become the default 1 pt black).
let tint_only = p.as_object().is_some_and(|o| o.keys().all(|k| k == "tint" || k == "ids"));
set_flag(
s,
p,
move |i| {
if tint_only && i.stroke.is_none() {
return;
}
let mut st = if i.stroke.is_none() && p2.get("swatch").is_none() { Stroke::default() } else { i.stroke.clone() };
if let Some(v) = p2.get("swatch").and_then(Value::as_str) {
st.swatch = v.into();
@@ -3443,3 +3470,31 @@ mod copy_ids_tests {
assert!(ids(&r).iter().all(|i| s.doc().unwrap().doc.item(designcraft_doc::ItemId(*i)).is_some()));
}
}
#[cfg(test)]
mod tint_alone_tests {
use serde_json::json;
#[test]
fn a_tint_alone_keeps_each_swatch_and_leaves_no_stroke_alone() {
let mut s = crate::Session::new();
s.execute("file.new", &json!({})).unwrap();
let a = s.execute("frame.create", &json!({"rect": [0, 0, 50, 50]})).unwrap()["id"].as_u64().unwrap();
let b = s.execute("frame.create", &json!({"rect": [60, 0, 110, 50]})).unwrap()["id"].as_u64().unwrap();
s.execute("object.fill", &json!({"swatch": "[Black]", "ids": [a]})).unwrap();
s.execute("object.fill", &json!({"swatch": "C=100 M=0 Y=0 K=0", "ids": [b]})).unwrap();
s.execute("object.stroke", &json!({"swatch": "[None]", "ids": [a, b]})).unwrap();
// Fill tint on both: each keeps its swatch.
s.execute("object.fill", &json!({"tint": 0.25, "ids": [a, b]})).unwrap();
let item = |s: &crate::Session, id: u64| s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().clone();
assert_eq!((item(&s, a).fill.swatch.as_str(), item(&s, a).fill.tint), ("[Black]", 0.25));
assert_eq!((item(&s, b).fill.swatch.as_str(), item(&s, b).fill.tint), ("C=100 M=0 Y=0 K=0", 0.25));
// A swatch alone is 100%.
s.execute("object.fill", &json!({"swatch": "[Paper]", "ids": [a]})).unwrap();
assert_eq!(item(&s, a).fill.tint, 1.0);
// A stroke tint on no stroke: still no stroke.
s.execute("object.stroke", &json!({"tint": 0.5, "ids": [a]})).unwrap();
assert!(item(&s, a).stroke.is_none(), "{:?}", item(&s, a).stroke);
assert!(s.execute("object.fill", &json!({"ids": [a]})).is_err(), "neither swatch nor tint");
}
}