diff --git a/crates/doc/src/artboard.rs b/crates/doc/src/artboard.rs index d7bfcafca..ac5db48c7 100644 --- a/crates/doc/src/artboard.rs +++ b/crates/doc/src/artboard.rs @@ -1,4 +1,4 @@ -use super::{Arc, Deserialize, Node, NodeId, NodeKind, Rect, Serialize, SymbolBoxes, skip}; +use super::{Arc, Deserialize, LayerColor, Node, NodeId, NodeKind, Rect, Serialize, SymbolBoxes, skip}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Artboard { @@ -39,6 +39,18 @@ pub struct Artboard { /// it behind the art ([`super::Document::with_artboard_fills`]). #[serde(default, skip_serializing_if = "Option::is_none")] pub color: Option, + /// Guide label (Artboard Options): the color the canvas draws the artboard's own ruler guides + /// in (a guide whose [`super::Guide::artboard`] is this artboard's id), a [`LayerColor`] preset + /// or a custom RGB. None (No label) draws them in the guide color preference, as canvas guides + /// are. Only the canvas uses it: the paper, exports and print don't. Written only when set; a + /// file's preset outside [`super::LAYER_COLORS`] reads as no label. + #[serde(default, deserialize_with = "label_within_palette", skip_serializing_if = "Option::is_none")] + pub guide_label: Option, +} + +/// A file's guide label: a preset index outside [`super::LAYER_COLORS`] reads as no label. +fn label_within_palette<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { + Ok(Option::::deserialize(d)?.filter(|c| !matches!(c, LayerColor::Preset(i) if usize::from(*i) >= super::LAYER_COLORS.len()))) } /// The Video Ruler Pixel Aspect Ratios an artboard takes (Artboard Options): from a pixel ten times diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 2a2f8a8d7..f3f159e53 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -208,6 +208,7 @@ impl Document { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }], layers: vec![], swatches: swatches.into(), diff --git a/crates/doc/src/node/ids.rs b/crates/doc/src/node/ids.rs index 9e09a47a9..58915eb66 100644 --- a/crates/doc/src/node/ids.rs +++ b/crates/doc/src/node/ids.rs @@ -56,4 +56,30 @@ impl LayerColor { LayerColor::Custom(c) => c, } } + + /// The color as text: a preset's name from [`LAYER_COLORS`], else `#rrggbb` (lowercase). + pub fn label_text(self) -> String { + let preset = match self { + LayerColor::Preset(i) => LAYER_COLORS.get(usize::from(i)), + LayerColor::Custom(_) => None, + }; + preset.map_or_else( + || { + let [r, g, b] = self.rgb(); + format!("#{r:02x}{g:02x}{b:02x}") + }, + |(name, _)| (*name).to_string(), + ) + } + + /// The color [`LayerColor::label_text`] names: a preset's name exactly as [`LAYER_COLORS`] + /// spells it, or `#rrggbb` with six hex digits. None for anything else. + pub fn from_label_text(s: &str) -> Option { + if let Some(i) = LAYER_COLORS.iter().position(|(name, _)| *name == s) { + return u8::try_from(i).ok().map(LayerColor::Preset); + } + let hex = s.strip_prefix('#').filter(|h| h.len() == 6 && h.bytes().all(|b| b.is_ascii_hexdigit()))?; + let byte = |k: usize| u8::from_str_radix(&hex[k..k + 2], 16).ok(); + Some(LayerColor::Custom([byte(0)?, byte(2)?, byte(4)?])) + } } diff --git a/crates/doc/src/tests.rs b/crates/doc/src/tests.rs index 4b75b0149..21fe8d7ce 100644 --- a/crates/doc/src/tests.rs +++ b/crates/doc/src/tests.rs @@ -5,6 +5,52 @@ mod lookups; mod slices; mod units; +/// A guide label's text is a layer color's name or `#rrggbb`, and reads back as the same color; +/// other spellings, an alpha part and a short form are not labels. +#[test] +fn guide_label_text_names_a_layer_color_or_rgb() { + for (i, (name, _)) in LAYER_COLORS.iter().enumerate() { + let c = LayerColor::Preset(u8::try_from(i).unwrap()); + assert_eq!(c.label_text(), *name); + assert_eq!(LayerColor::from_label_text(name), Some(c)); + } + let custom = LayerColor::Custom([0x0a, 0xbc, 0xff]); + assert_eq!(custom.label_text(), "#0abcff"); + assert_eq!(LayerColor::from_label_text("#0ABCff"), Some(custom)); + // A preset past the layer colors has no name: its text is the color it draws in. + let [r, g, b] = LayerColor::Preset(200).rgb(); + assert_eq!(LayerColor::Preset(200).label_text(), format!("#{r:02x}{g:02x}{b:02x}")); + for bad in ["red", "Red ", "", "#", "#fff", "#0abcff00", "0abcff", "#0abcfg", "#0abcff"] { + assert_eq!(LayerColor::from_label_text(bad), None, "{bad:?}"); + } +} + +/// An artboard's guide label: none unless set, left out of the JSON when none, and a preset past +/// the layer colors reading as none. +#[test] +fn artboard_guide_label_defaults_and_skips_in_json() { + let mut a = Document::new(100.0, 100.0).artboards[0].clone(); + assert_eq!(a.guide_label, None); + let plain = serde_json::to_value(&a).unwrap(); + assert!(plain.get("guide_label").is_none(), "{plain}"); + a.guide_label = Some(LayerColor::Custom([1, 2, 3])); + let v = serde_json::to_value(&a).unwrap(); + assert_eq!(v["guide_label"], serde_json::json!({"Custom": [1, 2, 3]})); + assert_eq!(serde_json::from_value::(v).unwrap(), a); + let read = |label: serde_json::Value| { + let mut v = plain.clone(); + v["guide_label"] = label; + serde_json::from_value::(v).map(|a| a.guide_label) + }; + assert_eq!(read(serde_json::json!({"Preset": 26})).unwrap(), Some(LayerColor::Preset(26))); + assert_eq!(read(serde_json::json!({"Preset": 27})).unwrap(), None); + assert_eq!(read(serde_json::json!({"Preset": 255})).unwrap(), None); + assert_eq!(read(serde_json::Value::Null).unwrap(), None); + assert!(read(serde_json::json!({"Preset": -1})).is_err()); + assert!(read(serde_json::json!({"Tint": 1})).is_err()); + assert_eq!(serde_json::from_value::(plain).unwrap().guide_label, None, "older files have none"); +} + /// Zooming in on an inch ruler divided the inch into tenths (#2827). Its steps below an inch /// are binary fractions now, and the minor ticks between them stay binary too; every other /// unit is still read in tenths. @@ -146,6 +192,7 @@ fn art_on_a_locked_artboard_cannot_be_selected_or_edited() { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); let rect = |d: &mut Document, r: Rect, appearance: Appearance| { let id = d.alloc_id(); diff --git a/crates/engine/src/cmd/fileio/affinity/document.rs b/crates/engine/src/cmd/fileio/affinity/document.rs index e87e9ac0f..9114e9a40 100644 --- a/crates/engine/src/cmd/fileio/affinity/document.rs +++ b/crates/engine/src/cmd/fileio/affinity/document.rs @@ -88,6 +88,7 @@ impl Builder { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }) .collect(); if self.doc.artboards.is_empty() { diff --git a/crates/engine/src/cmd/fileio/encode.rs b/crates/engine/src/cmd/fileio/encode.rs index 1108d3e2c..84188c313 100644 --- a/crates/engine/src/cmd/fileio/encode.rs +++ b/crates/engine/src/cmd/fileio/encode.rs @@ -373,6 +373,7 @@ pub(crate) fn with_single_artboard(mut d: Document, rect: Rect, name: &str) -> D show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }]; d } diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 25201db87..07571ad32 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -128,7 +128,7 @@ pub fn specs() -> Vec { "New Artboard", ["Window", "Artboards"], None, - "{x?, y?, width?, height?, name?, color?: color (its background color, as artboard.setProps takes it, #rrggbbaa and #rgba refused; default: none), showCenterMark?: bool, showCrossHairs?: bool, showVideoSafeAreas?: bool (as artboard.setProps takes them; default false), videoRulerPixelAspectRatio?: number 0.1–10 (as artboard.setProps takes it; default 1), side?: \"left\"|\"right\"|\"top\"|\"bottom\" (the size of artboard `beside`, 20 pt from that side of it; refused with x, y, width or height), beside?: n (the artboard `side` places it by; default the last)} (default place: right of the last)", + "{x?, y?, width?, height?, name?, color?: color (its background color, as artboard.setProps takes it, #rrggbbaa and #rgba refused; default: none), guideLabel?: name | \"#rrggbb\" | null (its guide label, as artboard.setProps takes it; default: none), showCenterMark?: bool, showCrossHairs?: bool, showVideoSafeAreas?: bool (as artboard.setProps takes them; default false), videoRulerPixelAspectRatio?: number 0.1–10 (as artboard.setProps takes it; default 1), side?: \"left\"|\"right\"|\"top\"|\"bottom\" (the size of artboard `beside`, 20 pt from that side of it; refused with x, y, width or height), beside?: n (the artboard `side` places it by; default the last)} (default place: right of the last)", has_doc, artboard_new ), @@ -155,7 +155,7 @@ pub fn specs() -> Vec { "Artboard Options…", ["Window", "Artboards"], None, - "{index | artboards: [{index, name?, color?, locked?, showCenterMark?, showCrossHairs?, showVideoSafeAreas?, videoRulerPixelAspectRatio?, 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, #rrggbbaa and #rgba refused: 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), locked?: bool (the Artboards panel's lock column: a locked artboard keeps its rectangle (a new x, y, width or height is an error unless locked is false), can't be moved, deleted or cut, and Rearrange and Fit to Artwork Bounds leave it as it is; the art on it (each object of a layer or sublayer whose bounds with strokes overlap it, a group as one object) acts as locked art until it is unlocked: clicks, marquees, select.set, select.all and the tools pass over it (art drawn or pasted onto it stays selected and editable until a selection leaves it out or it is moved off the locked artboards), it is deselected, and when another artboard moves or scales it stays unless lockedAndHidden), showCenterMark?: bool (Artboard Options' Show Center Mark: the canvas draws a mark at the artboard's center), showCrossHairs?: bool (Show Cross Hairs: the canvas draws a line from the middle of each side through the center), showVideoSafeAreas?: bool (Show Video Safe Areas: the canvas draws the action-safe area 3.5% and the title-safe area 5% in from each edge, as EBU R 95 sets them; the three are screen-only, as the artboard's edges are: exports and print leave them out), videoRulerPixelAspectRatio?: number 0.1–10 (Video Ruler Pixel Aspect Ratio: the width of one video pixel over its height, which View › Rulers › Show Video Rulers counts the artboard in; default 1, square pixels), 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?, guideLabel?, locked?, showCenterMark?, showCrossHairs?, showVideoSafeAreas?, videoRulerPixelAspectRatio?, 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, #rrggbbaa and #rgba refused: 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), guideLabel?: name | \"#rrggbb\" | null (the guide label: the canvas draws the artboard's own guides (guide.add with artboard) in this color instead of the guide color preference, selected ones still in the selection color; a layer color's name spelled exactly as Layer Options lists it (\"Light Blue\", \"Red\", …) or #rrggbb; null for none, which new artboards and older files have; screen-only, so exports and print leave it out; it can change on a locked artboard; an invalid value anywhere in the list changes nothing), locked?: bool (the Artboards panel's lock column: a locked artboard keeps its rectangle (a new x, y, width or height is an error unless locked is false), can't be moved, deleted or cut, and Rearrange and Fit to Artwork Bounds leave it as it is; the art on it (each object of a layer or sublayer whose bounds with strokes overlap it, a group as one object) acts as locked art until it is unlocked: clicks, marquees, select.set, select.all and the tools pass over it (art drawn or pasted onto it stays selected and editable until a selection leaves it out or it is moved off the locked artboards), it is deselected, and when another artboard moves or scales it stays unless lockedAndHidden), showCenterMark?: bool (Artboard Options' Show Center Mark: the canvas draws a mark at the artboard's center), showCrossHairs?: bool (Show Cross Hairs: the canvas draws a line from the middle of each side through the center), showVideoSafeAreas?: bool (Show Video Safe Areas: the canvas draws the action-safe area 3.5% and the title-safe area 5% in from each edge, as EBU R 95 sets them; the three are screen-only, as the artboard's edges are: exports and print leave them out), videoRulerPixelAspectRatio?: number 0.1–10 (Video Ruler Pixel Aspect Ratio: the width of one video pixel over its height, which View › Rulers › Show Video Rulers counts the artboard in; default 1, square pixels), 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}; no undo step when nothing changes", has_doc, artboard_set ), @@ -859,6 +859,22 @@ fn artboard_color(p: &Value, cmd: &str) -> Result Result>> { + match p.get("guideLabel") { + None => Ok(None), + Some(Value::Null) => Ok(Some(None)), + Some(v) => v + .as_str() + .and_then(LayerColor::from_label_text) + .map(|c| Some(Some(c))) + .ok_or_else(|| bad(cmd, "`guideLabel` is null, a layer color's name or #rrggbb")), + } +} + /// An artboard command's text param `key`: None when absent or null, else the string. Any other /// value, such as 5, is an error: "`{key}` must be a string". fn artboard_str<'a>(p: &'a Value, key: &str, cmd: &str) -> Result> { @@ -898,6 +914,7 @@ fn artboard_new(s: &mut Session, p: &Value) -> Result { 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 color = artboard_color(p, "artboard.new")?.flatten(); + let guide_label = artboard_guide_label(p, "artboard.new")?.flatten(); check_switches(p, &["showCenterMark", "showCrossHairs", "showVideoSafeAreas"], "artboard.new")?; check_numbers(p, &["x", "y", "width", "height"], "artboard.new")?; let r = match artboard_side(p, "artboard.new")? { @@ -932,6 +949,7 @@ fn artboard_new(s: &mut Session, p: &Value) -> Result { show_video_safe_areas, video_ruler_pixel_aspect_ratio, color, + guide_label, }); Ok(d.artboards.len() - 1) })?; @@ -1001,6 +1019,7 @@ struct ArtboardEdit<'a> { now: Rect, name: Option<&'a str>, color: Option>, + guide_label: Option>, lock: Option, center_mark: Option, cross_hairs: Option, @@ -1013,6 +1032,23 @@ struct ArtboardEdit<'a> { art: Vec, } +impl ArtboardEdit<'_> { + /// Does the edit change artboard `a` (the one at its index) or the art and guides on it? + fn changes(&self, a: &Artboard) -> bool { + let name = self.name.map(|n| if n.is_empty() { format!("Artboard {}", self.index + 1) } else { n.to_string() }); + self.now != a.rect + || self.transform.is_some() + || name.is_some_and(|n| n != a.name) + || self.color.is_some_and(|c| c != a.color) + || self.guide_label.is_some_and(|c| c != a.guide_label) + || self.lock.is_some_and(|l| l != a.locked) + || self.center_mark.is_some_and(|m| m != a.show_center_mark) + || self.cross_hairs.is_some_and(|c| c != a.show_cross_hairs) + || self.safe_areas.is_some_and(|v| v != a.show_video_safe_areas) + || self.pixel_aspect.is_some_and(|r| r != a.video_ruler_pixel_aspect_ratio) + } +} + fn artboard_set(s: &mut Session, p: &Value) -> Result { // One artboard (`index` and its fields), or several (`artboards`, each with its own fields). let entries: Vec<&Value> = match p.get("artboards") { @@ -1044,6 +1080,8 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result { 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")?; + // The guide label likewise; a locked artboard's can change, as its name can. + let guide_label = artboard_guide_label(q, "artboard.setProps")?; let now = rect_from(q, was); let lock = q.get("locked").and_then(Value::as_bool); let (center_mark, cross_hairs) = (q.get("showCenterMark").and_then(Value::as_bool), q.get("showCrossHairs").and_then(Value::as_bool)); @@ -1070,6 +1108,7 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result { now, name: artboard_str(q, "name", "artboard.setProps")?.map(str::trim), color, + guide_label, lock, center_mark, cross_hairs, @@ -1082,6 +1121,10 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result { art, }); } + // Nothing to change: no undo step. + if !edits.iter().any(|e| d.artboards.get(e.index).is_none_or(|a| e.changes(a))) { + return if scale_art { Ok(json!({ "scaled": [] })) } else { ok() }; + } // Noted in the journal when art goes along, so a replay transforms the same objects whatever // the preference is then. if edits.iter().any(|e| e.transform.is_some()) { @@ -1102,6 +1145,9 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result { if let Some(c) = e.color { a.color = c; } + if let Some(c) = e.guide_label { + a.guide_label = c; + } if let Some(l) = e.lock { a.locked = l; } diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index 0a4c93dc5..ed1767779 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -1319,6 +1319,7 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); converted.push(id); } diff --git a/crates/engine/src/cmd/newdoc.rs b/crates/engine/src/cmd/newdoc.rs index d8f0681cc..20dc187ab 100644 --- a/crates/engine/src/cmd/newdoc.rs +++ b/crates/engine/src/cmd/newdoc.rs @@ -344,6 +344,7 @@ impl DocSettings { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }) .collect(); d diff --git a/crates/engine/src/inspect.rs b/crates/engine/src/inspect.rs index 14df8529d..c474e2575 100644 --- a/crates/engine/src/inspect.rs +++ b/crates/engine/src/inspect.rs @@ -133,7 +133,8 @@ pub fn document(s: &Session) -> Value { document_opts(s, SummaryOpts::default()) } -/// An artboard's name and rectangle, its background color (`#rrggbb`) when it has one, +/// An artboard's name and rectangle, its background color (`#rrggbb`) when it has one, its guide +/// label (a layer color's name or `#rrggbb`) when it has one, /// `locked: true` when it is locked, `showCenterMark: true`, `showCrossHairs: true` and /// `showVideoSafeAreas: true` when it shows them, and `videoRulerPixelAspectRatio` when it isn't 1. fn artboard_summary(a: &vectorcraft_doc::Artboard) -> Value { @@ -141,6 +142,9 @@ fn artboard_summary(a: &vectorcraft_doc::Artboard) -> Value { if let Some(c) = a.color { v["color"] = json!(c.to_hex()); } + if let Some(c) = a.guide_label { + v["guideLabel"] = json!(c.label_text()); + } for (key, on) in [ ("locked", a.locked), ("showCenterMark", a.show_center_mark), diff --git a/crates/engine/src/tests/panelcmds.rs b/crates/engine/src/tests/panelcmds.rs index 239d6bee7..4d549c31e 100644 --- a/crates/engine/src/tests/panelcmds.rs +++ b/crates/engine/src/tests/panelcmds.rs @@ -2,7 +2,7 @@ use serde_json::json; use vectorcraft_color::{Color, GradientKind, Paint}; -use vectorcraft_doc::{AppearanceItem, NodeKind}; +use vectorcraft_doc::{AppearanceItem, LayerColor, NodeKind}; use crate::*; @@ -1387,6 +1387,128 @@ fn artboard_video_display_options_are_set_in_one_undo_step() { assert_eq!(video(&s, 1), (false, 10.0), "10 is in range"); } +/// Artboard Options' guide label: `artboard.setProps {guideLabel}` takes a layer color's name or +/// `#rrggbb` and null clears it, each change one undo step, also on a locked artboard (whose +/// rectangle stays locked) and for several artboards in one list; a value equal to the artboard's +/// adds no undo step. `artboard.new` takes it, a duplicate and an undone delete keep it, and +/// `inspect_document` reports it. A bad value anywhere in a list changes nothing. +#[test] +fn artboard_guide_labels_are_set_and_cleared_in_one_undo_step() { + let mut s = session(); + s.execute("artboard.new", &json!({})).unwrap(); + let label = |s: &Session, i: usize| s.doc().unwrap().doc.artboards[i].guide_label; + let undo = |s: &Session| s.doc().unwrap().history.undo.len(); + assert_eq!((label(&s, 0), label(&s, 1)), (None, None), "no label by default"); + let u0 = undo(&s); + s.execute("artboard.setProps", &json!({"index": 1, "guideLabel": "Red"})).unwrap(); + assert_eq!((label(&s, 0), label(&s, 1)), (None, Some(LayerColor::Preset(1))), "only that artboard"); + s.execute("artboard.setProps", &json!({"index": 1, "guideLabel": "#12aBcd"})).unwrap(); + assert_eq!(label(&s, 1), Some(LayerColor::Custom([0x12, 0xab, 0xcd]))); + s.execute("artboard.setProps", &json!({"index": 1, "guideLabel": null})).unwrap(); + assert_eq!(label(&s, 1), None, "null clears it"); + assert_eq!(undo(&s), u0 + 3, "one undo step each"); + for was in [Some(LayerColor::Custom([0x12, 0xab, 0xcd])), Some(LayerColor::Preset(1)), None] { + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(label(&s, 1), was); + } + s.execute("edit.redo", &json!({})).unwrap(); + // The same label again, or a list that changes nothing: no undo step. + let u = undo(&s); + s.execute("artboard.setProps", &json!({"index": 1, "guideLabel": "Red"})).unwrap(); + s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "guideLabel": null}, {"index": 1, "guideLabel": "Red"}]})).unwrap(); + assert_eq!(undo(&s), u, "nothing changed, no undo step"); + // A locked artboard takes a label; its rectangle stays locked. + s.execute("artboard.setProps", &json!({"index": 0, "locked": true})).unwrap(); + let rect0 = s.doc().unwrap().doc.artboards[0].rect; + s.execute("artboard.setProps", &json!({"index": 0, "guideLabel": "Gold"})).unwrap(); + assert_eq!(label(&s, 0), Some(LayerColor::Preset(15))); + assert!(s.execute("artboard.setProps", &json!({"index": 0, "x": 999, "guideLabel": "Teal"})).is_err()); + assert_eq!((s.doc().unwrap().doc.artboards[0].rect, label(&s, 0)), (rect0, Some(LayerColor::Preset(15)))); + let u = undo(&s); + s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "guideLabel": "Teal"}, {"index": 1, "guideLabel": "#000000"}]})).unwrap(); + assert_eq!((label(&s, 0), label(&s, 1)), (Some(LayerColor::Preset(11)), Some(LayerColor::Custom([0, 0, 0]))), "each its own"); + assert_eq!(undo(&s), u + 1, "in one undo step"); + let doc = crate::inspect::document(&s); + assert_eq!((doc["artboards"][0]["guideLabel"].clone(), doc["artboards"][1]["guideLabel"].clone()), (json!("Teal"), json!("#000000"))); + s.execute("artboard.setProps", &json!({"index": 1, "guideLabel": null})).unwrap(); + assert!(crate::inspect::document(&s)["artboards"][1].get("guideLabel").is_none(), "no label isn't reported"); + let copy = s.execute("artboard.duplicate", &json!({"index": 0})).unwrap()["index"].as_u64().unwrap() as usize; + assert_eq!(label(&s, copy), Some(LayerColor::Preset(11)), "a duplicate keeps it"); + let made = s.execute("artboard.new", &json!({"guideLabel": "Violet"})).unwrap()["index"].as_u64().unwrap() as usize; + assert_eq!(label(&s, made), Some(LayerColor::Preset(14)), "a new artboard takes it"); + let plain = s.execute("artboard.new", &json!({})).unwrap()["index"].as_u64().unwrap() as usize; + assert_eq!(label(&s, plain), None); + s.execute("artboard.delete", &json!({"index": made})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(label(&s, made), Some(LayerColor::Preset(14)), "undoing a delete brings it back"); + let before = s.doc().unwrap().doc.clone(); + let bads = [ + json!("red"), + json!(" Red"), + json!("#12345"), + json!("#1234567"), + json!("#12345678"), + json!("#fff"), + json!("#gg0000"), + json!(1), + json!([1, 2, 3]), + json!({"Preset": 1}), + json!(true), + ]; + for bad in bads { + assert!(s.execute("artboard.setProps", &json!({"index": 1, "guideLabel": bad})).is_err(), "{bad}"); + assert!(s.execute("artboard.new", &json!({"guideLabel": bad})).is_err(), "{bad}"); + assert!( + s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "guideLabel": "Red"}, {"index": 1, "guideLabel": bad}]})).is_err(), + "{bad}" + ); + } + assert!(s.execute("artboard.setProps", &json!({"artboards": [{"index": 1, "guideLabel": "Red"}, {"index": 1, "guideLabel": "Red"}]})).is_err()); + assert!(s.execute("artboard.setProps", &json!({"artboards": [{"index": 1, "guideLabel": "Red"}, {"index": 99, "guideLabel": "Red"}]})).is_err()); + assert_eq!(*s.doc().unwrap().doc, *before, "a bad value changes nothing"); +} + +/// The native format keeps the guide label, as `{"Preset": n}` or `{"Custom": [r, g, b]}`. An +/// artboard without one is written as before labels could be set, and a preset outside the layer +/// colors in a file reads as no label. Exports leave the label out. +#[test] +fn artboard_guide_labels_round_trip_through_the_native_format() { + let mut s = session(); + rect(&mut s); + s.execute("artboard.new", &json!({})).unwrap(); + let save = |s: &Session| vectorcraft_format::save(&s.doc().unwrap().doc, false); + let before = save(&s); + let plain = (*s.doc().unwrap().doc).clone(); + s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "guideLabel": "Green"}, {"index": 1, "guideLabel": "#102030"}]})).unwrap(); + let bytes = save(&s); + let saved: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + let ab = |i: usize| saved["document"]["artboards"][i]["guide_label"].clone(); + assert_eq!((ab(0), ab(1)), (json!({"Preset": 2}), json!({"Custom": [16, 32, 48]}))); + let back = vectorcraft_format::load(&bytes).unwrap(); + assert_eq!(back.artboards, s.doc().unwrap().doc.artboards); + // Screen-only: SVG and PDF exports are the same as without the labels. + let on = (*s.doc().unwrap().doc).clone(); + assert_eq!(vectorcraft_svg::export(&on, &Default::default()), vectorcraft_svg::export(&plain, &Default::default())); + let options = vectorcraft_pdf::PdfOptions { created: Some(0), ..Default::default() }; + assert_eq!(crate::export_pdf(&on, &options).unwrap(), crate::export_pdf(&plain, &options).unwrap()); + s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "guideLabel": null}, {"index": 1, "guideLabel": null}]})).unwrap(); + assert_eq!(save(&s), before, "without labels, the file is as it was"); + // A file's labels as another writer might put them: a preset past the layer colors reads as + // none, and a label that isn't a layer color at all is refused. + let mut edited = saved.clone(); + edited["document"]["artboards"][0]["guide_label"] = json!({"Preset": 27}); + edited["document"]["artboards"][1]["guide_label"] = json!({"Preset": 26}); + let back = vectorcraft_format::load(&serde_json::to_vec(&edited).unwrap()).unwrap(); + assert_eq!((back.artboards[0].guide_label, back.artboards[1].guide_label), (None, Some(LayerColor::Preset(26)))); + for bad in [json!({"Preset": 256}), json!({"Custom": [1, 2]}), json!("Red"), json!({"Custom": [1, 2, 300]})] { + edited["document"]["artboards"][0]["guide_label"] = bad.clone(); + assert!(vectorcraft_format::load(&serde_json::to_vec(&edited).unwrap()).is_err(), "{bad}"); + } + edited["document"]["artboards"][0]["guide_label"] = json!(null); + let back = vectorcraft_format::load(&serde_json::to_vec(&edited).unwrap()).unwrap(); + assert_eq!(back.artboards[0].guide_label, None); +} + /// The native format keeps Show Video Safe Areas and the Video Ruler Pixel Aspect Ratio. A /// document whose artboards hide the safe areas and have square pixels is written byte for byte as /// before they could be set, a file written without the two keys opens with the safe areas off diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 63e9f3df7..0e1ca6767 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -1209,6 +1209,7 @@ impl<'a> Reader<'a> { show_video_safe_areas: b.safe_areas, video_ruler_pixel_aspect_ratio: b.par, color: None, + guide_label: None, }) .collect(); if doc.artboards.is_empty() { @@ -1223,6 +1224,7 @@ impl<'a> Reader<'a> { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }]; } doc.opened_artboard = active.and_then(|i| doc.artboards.get(i)).map(|a| a.id); diff --git a/crates/pdf/src/import/entry.rs b/crates/pdf/src/import/entry.rs index 687fcb156..38f22689a 100644 --- a/crates/pdf/src/import/entry.rs +++ b/crates/pdf/src/import/entry.rs @@ -375,6 +375,7 @@ fn read_opened( show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); let page_layer = |b: &mut Builder<'_>, art: Vec>, shown: bool| { let name = if shown { format!("Page {}", number + 1) } else { format!("Page {} (unsorted)", number + 1) }; diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 365b65389..af744437d 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -131,6 +131,7 @@ fn one_page_per_artboard_with_sizes() { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); d.artboards.push(Artboard { id: 3, @@ -142,6 +143,7 @@ fn one_page_per_artboard_with_sizes() { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); let bytes = export(&d, &PdfOptions::default()).unwrap(); let pdf = craft_pdf::syntax::Pdf::new(bytes).unwrap(); @@ -162,6 +164,7 @@ fn artboard_range_and_bad_index() { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); let bytes = export(&d, &PdfOptions { artboards: Some(vec![1]), ..Default::default() }).unwrap(); let pdf = craft_pdf::syntax::Pdf::new(bytes).unwrap(); @@ -893,6 +896,7 @@ fn import_max_pages_and_multi_page() { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); add(&mut d, rect_node(Rect::new(10.0, 10.0, 50.0, 50.0), Color::BLACK)); add(&mut d, rect_node(Rect::new(310.0, 10.0, 350.0, 50.0), Color::BLACK)); diff --git a/crates/pdf/src/tests/print.rs b/crates/pdf/src/tests/print.rs index daf2bab07..2f59563e7 100644 --- a/crates/pdf/src/tests/print.rs +++ b/crates/pdf/src/tests/print.rs @@ -33,6 +33,7 @@ fn artboard(id: u32, x: f64) -> Artboard { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, } } diff --git a/crates/pdf/src/tests/webview.rs b/crates/pdf/src/tests/webview.rs index 9a4659ddc..a40db425e 100644 --- a/crates/pdf/src/tests/webview.rs +++ b/crates/pdf/src/tests/webview.rs @@ -31,6 +31,7 @@ fn doc(n: usize, image: bool, on_first: bool) -> Document { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); } let mut png = vec![]; diff --git a/crates/svg/src/import/layers.rs b/crates/svg/src/import/layers.rs index 04e388794..15a77e37f 100644 --- a/crates/svg/src/import/layers.rs +++ b/crates/svg/src/import/layers.rs @@ -146,6 +146,7 @@ pub(super) fn inkscape_pages(xml: &roxmltree::Document, user: Affine) -> Option< show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }) .collect(); (!pages.is_empty()).then_some(pages) diff --git a/crates/tools/src/rulerguide/tests.rs b/crates/tools/src/rulerguide/tests.rs index a4a91d24c..4b4f1be72 100644 --- a/crates/tools/src/rulerguide/tests.rs +++ b/crates/tools/src/rulerguide/tests.rs @@ -289,6 +289,7 @@ fn an_artboard_guide_is_picked_and_snapped_to_across_its_artboard_only() { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); d.guides = vec![Guide { artboard: Some(id), ..Guide::new(false, 150.0) }]; let (s, p) = (Selection::default(), paint()); @@ -604,6 +605,7 @@ fn ruler_guide_readouts_count_from_the_active_artboard_with_artboard_rulers() { show_video_safe_areas: false, video_ruler_pixel_aspect_ratio: 1.0, color: None, + guide_label: None, }); doc.ruler_origin = Point::new(5.0, 5.0); let (paint, selection) = (paint(), Selection::default()); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c25acd07a..63be246da 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1087,11 +1087,14 @@ fn guide_lines(app: &VectorcraftApp, painter: &egui::Painter, xf: &Xf, rect: egu } } -/// The ruler guides of document `st`, its selected guides in the selection colour. +/// The ruler guides of document `st`, its selected guides in the selection colour. An artboard +/// guide of an artboard with a guide label ([`vectorcraft_doc::Artboard::guide_label`]) is drawn in +/// the label's color, in the guide preference's style; the others in the guide preference's color. fn guides_of(app: &VectorcraftApp, st: &vectorcraft_engine::DocState, (painter, xf, t): (&egui::Painter, &Xf, &Tokens), rect: egui::Rect) { let doc = &st.doc; let look = LineLook::guides(&app.session.prefs, t); let picked = LineLook { color: t.selection, ..look }; + let labels = guide_labels(doc); let selected: &[usize] = &st.selection.guides; let key = st.selection.key_guide; let view = view_bounds(xf); @@ -1108,7 +1111,8 @@ fn guides_of(app: &VectorcraftApp, st: &vectorcraft_engine::DocState, (painter, ends } }; - if selected.contains(&i) { picked } else { look }.guide(painter, a, b); + let own = g.artboard.and_then(|id| labels.get(&id)).map_or(look, |&color| LineLook { color, ..look }); + if selected.contains(&i) { picked } else { own }.guide(painter, a, b); // The key guide (Align to Key Object, #2143): thicker, as a key object's outline is. if key == Some(i) { painter.line_segment([a, b], Stroke::new(KEY_OUTLINE, picked.color)); @@ -1116,6 +1120,17 @@ fn guides_of(app: &VectorcraftApp, st: &vectorcraft_engine::DocState, (painter, } } +/// The guide label colors of `doc`'s artboards that have one, by artboard id. +pub(super) fn guide_labels(doc: &vectorcraft_doc::Document) -> std::collections::HashMap { + doc.artboards + .iter() + .filter_map(|a| { + let [r, g, b] = a.guide_label?.rgb(); + Some((a.id, Color32::from_rgb(r, g, b))) + }) + .collect() +} + /// The selection's visuals and the tools' overlays, to be drawn. fn selection_visuals( app: &mut VectorcraftApp, diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index b76599a4b..a9ef2c228 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -1208,6 +1208,73 @@ fn a_drag_from_a_ruler_onto_the_canvas_makes_a_guide() { assert_eq!(app.session.active().unwrap().doc.art_bounds(), None, "the tool drew nothing"); } +/// An artboard's guide label colors its own guides only: a canvas guide, a guide of an artboard +/// without a label and a guide of a deleted artboard keep the guide color preference, and a +/// selected labeled guide takes the selection color. Hide Guides hides labeled guides too. +#[test] +fn artboard_guide_label_colors_only_its_own_guides() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.session.execute("artboard.new", &json!({})).unwrap(); + app.session.execute("artboard.new", &json!({})).unwrap(); + app.session.execute("artboard.setProps", &json!({"index": 0, "guideLabel": "Red"})).unwrap(); + app.session.execute("artboard.setProps", &json!({"index": 2, "guideLabel": "#00ff00"})).unwrap(); + // Vertical guides at x 100 (artboard 1, Red), 520 (artboard 2, no label) and 940 (artboard + // 3, whose artboard is then deleted), and a horizontal canvas guide at y 150. + for (pos, artboard) in [(100, json!(0)), (520, json!(1)), (940, json!(2))] { + app.session.execute("guide.add", &json!({"vertical": true, "pos": pos, "artboard": artboard})).unwrap(); + } + app.session.execute("guide.add", &json!({"vertical": false, "pos": 150})).unwrap(); + // Deleting artboard 3 deletes its guide; a guide still naming its id (as a file might) is an + // orphan, drawn across the canvas. + let gone = app.session.active().unwrap().doc.artboards[2].id; + app.session.execute("artboard.setProps", &json!({"index": 2, "guideLabel": null})).unwrap(); + app.session.execute("artboard.delete", &json!({"index": 2})).unwrap(); + app.session.execute("guide.add", &json!({"vertical": true, "pos": 300})).unwrap(); + { + let st = app.session.active_mut().unwrap(); + let doc = std::sync::Arc::make_mut(&mut st.doc); + let last = doc.guides.len() - 1; + doc.guides[last].artboard = Some(gone); + } + app.ui.view.guides = true; + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let t = Tokens::get(&ctx); + let pref = LineLook::guides(&app.session.prefs, &t).color; + let colors = |app: &mut VectorcraftApp, at: f64, vertical: bool| -> Vec { + shapes(app, &ctx) + .iter() + .filter_map(|s| match s { + Shape::LineSegment { points, stroke } => { + let (a, b) = (xf.to_doc(points[0]), xf.to_doc(points[1])); + let (u, v) = if vertical { (a.x, b.x) } else { (a.y, b.y) }; + ((u - at).abs() < 0.01 && (v - at).abs() < 0.01).then_some(stroke.color) + } + _ => None, + }) + .collect() + }; + let red = Color32::from_rgb(0xff, 0x4f, 0x4f); + assert_eq!(colors(&mut app, 100.0, true), [red], "the labeled artboard's guide"); + assert_eq!(colors(&mut app, 520.0, true), [pref], "an artboard without a label"); + assert_eq!(colors(&mut app, 150.0, false), [pref], "a canvas guide"); + assert!(colors(&mut app, 940.0, true).is_empty(), "deleted with its artboard"); + assert_eq!(colors(&mut app, 300.0, true), [pref], "a guide whose artboard is gone"); + assert_ne!(red, pref); + // Selected, it is drawn in the selection color; deselected, in the label's again. + app.session.execute("guide.select", &json!({"indexes": [0]})).unwrap(); + assert_eq!(colors(&mut app, 100.0, true), [t.selection], "selected"); + app.session.execute("guide.select", &json!({"indexes": []})).unwrap(); + assert_eq!(colors(&mut app, 100.0, true), [red], "deselected"); + // A new label shows on the next frame. + app.session.execute("artboard.setProps", &json!({"index": 0, "guideLabel": "Black"})).unwrap(); + assert_eq!(colors(&mut app, 100.0, true), [Color32::BLACK]); + app.ui.view.guides = false; + assert!(colors(&mut app, 100.0, true).is_empty(), "Hide Guides hides it"); +} + /// #4646: a guide dragged out of a ruler shows as a line across the canvas while it is dragged, /// also with Preferences › Performance › Real-time Drawing and Editing off (only its position /// label showed then). diff --git a/crates/ui-egui/src/dialogs/artboard_options.rs b/crates/ui-egui/src/dialogs/artboard_options.rs index 0ce0de935..1854eba4b 100644 --- a/crates/ui-egui/src/dialogs/artboard_options.rs +++ b/crates/ui-egui/src/dialogs/artboard_options.rs @@ -1,7 +1,8 @@ //! Artboard Options (double-click with the Artboard tool, double-click the tool, or the Artboards //! panel's options icon or menu): the artboard's fields, sizes parsed with units, its position //! from a reference point, its background color (`color`: a color as the paint commands take -//! it, or null for none), Show Center Mark and Show Cross Hairs (`showCenterMark`, +//! it, or null for none), its guide label (`guideLabel`: the color its own guides are drawn in, a +//! layer color's name or `#rrggbb`, or null for none), Show Center Mark and Show Cross Hairs (`showCenterMark`, //! `showCrossHairs`), the display marks the canvas draws on the artboard //! (https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/modify-display-settings-of-artboards.html), //! and its video display options, Show Video Safe Areas and Video Ruler Pixel Aspect Ratio @@ -15,7 +16,7 @@ use serde_json::{Value, json}; use vectorcraft_color::Color; -use vectorcraft_doc::Unit; +use vectorcraft_doc::{LAYER_COLORS, LayerColor, Unit}; use vectorcraft_engine::cmd::color_value; use vectorcraft_geom::{Point, Rect}; @@ -32,7 +33,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec { confirm, // A new artboard's dialog opens without these; its swatch, checkboxes and Video Ruler Pixel // Aspect Ratio field add them. The Global checkboxes' fields come from the preferences. - inputs: |_| &["color", "showCenterMark", "showCrossHairs", "showVideoSafeAreas", PIXEL_ASPECT, FADE, UPDATE_WHILE_DRAGGING], + inputs: |_| &["color", GUIDE_LABEL, "showCenterMark", "showCrossHairs", "showVideoSafeAreas", PIXEL_ASPECT, FADE, UPDATE_WHILE_DRAGGING], ..DialogSpec::FORM }; @@ -49,7 +50,7 @@ fn placed(d: &Dialog) -> Rect { /// as the Transform panel and the Control bar show an artboard's. A new Width or Height keeps the /// reference point where it was, as their sizes do. /// Then Background Color: the Fill list (None, White, Black; Custom for another color) and a swatch -/// that picks a color of its own, the Show Center Mark, Show Cross Hairs and Show Video Safe Areas +/// that picks a color of its own, the Guide Label row ([`guide_label_row`]), the Show Center Mark, Show Cross Hairs and Show Video Safe Areas /// checkboxes, and the Video Ruler Pixel Aspect Ratio field. Last, under Global, the Fade Region /// Outside Artboard and Update While Dragging checkboxes. fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { @@ -116,6 +117,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { if let Some(c) = set { d.fields.insert("color".into(), c.as_ref().map_or(Value::Null, crate::panels::color_json)); } + guide_label_row(ui, d); ui.add_space(8.0); form::check(ui, d, "showCenterMark", tl!("Show Center Mark")); form::check(ui, d, "showCrossHairs", tl!("Show Cross Hairs")); @@ -137,6 +139,64 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { false } +/// The field of the guide label, as `artboard.setProps` and `artboard.new` take it: a layer +/// color's name, `#rrggbb`, or null for none. +pub(super) const GUIDE_LABEL: &str = "guideLabel"; + +/// The size of the color chip before each name in the Guide Label list. +const CHIP: egui::Vec2 = egui::vec2(16.0, 10.0); + +/// The Guide Label row: a list of None, the layer colors (each after a chip of its color) and +/// Custom, and, while the artboard has a label, a swatch that picks a color of its own. Custom +/// keeps the label's color as a `#rrggbb` value. +fn guide_label_row(ui: &mut egui::Ui, d: &mut Dialog) { + let now = d.fields.get(GUIDE_LABEL).and_then(Value::as_str).and_then(LayerColor::from_label_text); + let shown = match now { + None => "None", + Some(LayerColor::Preset(i)) => LAYER_COLORS.get(usize::from(i)).map_or("Custom", |(n, _)| *n), + Some(LayerColor::Custom(_)) => "Custom", + }; + let mut set = None; + ui.horizontal(|ui| { + let row = form::row_label(ui, tl!("Guide Label:")); + ui.label_inputs_by(row.id, |ui| { + let border = crate::theme::Tokens::get(ui.ctx()).input_border; + let rows = std::iter::once(("None", None)).chain(LAYER_COLORS.iter().map(|(n, c)| (*n, Some(*c)))).chain([("Custom", None)]); + let picked = crate::widgets::combo(ui, "ab-options-guide-label", tl!(shown), 140.0, |ui| { + let mut chosen = None; + for (i, (name, rgb)) in rows.enumerate() { + let chip = egui::IdSalt::new(("ab-guide-label-chip", i)); + let r = egui::Button::selectable(name == shown, (egui::Atom::custom(chip, CHIP), tl!(name))).atom_ui(ui); + if let (Some(rect), Some([cr, cg, cb])) = (r.rect(chip), rgb) { + ui.painter().rect_filled(rect, 1.0, egui::Color32::from_rgb(cr, cg, cb)); + ui.painter().rect_stroke(rect, 1.0, egui::Stroke::new(1.0, border), egui::StrokeKind::Inside); + } + if r.response.clicked() { + chosen = Some(i); + } + } + chosen + }); + match picked { + Some(0) => set = Some(None), + Some(i) if i <= LAYER_COLORS.len() => set = u8::try_from(i - 1).ok().map(|i| Some(LayerColor::Preset(i))), + Some(_) => set = Some(Some(LayerColor::Custom(now.map_or(LAYER_COLORS[0].1, LayerColor::rgb)))), + None => {} + } + if let Some(c) = now { + let mut rgb = c.rgb(); + let swatch_name = format!("{} {}", tl!("Guide Label:"), tl!("Swatches")); + if ui.name_inputs(swatch_name, |ui| ui.color_edit_button_srgb(&mut rgb)).changed() { + set = Some(Some(LayerColor::Custom(rgb))); + } + } + }); + }); + if let Some(c) = set { + d.fields.insert(GUIDE_LABEL.into(), c.map_or(Value::Null, |c| json!(c.label_text()))); + } +} + /// The preference and field of Fade Region Outside Artboard: while the Artboard tool is active, /// the canvas draws everything outside the artboards darker. const FADE: &str = "fadeRegionOutsideArtboard"; @@ -292,8 +352,10 @@ pub fn open(app: &mut VectorcraftApp) -> Result { let a = st.doc.artboards.get(index).ok_or("no artboard")?; let r = a.rect; let color = a.color.as_ref().map(crate::panels::color_json); + let guide_label = a.guide_label.map(LayerColor::label_text); let fields = json!({ "index": index, "name": a.name, "x": r.x0, "y": r.y0, "width": r.width(), "height": r.height(), "color": color, + GUIDE_LABEL: guide_label, "showCenterMark": a.show_center_mark, "showCrossHairs": a.show_cross_hairs, "showVideoSafeAreas": a.show_video_safe_areas, PIXEL_ASPECT: a.video_ruler_pixel_aspect_ratio, }); @@ -374,7 +436,7 @@ pub fn open_new(app: &mut VectorcraftApp) -> Result { } /// OK for a new artboard: `artboard.new` with the fields (an empty name gives the default one), -/// the background color picked, the display marks ticked and the Video Ruler Pixel Aspect Ratio +/// the background color and guide label picked, the display marks ticked and the Video Ruler Pixel Aspect Ratio /// typed, and the new artboard becomes the active one. fn confirm_new(app: &mut VectorcraftApp, d: &Dialog) -> Result { let mut p = json!({}); @@ -388,6 +450,9 @@ fn confirm_new(app: &mut VectorcraftApp, d: &Dialog) -> Result { if let Some(c) = d.fields.get("color") { p["color"] = c.clone(); } + if let Some(c) = d.fields.get(GUIDE_LABEL).filter(|c| !c.is_null()) { + p[GUIDE_LABEL] = c.clone(); + } for k in ["showCenterMark", "showCrossHairs", "showVideoSafeAreas"] { if d.bool(k) { p[k] = json!(true); diff --git a/crates/ui-egui/src/dialogs/form.rs b/crates/ui-egui/src/dialogs/form.rs index c87cfb7e6..bbddd222f 100644 --- a/crates/ui-egui/src/dialogs/form.rs +++ b/crates/ui-egui/src/dialogs/form.rs @@ -120,7 +120,7 @@ fn suffix(kind: &str, key: &str) -> Option<&'static str> { /// The fields of the form dialogs ([`grid`]) that the dialog's own body draws, by dialog kind. fn own_rows(kind: &str) -> &'static [&'static str] { match kind { - "artboardOptions" => &["color", super::artboard_options::PIXEL_ASPECT], + "artboardOptions" => &["color", super::artboard_options::GUIDE_LABEL, super::artboard_options::PIXEL_ASPECT], // The grids' Reference Point is their 9-point locator. "rectangularGrid" | "polarGrid" => &["reference"], _ => &[], diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index c5bdfea5c..2ad8bdf21 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -1755,6 +1755,64 @@ fn new_artboard_options_give_the_new_artboard_its_background_color() { } } +/// Artboard Options shows the artboard's guide label on its Guide Label: row (None, a layer color's +/// name, or Custom). OK sets a label picked there in one undo step, keeps it while another field +/// changes, and a null `guideLabel` clears it; Cancel leaves it. OK with nothing changed adds no +/// undo step, and a new artboard's Artboard Options gives it the label picked. +#[test] +fn artboard_options_shows_sets_and_clears_the_guide_label() { + let mut app = app(); + let label = |app: &VectorcraftApp| app.session.doc().unwrap().doc.artboards[0].guide_label.map(vectorcraft_doc::LayerColor::label_text); + let undo = |app: &VectorcraftApp| app.session.doc().unwrap().history.undo.len(); + let lines = |app: &mut VectorcraftApp| { + let text = crate::tests_labels::painted_text(app, |app, ui| show(app, ui.ctx())); + text.lines().map(str::trim).map(str::to_string).collect::>() + }; + crate::dialogs::artboard_options::open(&mut app).unwrap(); + let shown = lines(&mut app); + assert!(shown.iter().any(|l| l == "Guide Label:"), "{shown:?}"); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["guideLabel"], json!(null)); + let u = undo(&app); + super::confirm(&mut app).unwrap(); + assert_eq!((label(&app), undo(&app)), (None, u), "OK with nothing changed"); + crate::dialogs::artboard_options::open(&mut app).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("guideLabel".into(), json!("Orange")); + super::confirm(&mut app).unwrap(); + assert_eq!((label(&app).as_deref(), undo(&app)), (Some("Orange"), u + 1)); + crate::dialogs::artboard_options::open(&mut app).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["guideLabel"], json!("Orange")); + assert!(lines(&mut app).iter().any(|l| l == "Orange")); + app.ui.dialog.as_mut().unwrap().fields.insert("guideLabel".into(), json!("#123456")); + cancel(&mut app); + assert_eq!(label(&app).as_deref(), Some("Orange"), "Cancel leaves it"); + crate::dialogs::artboard_options::open(&mut app).unwrap(); + assert!(lines(&mut app).iter().any(|l| l == "Orange")); + app.ui.dialog.as_mut().unwrap().fields.insert("guideLabel".into(), json!("#123456")); + super::confirm(&mut app).unwrap(); + crate::dialogs::artboard_options::open(&mut app).unwrap(); + assert!(lines(&mut app).iter().any(|l| l == "Custom")); + app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Card")); + super::confirm(&mut app).unwrap(); + assert_eq!(label(&app).as_deref(), Some("#123456"), "kept while another field changes"); + crate::dialogs::artboard_options::open(&mut app).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("guideLabel".into(), json!("purple")); + assert!(super::confirm(&mut app).is_err(), "a name in other letter case is refused"); + assert_eq!(label(&app).as_deref(), Some("#123456")); + crate::dialogs::artboard_options::open(&mut app).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("guideLabel".into(), json!(null)); + super::confirm(&mut app).unwrap(); + assert_eq!(label(&app), None); + for (picked, want) in [(Some(json!("Teal")), Some("Teal")), (None, None)] { + app.run("ui.newArtboard", json!({})).unwrap(); + if let Some(c) = picked { + app.ui.dialog.as_mut().unwrap().fields.insert("guideLabel".into(), c); + } + super::confirm(&mut app).unwrap(); + let ab = app.session.doc().unwrap().doc.artboards.last().cloned().unwrap(); + assert_eq!(ab.guide_label.map(vectorcraft_doc::LayerColor::label_text).as_deref(), want); + } +} + /// Artboard Options shows Show Center Mark and Show Cross Hairs, ticked as the artboard has them /// whether it opens from the Artboards panel or from a double-click with the Artboard tool. OK /// sets them in one undo step, and a new artboard's Artboard Options gives it the ones ticked. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index fe765e1f4..ee32cee33 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -276,6 +276,7 @@ font-technology Variable Променливи Average… Усредняване… Axis: Ос: Background Color: Цвят на фона: + Guide Label: Етикет на водачите: Show Center Mark Показване на централен знак Show Cross Hairs Показване на кръстосани линии Show Video Safe Areas Показване на безопасните видеозони diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index b8cb39bc3..0970a7092 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -2623,6 +2623,7 @@ digits Malayalam Malajalam File Dimensions Dimenzije datoteke Transforms Transformacije Background Color: Boja pozadine: + Guide Label: Oznaka vodilica: Show Center Mark Prikaži oznaku centra Show Cross Hairs Prikaži končanicu Show Video Safe Areas Prikaži sigurne oblasti za video diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index fe9ea5e9d..84b6833c4 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -428,6 +428,7 @@ antiAlias Sharp Nítid B5 B5 Back Enrere Background Color: Color de fons: + Guide Label: Etiqueta de les guies: Background Contents Contingut del fons Background Level Nivell de fons Background: Fons: @@ -5129,6 +5130,7 @@ digits Malayalam Malaiàlam @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis ha de ser "vertical", "horizontal" o un angle en graus, no `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" fons {other}: transparent, blanc, negre o un color com "#ff8800" @msg bad color {v} color incorrecte {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` és null, el nom d’un color de capa o #rrggbb @msg bad colour `{hex}` (#rrggbb) color incorrecte `{hex}` (#rrggbb) @msg bad page tree arbre de pàgines incorrecte @msg bad shortcut drecera incorrecta diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 9e05cd632..733a3e813 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2654,6 +2654,7 @@ digits Malayalam Malajálamské File Dimensions Rozměry souboru Transforms Transformace Background Color: Barva pozadí: + Guide Label: Štítek vodítek: Show Center Mark Zobrazit středovou značku Show Cross Hairs Zobrazit nitkový kříž Show Video Safe Areas Zobrazit bezpečné oblasti videa @@ -4594,6 +4595,7 @@ imageTrace Outline Obrys @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis musí být "vertical", "horizontal" nebo úhel ve stupních, ne `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" pozadí {other}: transparent, white, black nebo barva jako "#ff8800" @msg bad color {v} chybná barva {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` je null, název barvy vrstvy nebo #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio musí být číslo od 0,1 do 10 @msg the Color Picker is not open Výběr barvy není otevřený @msg the Simplify slider is not open Posuvník Zjednodušit není otevřený diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b1ee1a1a6..d1871b2ed 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2442,6 +2442,7 @@ digits Malayalam Malayalam File Dimensions Fildimensioner Transforms Transformationer Background Color: Baggrundsfarve: + Guide Label: Hjælpelinjeetiket: Show Center Mark Vis midtermærke Show Cross Hairs Vis trådkors Show Video Safe Areas Vis sikre videoområder diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 1a742ca5c..8c75b25b6 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -264,6 +264,7 @@ font-technology Variable Variabel Average… Durchschnitt berechnen… Axis: Achse: Background Color: Hintergrundfarbe: + Guide Label: Farbmarkierung der Hilfslinien: Show Center Mark Mittelpunktmarke einblenden Show Cross Hairs Fadenkreuz einblenden Show Video Safe Areas Videosichere Bereiche einblenden @@ -4201,6 +4202,7 @@ theme Appearance Erscheinungsbild @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis muss "vertical", "horizontal" oder ein Winkel in Grad sein, nicht `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparent, white, black oder eine Farbe wie "#ff8800" @msg bad color {v} ungültige Farbe {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` ist null, der Name einer Ebenenfarbe oder #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio muss eine Zahl von 0,1 bis 10 sein @msg the Color Picker is not open der Farbwähler ist nicht geöffnet @msg the Simplify slider is not open der Regler „Vereinfachen“ ist nicht geöffnet diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index ec7b41b34..7813ee6cf 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -267,6 +267,7 @@ font-technology Variable Μεταβλητές Average… Μέσος όρος… Axis: Άξονας: Background Color: Χρώμα φόντου: + Guide Label: Ετικέτα οδηγών: Show Center Mark Εμφάνιση σημαδιού κέντρου Show Cross Hairs Εμφάνιση σταυρονήματος Show Video Safe Areas Εμφάνιση ασφαλών περιοχών βίντεο @@ -4245,6 +4246,7 @@ digits Malayalam Μαλαγιάλαμ @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` το axis πρέπει να είναι "vertical", "horizontal" ή γωνία σε μοίρες, όχι `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" φόντο {other}: transparent, white, black ή χρώμα όπως "#ff8800" @msg bad color {v} μη έγκυρο χρώμα {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb το `guideLabel` είναι null, όνομα χρώματος επιπέδου ή #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 Το videoRulerPixelAspectRatio πρέπει να είναι αριθμός από 0,1 έως 10 @msg bad colour `{hex}` (#rrggbb) μη έγκυρο χρώμα `{hex}` (#rrggbb) @msg bad page tree μη έγκυρο δέντρο σελίδων diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index b44e03086..d8bcecbc6 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -289,6 +289,7 @@ font-technology Variable Variables Average… Promediar… Axis: Eje: Background Color: Color de fondo: + Guide Label: Etiqueta de las guías: Show Center Mark Mostrar marca central Show Cross Hairs Mostrar líneas en cruz Show Video Safe Areas Mostrar áreas seguras de video @@ -4285,6 +4286,7 @@ axis Both Ambos @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis debe ser "vertical", "horizontal" o un ángulo en grados, no `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" fondo {other}: transparent, white, black o un color como "#ff8800" @msg bad color {v} color no válido {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` es null, el nombre de un color de capa o #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio debe ser un número de 0,1 a 10 @msg the Color Picker is not open el Selector de color no está abierto @msg the Simplify slider is not open el regulador de Simplificar no está abierto diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 814d768b2..d3b9fb326 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -2598,6 +2598,7 @@ digits Malayalam Malajalam File Dimensions Tiedoston mitat Transforms Muunnokset Background Color: Taustaväri: + Guide Label: Apuviivojen tunniste: Show Center Mark Näytä keskimerkki Show Cross Hairs Näytä hiusristikko Show Video Safe Areas Näytä videon turva-alueet @@ -4507,6 +4508,7 @@ imageTrace Outline Ääriviiva @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis-arvon on oltava "vertical", "horizontal" tai kulma asteina, ei `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" tausta {other}: transparent, white, black tai väri, kuten "#ff8800" @msg bad color {v} virheellinen väri {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` on null, tasovärin nimi tai #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio on oltava luku väliltä 0,1–10 @msg bad colour `{hex}` (#rrggbb) virheellinen väri `{hex}` (#rrggbb) @msg bad page tree virheellinen sivupuu diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index ff19cf643..6db506204 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -290,6 +290,7 @@ font-technology Variable Variables Average… Alignement… Axis: Axe : Background Color: Couleur d’arrière-plan : + Guide Label: Étiquette des repères : Show Center Mark Afficher le repère central Show Cross Hairs Afficher le réticule Show Video Safe Areas Afficher les zones de sécurité vidéo @@ -4274,6 +4275,7 @@ axis Both Les deux @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis doit être "vertical", "horizontal" ou un angle en degrés, pas `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" arrière-plan {other} : transparent, white, black ou une couleur comme "#ff8800" @msg bad color {v} couleur non valide {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` est null, le nom d’une couleur de calque ou #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio doit être un nombre de 0,1 à 10 @msg the Color Picker is not open le Sélecteur de couleurs n’est pas ouvert @msg the Simplify slider is not open le curseur Simplifier n’est pas ouvert diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index a88e27f5b..3fa226908 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -497,6 +497,7 @@ antiAlias Sharp शार्प B5 B5 Back वापस Background Color: बैकग्राउंड रंग: + Guide Label: गाइड लेबल: Background Contents बैकग्राउंड सामग्री Background Level बैकग्राउंड स्तर Background: बैकग्राउंड: @@ -6040,6 +6041,7 @@ antiAlias Sharp शार्प @msg bad base64 बुरा आधार64 @msg bad color {c} बुरा रंग {c} @msg bad color {v} बुरा रंग {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` null, किसी लेयर रंग का नाम या #rrggbb होता है @msg bad colour बुरा रंग @msg bad colour {v} बुरा रंग {v} @msg bad colour {v} in groupColors groupColors में अमान्य रंग {v} diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 7e079ad65..27e22d0ad 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -385,6 +385,7 @@ font-technology Variable Változó B4 B4 B5 B5 Background Color: Háttérszín: + Guide Label: Segédvonalak címkéje: Show Center Mark Középjel megjelenítése Show Cross Hairs Szálkereszt megjelenítése Show Video Safe Areas Biztonságos videoterületek megjelenítése @@ -4833,6 +4834,7 @@ imageTrace Outline Körvonal @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` az axis értéke "vertical", "horizontal" vagy fokban megadott szög lehet, ez nem az: `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparent, white, black vagy egy szín, például "#ff8800" @msg bad color {v} hibás szín: {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb a `guideLabel` értéke null, egy rétegszín neve vagy #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio: 0,1 és 10 közötti szám kell @msg the Color Picker is not open a Színválasztó nincs megnyitva @msg the Simplify slider is not open az Egyszerűsítés csúszka nincs megnyitva diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 471a04061..c4e97482a 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -2442,6 +2442,7 @@ digits Malayalam Malayalam File Dimensions Dimensi file Transforms Transformasi Background Color: Warna latar belakang: + Guide Label: Label panduan: Show Center Mark Tampilkan tanda tengah Show Cross Hairs Tampilkan garis silang Show Video Safe Areas Tampilkan area aman video diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 7acfe6fbd..987505a43 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -289,6 +289,7 @@ font-technology Variable Variabili Average… Media… Axis: Asse: Background Color: Colore di sfondo: + Guide Label: Etichetta delle guide: Show Center Mark Mostra segno centrale Show Cross Hairs Mostra mirino Show Video Safe Areas Mostra aree sicure video @@ -4328,6 +4329,7 @@ axis Both Entrambi @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis deve essere "vertical", "horizontal" o un angolo in gradi, non `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" sfondo {other}: transparent, white, black o un colore come "#ff8800" @msg bad color {v} colore non valido {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` è null, il nome di un colore di livello o #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio deve essere un numero da 0,1 a 10 @msg the Color Picker is not open il Selettore colore non è aperto @msg the Simplify slider is not open il cursore Semplifica non è aperto diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 9213bed27..d3c0b6603 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1077,6 +1077,7 @@ antiAlias Sharp シャープ Average 平均 BLANK DOCUMENT PRESETS 空白のドキュメントプリセット Background Color: 背景色: + Guide Label: ガイドのラベル: Show Center Mark 中心マークを表示 Show Cross Hairs 十字線を表示 Show Video Safe Areas ビデオセーフエリアを表示 @@ -4684,6 +4685,7 @@ swatch Bright Violet ビビッドバイオレット @msg {label}… {label}… @msg no blend options ブレンドオプションがありません @msg bad color {v} カラー {v} が正しくありません +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` は null、レイヤーカラーの名前、または #rrggbb です @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio には 0.1 〜 10 の数値を指定してください @msg the Color Picker is not open カラーピッカーが開いていません @msg the Simplify slider is not open 単純化スライダーが開いていません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 80a26e2e6..5a05ccec3 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -2343,6 +2343,7 @@ digits Malayalam მალაიალამური File Dimensions ფაილის ზომები Transforms გარდაქმნები Background Color: ფონის ფერი: + Guide Label: დამხმარე ხაზების იარლიყი: Show Center Mark ცენტრის ნიშნის ჩვენება Show Cross Hairs ჯვარედინი ხაზების ჩვენება Show Video Safe Areas ვიდეოს უსაფრთხო არეების ჩვენება @@ -4042,6 +4043,7 @@ font-technology Variable ცვლადი @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis უნდა იყოს "vertical", "horizontal" ან კუთხე გრადუსებში და არა `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" ფონი {other}: transparent, white, black ან ფერი, მაგალითად "#ff8800" @msg bad color {v} არასწორი ფერი {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` არის null, ფენის ფერის სახელი ან #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio უნდა იყოს რიცხვი 0,1-დან 10-მდე @msg bad colour `{hex}` (#rrggbb) არასწორი ფერი `{hex}` (#rrggbb) @msg bad page tree გვერდების არასწორი ხე diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index b82b189d5..b0fc26d1f 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1067,6 +1067,7 @@ antiAlias Sharp 날카롭게 Average 평균 BLANK DOCUMENT PRESETS 빈 문서 프리셋 Background Color: 배경색: + Guide Label: 안내선 레이블: Show Center Mark 중심 표시 보기 Show Cross Hairs 십자선 보기 Show Video Safe Areas 비디오 안전 영역 보기 @@ -4611,6 +4612,7 @@ theme Appearance 외관 @msg {label}… {label}… @msg no blend options 혼합 옵션이 없습니다 @msg bad color {v} 잘못된 색상 {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel`은 null, 레이어 색상 이름 또는 #rrggbb입니다 @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio은(는) 0.1에서 10 사이의 숫자여야 합니다 @msg the Color Picker is not open 색상 피커가 열려 있지 않습니다 @msg the Simplify slider is not open 단순화 슬라이더가 열려 있지 않습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 537adfb1c..c30a50e71 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2456,6 +2456,7 @@ digits Malayalam Malayalam File Dimensions Bestandsafmetingen Transforms Transformaties Background Color: Achtergrondkleur: + Guide Label: Hulplijnlabel: Show Center Mark Middenmarkering tonen Show Cross Hairs Dradenkruis tonen Show Video Safe Areas Veilige videogebieden tonen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index b77f8912b..8450cb654 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2567,6 +2567,7 @@ digits Malayalam Malajalam File Dimensions Wymiary pliku Transforms Przekształcenia Background Color: Kolor tła: + Guide Label: Etykieta linii pomocniczych: Show Center Mark Pokaż znacznik środka Show Cross Hairs Pokaż krzyż nitkowy Show Video Safe Areas Pokaż bezpieczne obszary wideo @@ -4417,6 +4418,7 @@ imageTrace Outline Kontur @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis musi mieć wartość "vertical", "horizontal" lub kąt w stopniach, nie `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" tło {other}: transparent, white, black lub kolor, np. "#ff8800" @msg bad color {v} nieprawidłowy kolor {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` to null, nazwa koloru warstwy lub #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio musi być liczbą od 0,1 do 10 @msg the Color Picker is not open Próbnik kolorów nie jest otwarty @msg the Simplify slider is not open Suwak Uprość nie jest otwarty diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8b3adb806..00d9a8948 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -275,6 +275,7 @@ antiAlias Sharp Nítido Average… Média… BLANK DOCUMENT PRESETS PREDEFINIDOS DE DOCUMENTO EM BRANCO Background Color: Cor do Plano de Fundo: + Guide Label: Rótulo das Guias: Show Center Mark Mostrar Marca Central Show Cross Hairs Mostrar Linhas Cruzadas Show Video Safe Areas Mostrar Áreas Seguras de Vídeo @@ -3841,6 +3842,7 @@ axis Both Ambos @msg at this scale the drawing is {w} × {h} pt, more than the canvas holds: choose a smaller scale or fit it to the artboard nesta escala o desenho tem {w} × {h} pt, mais do que a tela comporta: escolha uma escala menor ou ajuste-o à prancheta @msg background {other}: transparent, white, black or a colour such as "#ff8800" plano de fundo {other}: transparent, white, black ou uma cor como "#ff8800" @msg bad color {v} cor inválida {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` é null, o nome de uma cor de camada ou #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio deve ser um número de 0,1 a 10 @msg bad colour `{hex}` (#rrggbb) cor inválida `{hex}` (#rrggbb) @msg bad page tree árvore de páginas inválida diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 7d734a370..24a6c74d5 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -287,6 +287,7 @@ font-technology Variable Вариативные Average… Усреднить… Axis: Ось: Background Color: Цвет фона: + Guide Label: Метка направляющих: Show Center Mark Показать центральную метку Show Cross Hairs Показать перекрестие Show Video Safe Areas Показать безопасные зоны видео @@ -4322,6 +4323,7 @@ axis Both Оба @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis должно быть "vertical", "horizontal" или углом в градусах, а не `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" фон {other}: transparent, white, black или цвет, например "#ff8800" @msg bad color {v} недопустимый цвет {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` должен быть null, именем цвета слоя или #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio должно быть числом от 0,1 до 10 @msg the Color Picker is not open палитра цветов не открыта @msg the Simplify slider is not open ползунок «Упростить» не открыт diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 0661638ac..c038612c1 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -278,6 +278,7 @@ font-technology Variable Variabla Average… Medelvärde… Axis: Axel: Background Color: Bakgrundsfärg: + Guide Label: Stödlinjeetikett: Show Center Mark Visa mittmarkering Show Cross Hairs Visa hårkors Show Video Safe Areas Visa videosäkra områden @@ -4194,6 +4195,7 @@ axis Both Båda @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis måste vara "vertical", "horizontal" eller en vinkel i grader, inte `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparent, white, black eller en färg som "#ff8800" @msg bad color {v} ogiltig färg {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` är null, namnet på en lagerfärg eller #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio måste vara ett tal från 0,1 till 10 @msg bad colour `{hex}` (#rrggbb) ogiltig färg `{hex}` (#rrggbb) @msg bad page tree ogiltigt sidträd diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 8f003df9d..282990771 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -274,6 +274,7 @@ font-technology Variable Değişken Average… Ortalama… Axis: Eksen: Background Color: Arka Plan Rengi: + Guide Label: Kılavuz Etiketi: Show Center Mark Merkez İşaretini Göster Show Cross Hairs Artı Çizgilerini Göster Show Video Safe Areas Video Güvenli Alanlarını Göster @@ -4050,6 +4051,7 @@ axis Both Her İkisi @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis "vertical", "horizontal" veya derece cinsinden bir açı olmalıdır, `{v}` değil @msg background {other}: transparent, white, black or a colour such as "#ff8800" arka plan {other}: şeffaf, beyaz, siyah veya "#ff8800" gibi bir renk @msg bad color {v} kötü renk {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` null, bir katman renginin adı veya #rrggbb olur @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio 0,1 ile 10 arasında bir sayı olmalıdır @msg the Color Picker is not open Renk Seçici açık değil @msg the screen color sampler is already open ekran rengi örnekleyici zaten açık diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7a000c4de..88bcb4702 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -283,6 +283,7 @@ font-technology Variable Варіативні Average… Усереднити… Axis: Вісь: Background Color: Колір тла: + Guide Label: Мітка напрямних: Show Center Mark Показати центральну позначку Show Cross Hairs Показати перехрестя Show Video Safe Areas Показати безпечні зони відео @@ -4312,6 +4313,7 @@ axis Both Обидві @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis має бути "vertical", "horizontal" або кутом у градусах, а не `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" фон {other}: transparent, white, black або колір, наприклад "#ff8800" @msg bad color {v} неприпустимий колір {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` має бути null, назвою кольору шару або #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio має бути числом від 0,1 до 10 @msg the Color Picker is not open палітра кольорів не відкрита @msg the Simplify slider is not open повзунок «Спростити» не відкрито diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 785509f78..90c292bc9 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -312,6 +312,7 @@ font-technology Variable Biến thiên Axis: Trục: Back Sau Background Color: Màu nền: + Guide Label: Nhãn đường căn: Show Center Mark Hiện dấu tâm Show Cross Hairs Hiện đường chữ thập Show Video Safe Areas Hiện vùng an toàn video @@ -4482,6 +4483,7 @@ theme Appearance Giao diện @msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` axis phải là "vertical", "horizontal" hoặc góc theo độ, không phải `{v}` @msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparent, white, black hoặc màu như "#ff8800" @msg bad color {v} màu không hợp lệ {v} +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` là null, tên một màu lớp hoặc #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio phải là một số từ 0,1 đến 10 @msg the Color Picker is not open Bộ chọn màu chưa được mở @msg the Simplify slider is not open Thanh trượt Đơn giản hóa chưa được mở diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 7efe77392..77578eec1 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -235,6 +235,7 @@ antiAlias Sharp 锐利 Average… 平均… Axis: 轴: Background Color: 背景颜色: + Guide Label: 参考线标签: Show Center Mark 显示中心标记 Show Cross Hairs 显示十字线 Show Video Safe Areas 显示视频安全区域 @@ -4396,6 +4397,7 @@ imageTrace Outline 轮廓 @msg {label}… {label}… @msg no blend options 没有混合选项 @msg bad color {v} 颜色 {v} 无效 +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` 应为 null、图层颜色名称或 #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio 必须是 0.1 到 10 之间的数字 @msg the Color Picker is not open 拾色器未打开 @msg the Simplify slider is not open 简化滑块未打开 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 26eb0a512..bdec63590 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -248,6 +248,7 @@ font-technology Variable 可變 Average… 平均… Axis: 軸: Background Color: 背景顏色: + Guide Label: 參考線標籤: Show Center Mark 顯示中心標記 Show Cross Hairs 顯示十字線 Show Video Safe Areas 顯示視訊安全區域 @@ -2224,6 +2225,7 @@ file-filter Templates 範本 @msg {label}… {label}… @msg no blend options 沒有漸變選項 @msg bad color {v} 顏色 {v} 無效 +@msg `guideLabel` is null, a layer color's name or #rrggbb `guideLabel` 應為 null、圖層顏色名稱或 #rrggbb @msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio 必須是 0.1 到 10 之間的數字 @msg the Color Picker is not open 「顏色選擇器」未開啟 @msg the Simplify slider is not open 「簡化」滑桿未開啟 diff --git a/docs/mcp.md b/docs/mcp.md index ad982f8cb..1894c11a4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4828,7 +4828,7 @@ value: true}`; the Control bar's and Properties' Scale Artwork with Artboard che Artboard) makes its handle drags proportional and passes `scaleArt`, as do Properties' and the Transform panel's artboard size fields while the tool is in use. -`artboard.setProps {artboards: [{index, name?, color?, locked?, showCenterMark?, showCrossHairs?, showVideoSafeAreas?, videoRulerPixelAspectRatio?, x?, y?, width?, height?}…], scaleArt?, moveArt?, …}` sets several +`artboard.setProps {artboards: [{index, name?, color?, guideLabel?, locked?, showCenterMark?, showCrossHairs?, showVideoSafeAreas?, videoRulerPixelAspectRatio?, x?, y?, width?, height?}…], scaleArt?, moveArt?, …}` sets several artboards in one undo step, each with its own fields; the other params apply to all of them. The art that moves or scales with each artboard is the art inside it before any of them changes, and an object inside several of them goes with the first one listed that moves or scales its art. An empty list and a list that names an artboard twice are @@ -4962,6 +4962,20 @@ is the width of one video pixel over its height, which `view.videoRulers` counts the native format keep both (written only when on or not 1), and `inspect_document` adds `showVideoSafeAreas: true` and `videoRulerPixelAspectRatio` to an artboard that has them. +Artboard Options' Guide Label: `artboard.setProps {index, guideLabel?: name | "#rrggbb" | null}` (or each entry of +`artboards`) gives the artboard a guide label, and `artboard.new` takes one too. The canvas draws the artboard's own +guides (`guide.add {artboard}`) in the label's color instead of the guide color preference, in the preference's style; +canvas guides, guides of an artboard without a label and guides whose artboard is gone keep the preference's color, and +selected guides are still drawn in the selection color. The value is a layer color's name, spelled exactly as Layer +Options lists it (`"Light Blue"`, `"Red"`, …), or `#rrggbb`; null clears it, and new artboards and older files have none. +The label is VectorCraft's own (no public documentation describes one): it is screen-only, so exports and print leave +it out, the paper and the artboard's background color stay as they are, and a locked artboard's label can change. +Anything else (a preset's index, another letter case, `#rrggbbaa`) is refused with "`guideLabel` is null, a layer +color's name or #rrggbb", and a list with a bad value anywhere changes nothing. Duplicates and copies keep it, the +native format writes it only when set (a preset index past the layer colors opens as none), and `inspect_document` adds +`guideLabel` (the name or `#rrggbb`) to an artboard that has one. `artboard.setProps` adds no undo step when nothing it +is given differs from the artboard. + Artboard Options' Global checkboxes are application preferences, not artboard properties: `fadeRegionOutsideArtboard` (Fade Region Outside Artboard) and `artboardUpdateWhileDragging` (Update While Dragging), both on by default (`prefs.set {values: {fadeRegionOutsideArtboard: false}}`; also Edit › Preferences › General › Artboard Options). With diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8184b0fe2..30743351e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -651,6 +651,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Rulers (#1212):** drag the intersection to preview and set a document zero point, or double-click it to reset to the active artboard's top-left. Transform, Properties and Control X/Y fields measure from it and convert entered positions back to absolute command coordinates. The origin survives every native format version and compression; geometry, guides and patterns stay in place. Public reference: [Use rulers](https://helpx.adobe.com/ca/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html). - **Global and artboard rulers:** View › Rulers › Change to Global / Artboard Rulers (Cmd+Alt+R, `view.globalRulers`). A new document has artboard rulers: the rulers and the Transform, Properties, Control and Info X/Y count from the active artboard's top-left corner, or the zero point dragged out for that artboard, which moves with it; with global rulers one document zero point serves every artboard. Guide readouts and Shift ruler-tick snapping follow the same zero point. The mode is saved with the document; files from before it open with global rulers, as they measured. Regression tests: `engine/tests/it/ruler_origin.rs`, `ui-egui/src/tests/ruler_origin.rs`, `tools/src/rulerguide.rs`. Public reference: [About rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/about-rulers.html). - **Video display options (#2849):** Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio, and View › Rulers › Show Video Rulers ([Set video display options for artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html)). Each artboard has its own (`artboard.setProps`/`artboard.new` `showVideoSafeAreas`, `videoRulerPixelAspectRatio` 0.1–10, default 1), set in one undo step and written to the native format only when on or not 1, so other documents save byte for byte as before. The help page names the safe areas without numbers; our choice is [EBU R 95](https://tech.ebu.ch/docs/r/r095.pdf)'s (note 5): the canvas outlines the action-safe area 3.5% and the title-safe (graphics-safe) area 5% in from each edge, 1 point wide and turned with the view; exports and print leave them out. Show Video Rulers (`view.videoRulers`) shows the rulers counting the active artboard in pixels from its top-left corner, whatever the ruler origin: a pixel is as many points wide as the ratio and one point tall, so a 720 × 480 pt artboard at 0.9 reads 800 × 480. Hide Video Rulers returns them to the General unit; Shift ruler-tick snapping of new guides keeps the General unit. Regression tests: `tests/panelcmds.rs` (`artboard_video_display_options_*`, `artboard_display_marks_stay_out_of_exports`), `tests/docsetup.rs` (`an_artboard_shows_its_video_safe_areas`), `canvas.rs` (`video_rulers_count_the_artboard_in_pixels_of_its_aspect_ratio`), `tests/ruler_origin.rs` (`show_video_rulers_counts_the_active_artboard_in_pixels`), `dialogs/tests.rs` (`artboard_options_show_video_display_options`). +- **Artboard guide labels (#6414, in part):** Artboard Options' Guide Label gives an artboard a color label (`artboard.setProps`/`artboard.new` `guideLabel`: a layer color's name, `#rrggbb` or null), and the canvas draws that artboard's own guides in it; canvas guides, guides of an unlabeled artboard and guides whose artboard is gone keep the guide color preference, and selected guides keep the selection color. The [help page](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/align-graphic-objects-with-guides.html) describes guides restricted to an artboard, not a label, so the label, its palette and the color precedence are VectorCraft's own. It is screen-only (exports and print leave it out), written to the native format only when set, kept by duplicates and copies, and changeable on a locked artboard; `artboard.setProps` adds no undo step when nothing changes. This VectorCraft addition does not raise a score. Regression tests: `tests/panelcmds.rs` (`artboard_guide_labels_are_set_and_cleared_in_one_undo_step`, `artboard_guide_labels_round_trip_through_the_native_format`), `doc` `tests.rs` (`guide_label_text_names_a_layer_color_or_rgb`, `artboard_guide_label_defaults_and_skips_in_json`), `canvas::tests` (`artboard_guide_label_colors_only_its_own_guides`), `dialogs/tests.rs` (`artboard_options_shows_sets_and_clears_the_guide_label`). - **Fade Region Outside Artboard:** Artboard Options' Global checkboxes Fade Region Outside Artboard and Update While Dragging ([user guide, Artboard options](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf)) are application preferences (`fadeRegionOutsideArtboard`, `artboardUpdateWhileDragging`), both on by default. While the Artboard tool is active the canvas draws everything outside the artboards, the pasteboard and the art on it, with each color channel at 60% of its value (VectorCraft's own amount), or only outside the box around all the artboards when crossing artboards would leave more than 4,096 rectangles in view; with Update While Dragging off it draws as usual during an artboard move or resize. Presentation Mode, exports and print do not change. Regression tests: `canvas::tests` (`the_artboard_tool_fades_the_pasteboard`, `presentation_mode_draws_no_fade`, `the_fade_darkens_art_on_the_pasteboard_and_not_on_the_artboard`, `outside_rects_cover_the_bounds_minus_the_boards`, `outside_rects_of_a_row_of_boards_stay_few`, `the_fade_of_crossing_boards_stays_bounded`, `update_while_dragging_keeps_or_drops_the_fade_during_an_artboard_drag`, `the_pasteboard_fade_leaves_exports_unchanged`), `dialogs/tests.rs` (`artboard_options_global_checkboxes_set_the_preferences`, `artboard_options_error_leaves_the_global_preferences_unchanged`). - **Saved views (G7):** View › New View… and Edit Views… ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html): up to 25 views per document, saved with it, renamed or deleted in Edit Views, chosen from the bottom of the View menu). Each view keeps the zoom, centre and rotation; the View menu lists all 25 (`view.goto1`–`view.goto25`), and Edit Views… lists them to rename or delete, applied together as one undo step. A file's views are checked when it opens (at most 25, unique names up to 255 characters, a finite centre and a positive zoom). Tests: `saved_views_round_trip_and_from_a_file_are_tidied` (`crates/format/tests/prop_format.rs`), `crates/engine/src/cmd/views.rs`, `crates/ui-egui/src/dialogs/edit_views.rs` and `every_saved_view_is_listed_at_the_bottom_of_the_view_menu` (`crates/ui-egui/src/menus.rs`). - **Window › Arrange › Tile (G7):** Tile (`window.arrange.tile`) shows every open document at once, edge to edge in the document area ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)), one pane each in a grid of as many columns as the square root of the number of documents, rounded up, each fitted to its pane. The current document's pane is the canvas; the other panes draw their documents with their artboards, guides and selected objects, and a press in one makes its document the current one. Consolidate All Windows (`window.arrange.consolidateAllWindows`) shows one document under its tab again. The grid and when the two items are enabled (Tile with two documents or more, Consolidate All Windows while they are tiled) are VectorCraft's own. Tests: `crates/ui-egui/src/canvas/panes/tests.rs`.