Artboards: each artboard can have a background color on the canvas

Squashed from #2403 onto main.
This commit is contained in:
bflatastic
2026-10-21 17:11:08 +07:00
parent 9acfd1ea8a
commit dda52d0b20
27 changed files with 527 additions and 35 deletions
+1 -1
View File
@@ -136,7 +136,7 @@ impl Builder {
.into_iter()
.enumerate()
.filter(|(_, (_, r))| r.width() > 0.0 && r.height() > 0.0)
.map(|(i, (name, rect))| Artboard { id: i as u32 + 1, name, rect, show_center_mark: false, show_cross_hairs: false })
.map(|(i, (name, rect))| Artboard { id: i as u32 + 1, name, rect, show_center_mark: false, show_cross_hairs: false, color: None })
.collect();
if self.doc.artboards.is_empty() {
return Err(err("the Affinity document has no page with a size"));
+1 -1
View File
@@ -340,7 +340,7 @@ pub(super) fn single_artboard(doc: &Document, rect: Rect, name: &str) -> Documen
/// `d` with one artboard, `rect`.
pub(crate) fn with_single_artboard(mut d: Document, rect: Rect, name: &str) -> Document {
d.artboards = vec![Artboard { id: 1, name: name.into(), rect, show_center_mark: false, show_cross_hairs: false }];
d.artboards = vec![Artboard { id: 1, name: name.into(), rect, show_center_mark: false, show_cross_hairs: false, color: None }];
d
}
+21 -4
View File
@@ -125,7 +125,7 @@ pub fn specs() -> Vec<CommandSpec> {
"New Artboard",
["Window", "Artboards"],
None,
"{x?, y?, width?, height?, name?} (default: right of the last)",
"{x?, y?, width?, height?, name?, color?: color (its background color, as artboard.setProps takes it; default: none)} (default place: right of the last)",
has_doc,
artboard_new
),
@@ -143,7 +143,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Artboard Options…",
["Window", "Artboards"],
None,
"{index | artboards: [{index, name?, x?, y?, width?, height?}…] (several artboards in one undo step, each with its own fields; the art inside each before any of them changes goes with it, and an object inside several goes with the first listed that moves or scales its art), name?, x?, y?, width?, height?, moveArt?: bool (default false; a position-only change moves the contained art with its artboard), scaleArt?: bool (Scale Artwork with Artboard: resized, the artboard takes the art fully inside it (locked and hidden art only with lockedAndHidden?: bool, default prefs moveLockedWithArtboard) and its guides from its old rectangle onto the new one, each side by its own ratio; strokes/corners/patterns as object.scale), strokes?: bool, corners?: bool, patterns?: bool (the three as object.scale, for art that moves or scales with the artboard), lockedAndHidden?: bool (locked and hidden art moves along; default: the preference; recorded in the journal so a replay matches)} → null, or with scaleArt {scaled: the art scaled}",
"{index | artboards: [{index, name?, color?, x?, y?, width?, height?}…] (several artboards in one undo step, each with its own fields; the art inside each before any of them changes goes with it, and an object inside several goes with the first listed that moves or scales its art), name?, color?: color (\"#rrggbb\", {c,m,y,k}, … as the paint commands take: the background color the canvas draws the artboard's paper in, multiplied with the Simulate Colored Paper color while that is on; exports and print leave it out) | null (no background color), x?, y?, width?, height?, moveArt?: bool (default false; a position-only change moves the contained art with its artboard), scaleArt?: bool (Scale Artwork with Artboard: resized, the artboard takes the art fully inside it (locked and hidden art only with lockedAndHidden?: bool, default prefs moveLockedWithArtboard) and its guides from its old rectangle onto the new one, each side by its own ratio; strokes/corners/patterns as object.scale), strokes?: bool, corners?: bool, patterns?: bool (the three as object.scale, for art that moves or scales with the artboard), lockedAndHidden?: bool (locked and hidden art moves along; default: the preference; recorded in the journal so a replay matches)} → null, or with scaleArt {scaled: the art scaled}",
has_doc,
artboard_set
),
@@ -803,16 +803,27 @@ fn rect_from(p: &Value, default: Rect) -> Rect {
Rect::new(x, y, x + w, y + h)
}
/// An artboard command's `color` param, its background color: None when absent, `Some(None)` for
/// null (no color), else `Some(Some(color))`.
fn artboard_color(p: &Value, cmd: &str) -> Result<Option<Option<vectorcraft_color::Color>>> {
match p.get("color") {
None => Ok(None),
Some(Value::Null) => Ok(Some(None)),
Some(v) => color_value(v).map(|c| Some(Some(c))).ok_or_else(|| bad(cmd, format!("bad color {v}"))),
}
}
fn artboard_new(s: &mut Session, p: &Value) -> Result<Value> {
let d0 = &s.doc()?.doc;
let last = d0.artboards.last().map(|a| a.rect).unwrap_or(Rect::new(0.0, 0.0, 612.0, 792.0));
let def = Rect::new(last.x1 + 20.0, last.y0, last.x1 + 20.0 + last.width(), last.y1);
let r = rect_from(p, def);
let color = artboard_color(p, "artboard.new")?.flatten();
let idx = s.edit("New Artboard", |d, _| {
let id = d.next_artboard_id();
let given = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty());
let name = given.map_or_else(|| format!("Artboard {}", d.artboards.len() + 1), str::to_string);
d.artboards.push(Artboard { id, name, rect: r, show_center_mark: false, show_cross_hairs: false });
d.artboards.push(Artboard { id, name, rect: r, show_center_mark: false, show_cross_hairs: false, color });
Ok(d.artboards.len() - 1)
})?;
Ok(json!({ "index": idx }))
@@ -841,6 +852,7 @@ struct ArtboardEdit<'a> {
index: usize,
now: Rect,
name: Option<&'a str>,
color: Option<Option<vectorcraft_color::Color>>,
delta: vectorcraft_geom::Vec2,
resized: bool,
fit: Option<Affine>,
@@ -872,6 +884,8 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad("artboard.setProps", "an artboard is listed twice"));
}
let was = d.artboards.get(index).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
// The background color: a color sets it, null clears it, absent leaves it as it is.
let color = artboard_color(q, "artboard.setProps")?;
let now = rect_from(q, was);
let resized = (now.width() - was.width()).abs() > 1e-6 || (now.height() - was.height()).abs() > 1e-6;
let fit = if scale_art && resized { rect_map(was, now) } else { None };
@@ -881,7 +895,7 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
let moving = !resized && bool_or(p, "moveArt", false) && delta != vectorcraft_geom::Vec2::ZERO;
let transform = fit.or_else(|| moving.then(|| Affine::translate(delta)));
let art = if transform.is_some() { d.art_on_artboard(was, all).into_iter().filter(|id| taken.insert(*id)).collect() } else { vec![] };
edits.push(ArtboardEdit { index, now, name: str_param(q, "name").map(str::trim), delta, resized, fit, transform, art });
edits.push(ArtboardEdit { index, now, name: str_param(q, "name").map(str::trim), color, delta, resized, fit, transform, art });
}
// Noted in the journal when art goes along, so a replay transforms the same objects whatever
// the preference is then.
@@ -900,6 +914,9 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
// A blank name gives the artboard its default name, Artboard N for the Nth artboard.
a.name = if n.is_empty() { format!("Artboard {}", e.index + 1) } else { n.to_string() };
}
if let Some(c) = e.color {
a.color = c;
}
let id = a.id;
if let Some(xf) = e.transform {
let mut sc = if e.fit.is_some_and(|xf| Scaling::factor(xf).is_some()) { scaling } else { Scaling::default() };
+1
View File
@@ -966,6 +966,7 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result<Value> {
rect: b,
show_center_mark: false,
show_cross_hairs: false,
color: None,
});
converted.push(id);
}
+1
View File
@@ -340,6 +340,7 @@ impl DocSettings {
rect,
show_center_mark: false,
show_cross_hairs: false,
color: None,
})
.collect();
d