Artboards: Artboard Options shows video safe areas and sets the pixel aspect ratio that View › Rulers › Show Video Rulers counts the artboard in

Squashed from #3831 onto main.
This commit is contained in:
bflatastic
2026-10-24 03:09:07 +07:00
parent 6134653951
commit 422ecc0c77
53 changed files with 779 additions and 82 deletions
+52
View File
@@ -456,6 +456,18 @@ pub struct Artboard {
/// be set.
#[serde(default)]
pub show_cross_hairs: bool,
/// Show Video Safe Areas (Artboard Options): the canvas draws the artboard's action-safe and
/// title-safe areas ([`Artboard::video_safe_areas`]). Exports and print leave them out. Written
/// only when on, so a document that never shows them saves as before.
#[serde(default, skip_serializing_if = "skip::is_default")]
pub show_video_safe_areas: bool,
/// Video Ruler Pixel Aspect Ratio (Artboard Options): the width of one video pixel over its
/// height, which View › Rulers › Show Video Rulers counts the artboard in: a pixel is this many
/// points wide and one point tall. 1 (square pixels) unless set, and written only when it isn't
/// 1; always within [`VIDEO_PIXEL_ASPECT_RANGE`]: the commands refuse others, and a file's is
/// brought into it as it opens.
#[serde(default = "square_pixels", deserialize_with = "pixel_aspect_within_range", skip_serializing_if = "is_square_pixels")]
pub video_ruler_pixel_aspect_ratio: f64,
/// Background color (Properties › Artboards › Fill, Artboard Options). The canvas draws the
/// artboard's paper in it, multiplied with the Simulate Colored Paper color while that is on;
/// without one the paper is white, or the Simulate Colored Paper color. Exports and print leave
@@ -464,6 +476,42 @@ pub struct Artboard {
pub color: Option<vectorcraft_color::Color>,
}
/// The Video Ruler Pixel Aspect Ratios an artboard takes (Artboard Options): from a pixel ten times
/// as tall as it is wide to one ten times as wide as it is tall, which holds the anamorphic and
/// standard-definition ratios video uses (0.9, 1.2, 1.33, 2) with room to spare.
pub const VIDEO_PIXEL_ASPECT_RANGE: std::ops::RangeInclusive<f64> = 0.1..=10.0;
/// How far in from each edge of an artboard the video safe areas lie, as a share of its width
/// (left and right) and of its height (top and bottom): the action-safe area 3.5% and the
/// title-safe (graphics-safe) area 5%, as EBU R 95 sets them for 16:9 television
/// (https://tech.ebu.ch/docs/r/r095.pdf, note 5).
pub const VIDEO_SAFE_INSETS: [f64; 2] = [0.035, 0.05];
fn square_pixels() -> f64 {
1.0
}
fn is_square_pixels(v: &f64) -> bool {
*v == 1.0
}
/// A file's Video Ruler Pixel Aspect Ratio, brought into [`VIDEO_PIXEL_ASPECT_RANGE`]: one that
/// isn't a number (NaN, infinite) reads as square pixels.
fn pixel_aspect_within_range<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
let v = f64::deserialize(d)?;
let (lo, hi) = (*VIDEO_PIXEL_ASPECT_RANGE.start(), *VIDEO_PIXEL_ASPECT_RANGE.end());
Ok(if v.is_finite() { v.clamp(lo, hi) } else { 1.0 })
}
impl Artboard {
/// The action-safe and the title-safe area of the artboard, in that order, each inset from its
/// edges by the share of [`VIDEO_SAFE_INSETS`] (Show Video Safe Areas).
pub fn video_safe_areas(&self) -> [Rect; 2] {
let r = self.rect;
VIDEO_SAFE_INSETS.map(|k| r.inflate(-k * r.width(), -k * r.height()))
}
}
/// A document's locked artboards ([`Document::locked_artboards`]).
pub struct LockedArtboards<'a> {
rects: Vec<Rect>,
@@ -1131,6 +1179,8 @@ impl Document {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
}],
layers: vec![],
@@ -2276,6 +2326,8 @@ mod tests {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
let rect = |d: &mut Document, r: Rect, appearance: Appearance| {
+2
View File
@@ -243,6 +243,8 @@ impl Builder {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
})
.collect();
+11 -1
View File
@@ -340,7 +340,17 @@ 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, locked: false, show_center_mark: false, show_cross_hairs: false, color: None }];
d.artboards = vec![Artboard {
id: 1,
name: name.into(),
rect,
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
}];
d
}
+45 -8
View File
@@ -125,7 +125,7 @@ pub fn specs() -> Vec<CommandSpec> {
"New Artboard",
["Window", "Artboards"],
None,
"{x?, y?, width?, height?, name?, color?: color (its background color, as artboard.setProps takes it; default: none), showCenterMark?: bool, showCrossHairs?: bool (as artboard.setProps takes them; default false)} (default place: right of the last)",
"{x?, y?, width?, height?, name?, color?: color (its background color, 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)} (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?, color?, locked?, showCenterMark?, showCrossHairs?, 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), 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; both are screen-only, as the artboard's edges are: exports and print leave them out), 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?, 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: 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}",
has_doc,
artboard_set
),
@@ -813,19 +813,44 @@ fn artboard_color(p: &Value, cmd: &str) -> Result<Option<Option<vectorcraft_colo
}
}
/// An artboard command's `videoRulerPixelAspectRatio` param: None when absent, else a number within
/// [`vectorcraft_doc::VIDEO_PIXEL_ASPECT_RANGE`].
fn video_pixel_aspect(p: &Value, cmd: &str) -> Result<Option<f64>> {
match p.get("videoRulerPixelAspectRatio") {
None => Ok(None),
Some(v) => v
.as_f64()
.filter(|r| vectorcraft_doc::VIDEO_PIXEL_ASPECT_RANGE.contains(r))
.map(Some)
.ok_or_else(|| bad(cmd, "videoRulerPixelAspectRatio must be a number from 0.1 to 10")),
}
}
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();
check_switches(p, &["showCenterMark", "showCrossHairs"], "artboard.new")?;
check_switches(p, &["showCenterMark", "showCrossHairs", "showVideoSafeAreas"], "artboard.new")?;
let (show_center_mark, show_cross_hairs) = (bool_or(p, "showCenterMark", false), bool_or(p, "showCrossHairs", false));
let show_video_safe_areas = bool_or(p, "showVideoSafeAreas", false);
let video_ruler_pixel_aspect_ratio = video_pixel_aspect(p, "artboard.new")?.unwrap_or(1.0);
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, locked: false, show_center_mark, show_cross_hairs, color });
d.artboards.push(Artboard {
id,
name,
rect: r,
locked: false,
show_center_mark,
show_cross_hairs,
show_video_safe_areas,
video_ruler_pixel_aspect_ratio,
color,
});
Ok(d.artboards.len() - 1)
})?;
Ok(json!({ "index": idx }))
@@ -848,9 +873,10 @@ fn artboard_delete(s: &mut Session, p: &Value) -> Result<Value> {
ok()
}
/// One artboard's part of `artboard.setProps`: its index, new rectangle, name, color, lock and
/// display marks (Show Center Mark, Show Cross Hairs), how far it moved, whether it was resized,
/// the map its art and guides take when they go with it (`fit` when it scales them), and that art.
/// One artboard's part of `artboard.setProps`: its index, new rectangle, name, color, lock,
/// display marks (Show Center Mark, Show Cross Hairs, Show Video Safe Areas) and Video Ruler Pixel
/// Aspect Ratio, how far it moved, whether it was resized, the map its art and guides take when
/// they go with it (`fit` when it scales them), and that art.
struct ArtboardEdit<'a> {
index: usize,
now: Rect,
@@ -859,6 +885,8 @@ struct ArtboardEdit<'a> {
lock: Option<bool>,
center_mark: Option<bool>,
cross_hairs: Option<bool>,
safe_areas: Option<bool>,
pixel_aspect: Option<f64>,
delta: vectorcraft_geom::Vec2,
resized: bool,
fit: Option<Affine>,
@@ -876,7 +904,7 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
};
check_switches(p, &["moveArt", "scaleArt", "strokes", "corners", "patterns", "lockedAndHidden"], "artboard.setProps")?;
for q in &entries {
check_switches(q, &["locked", "showCenterMark", "showCrossHairs"], "artboard.setProps")?;
check_switches(q, &["locked", "showCenterMark", "showCrossHairs", "showVideoSafeAreas"], "artboard.setProps")?;
check_numbers(q, &["x", "y", "width", "height"], "artboard.setProps")?;
}
// Scale Artwork with Artboard: resized, an artboard takes the art fully inside it (as Move
@@ -900,6 +928,7 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
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));
let (safe_areas, pixel_aspect) = (q.get("showVideoSafeAreas").and_then(Value::as_bool), video_pixel_aspect(q, "artboard.setProps")?);
// A locked artboard keeps its rectangle unless this unlocks it; its name can change.
let reshaped = [now.x0 - was.x0, now.y0 - was.y0, now.x1 - was.x1, now.y1 - was.y1].iter().any(|d| d.abs() > 1e-6);
if reshaped && lock != Some(false) {
@@ -925,6 +954,8 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
lock,
center_mark,
cross_hairs,
safe_areas,
pixel_aspect,
delta,
resized,
fit,
@@ -961,6 +992,12 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(c) = e.cross_hairs {
a.show_cross_hairs = c;
}
if let Some(v) = e.safe_areas {
a.show_video_safe_areas = v;
}
if let Some(r) = e.pixel_aspect {
a.video_ruler_pixel_aspect_ratio = r;
}
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() };
+2
View File
@@ -975,6 +975,8 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result<Value> {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
converted.push(id);
+2
View File
@@ -341,6 +341,8 @@ impl DocSettings {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
})
.collect();
+11 -3
View File
@@ -129,18 +129,26 @@ pub fn document(s: &Session) -> Value {
}
/// An artboard's name and rectangle, its background color (`#rrggbb`) when it has one,
/// `locked: true` when it is locked, and `showCenterMark: true` and `showCrossHairs: true` when
/// it shows them.
/// `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 {
let mut v = json!({"name": a.name, "x": a.rect.x0, "y": a.rect.y0, "width": a.rect.width(), "height": a.rect.height()});
if let Some(c) = a.color {
v["color"] = json!(c.to_hex());
}
for (key, on) in [("locked", a.locked), ("showCenterMark", a.show_center_mark), ("showCrossHairs", a.show_cross_hairs)] {
for (key, on) in [
("locked", a.locked),
("showCenterMark", a.show_center_mark),
("showCrossHairs", a.show_cross_hairs),
("showVideoSafeAreas", a.show_video_safe_areas),
] {
if on {
v[key] = json!(true);
}
}
if a.video_ruler_pixel_aspect_ratio != 1.0 {
v["videoRulerPixelAspectRatio"] = json!(a.video_ruler_pixel_aspect_ratio);
}
v
}
+129 -3
View File
@@ -916,21 +916,147 @@ fn artboard_display_marks_round_trip_through_the_native_format() {
assert!(back.artboards.iter().all(|a| !a.show_center_mark && !a.show_cross_hairs));
}
/// Show Center Mark and Show Cross Hairs are screen-only: SVG and PDF exports (and print, which
/// prints the PDF) come out the same with them on as with them off.
/// Show Center Mark, Show Cross Hairs, Show Video Safe Areas and the Video Ruler Pixel Aspect Ratio
/// are screen-only: SVG and PDF exports (and print, which prints the PDF) come out the same with
/// them on as with them off.
#[test]
fn artboard_display_marks_stay_out_of_exports() {
let mut s = session();
rect(&mut s);
let doc = |s: &Session| (*s.doc().unwrap().doc).clone();
let off = doc(&s);
s.execute("artboard.setProps", &json!({"index": 0, "showCenterMark": true, "showCrossHairs": true})).unwrap();
s.execute(
"artboard.setProps",
&json!({"index": 0, "showCenterMark": true, "showCrossHairs": true, "showVideoSafeAreas": true, "videoRulerPixelAspectRatio": 2}),
)
.unwrap();
let on = doc(&s);
assert_ne!(on.artboards, off.artboards);
assert_eq!(vectorcraft_svg::export(&on, &Default::default()), vectorcraft_svg::export(&off, &Default::default()));
assert_eq!(crate::export_pdf(&on, &Default::default()).unwrap(), crate::export_pdf(&off, &Default::default()).unwrap());
}
/// Artboard Options' video display options: `artboard.setProps {showVideoSafeAreas,
/// videoRulerPixelAspectRatio}` sets each on that artboard only, in one undo step, also for a
/// locked artboard and in an `artboards` list. A change without them leaves them, a duplicate
/// keeps them, `artboard.new` takes them, `inspect_document` reports them, and a value that isn't
/// true or false, or a ratio outside 0.1–10, is refused and changes nothing.
#[test]
fn artboard_video_display_options_are_set_in_one_undo_step() {
let mut s = session();
s.execute("artboard.new", &json!({})).unwrap();
let video = |s: &Session, i: usize| {
let a = &s.doc().unwrap().doc.artboards[i];
(a.show_video_safe_areas, a.video_ruler_pixel_aspect_ratio)
};
let undo = |s: &Session| s.doc().unwrap().history.undo.len();
assert_eq!((video(&s, 0), video(&s, 1)), ((false, 1.0), (false, 1.0)), "off and square by default");
let u0 = undo(&s);
s.execute("artboard.setProps", &json!({"index": 1, "showVideoSafeAreas": true})).unwrap();
assert_eq!((video(&s, 0), video(&s, 1)), ((false, 1.0), (true, 1.0)), "only that artboard");
s.execute("artboard.setProps", &json!({"index": 1, "videoRulerPixelAspectRatio": 0.9})).unwrap();
assert_eq!(video(&s, 1), (true, 0.9));
s.execute("artboard.setProps", &json!({"index": 1, "name": "Video"})).unwrap();
assert_eq!(video(&s, 1), (true, 0.9), "a change without them leaves them");
assert_eq!(undo(&s), u0 + 3, "one undo step each");
for was in [(true, 0.9), (true, 1.0), (false, 1.0)] {
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(video(&s, 1), was);
}
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(video(&s, 1), (true, 1.0));
// A locked artboard keeps its rectangle, not its video display options.
s.execute("artboard.setProps", &json!({"index": 0, "locked": true})).unwrap();
s.execute("artboard.setProps", &json!({"index": 0, "videoRulerPixelAspectRatio": 2})).unwrap();
assert_eq!(video(&s, 0), (false, 2.0));
let u = undo(&s);
s.execute(
"artboard.setProps",
&json!({"artboards": [{"index": 0, "showVideoSafeAreas": true}, {"index": 1, "showVideoSafeAreas": false, "videoRulerPixelAspectRatio": 1.2}]}),
)
.unwrap();
assert_eq!((video(&s, 0), video(&s, 1)), ((true, 2.0), (false, 1.2)), "each listed artboard takes its own");
assert_eq!(undo(&s), u + 1, "in one undo step");
let doc = crate::inspect::document(&s);
assert_eq!(doc["artboards"][0]["showVideoSafeAreas"], json!(true));
assert_eq!(
(doc["artboards"][0]["videoRulerPixelAspectRatio"].clone(), doc["artboards"][1]["videoRulerPixelAspectRatio"].clone()),
(json!(2.0), json!(1.2))
);
assert!(doc["artboards"][1].get("showVideoSafeAreas").is_none(), "{doc}");
s.execute("artboard.setProps", &json!({"index": 1, "videoRulerPixelAspectRatio": 1})).unwrap();
assert!(crate::inspect::document(&s)["artboards"][1].get("videoRulerPixelAspectRatio").is_none(), "square pixels aren't reported");
let copy = s.execute("artboard.duplicate", &json!({"index": 0})).unwrap()["index"].as_u64().unwrap() as usize;
assert_eq!(video(&s, copy), (true, 2.0), "a duplicate keeps them");
let made = s.execute("artboard.new", &json!({"showVideoSafeAreas": true, "videoRulerPixelAspectRatio": 0.1})).unwrap()["index"].as_u64().unwrap()
as usize;
assert_eq!(video(&s, made), (true, 0.1), "a new artboard takes them too");
let before = s.doc().unwrap().doc.clone();
for bad in [json!("yes"), json!(1)] {
assert!(s.execute("artboard.setProps", &json!({"index": 1, "showVideoSafeAreas": bad})).is_err());
assert!(s.execute("artboard.new", &json!({"showVideoSafeAreas": bad})).is_err());
}
for bad in [json!(0.09), json!(10.5), json!(0), json!(-1), json!("2"), json!(null)] {
assert!(s.execute("artboard.setProps", &json!({"index": 1, "videoRulerPixelAspectRatio": bad})).is_err(), "{bad}");
assert!(s.execute("artboard.new", &json!({"videoRulerPixelAspectRatio": bad})).is_err(), "{bad}");
}
assert!(
s.execute(
"artboard.setProps",
&json!({"artboards": [{"index": 0, "showVideoSafeAreas": false}, {"index": 1, "videoRulerPixelAspectRatio": 11}]})
)
.is_err()
);
assert_eq!(*s.doc().unwrap().doc, *before, "a bad value changes nothing");
s.execute("artboard.setProps", &json!({"index": 1, "videoRulerPixelAspectRatio": 10})).unwrap();
assert_eq!(video(&s, 1), (false, 10.0), "10 is in range");
}
/// 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
/// and square pixels, and a file's ratio outside 0.1–10 opens brought into that range.
#[test]
fn artboard_video_display_options_round_trip_through_the_native_format() {
let mut s = session();
s.execute("artboard.new", &json!({})).unwrap();
let save = |s: &Session| vectorcraft_format::save(&s.doc().unwrap().doc, false);
let before = save(&s);
s.execute(
"artboard.setProps",
&json!({"artboards": [{"index": 0, "showVideoSafeAreas": true}, {"index": 1, "videoRulerPixelAspectRatio": 0.9}]}),
)
.unwrap();
let bytes = save(&s);
let saved: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let ab = |i: usize| saved["document"]["artboards"][i].clone();
assert_eq!(ab(0)["show_video_safe_areas"], json!(true));
assert!(ab(0).get("video_ruler_pixel_aspect_ratio").is_none(), "square pixels aren't written: {}", ab(0));
assert_eq!(ab(1)["video_ruler_pixel_aspect_ratio"], json!(0.9));
assert!(ab(1).get("show_video_safe_areas").is_none(), "hidden safe areas aren't written: {}", ab(1));
let back = vectorcraft_format::load(&bytes).unwrap();
assert_eq!(back.artboards, s.doc().unwrap().doc.artboards);
assert_eq!((back.artboards[0].show_video_safe_areas, back.artboards[1].video_ruler_pixel_aspect_ratio), (true, 0.9));
s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "showVideoSafeAreas": false}, {"index": 1, "videoRulerPixelAspectRatio": 1}]}))
.unwrap();
assert_eq!(save(&s), before, "off and square again, the file is as it was");
// A file whose artboards have no such keys (older writers) opens with them off and square.
let mut old: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
for a in old["document"]["artboards"].as_array_mut().unwrap() {
let a = a.as_object_mut().unwrap();
a.remove("show_video_safe_areas");
a.remove("video_ruler_pixel_aspect_ratio");
}
let back = vectorcraft_format::load(&serde_json::to_vec(&old).unwrap()).unwrap();
assert!(back.artboards.iter().all(|a| !a.show_video_safe_areas && a.video_ruler_pixel_aspect_ratio == 1.0));
for (written, opens) in [(json!(0), 0.1), (json!(-3), 0.1), (json!(1e9), 10.0), (json!(2.5), 2.5)] {
let mut file: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
file["document"]["artboards"][1]["video_ruler_pixel_aspect_ratio"] = written.clone();
let back = vectorcraft_format::load(&serde_json::to_vec(&file).unwrap()).unwrap();
assert_eq!(back.artboards[1].video_ruler_pixel_aspect_ratio, opens, "{written}");
}
}
#[test]
fn swatch_groups_duplicate_sort() {
let mut s = session();
+4
View File
@@ -2329,6 +2329,8 @@ impl<'a> Reader<'a> {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
})
.collect();
@@ -2341,6 +2343,8 @@ impl<'a> Reader<'a> {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
}];
}
+2
View File
@@ -392,6 +392,8 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
let page_layer = |b: &mut Builder<'_>, art: Vec<Arc<Node>>, shown: bool| {
+8
View File
@@ -82,6 +82,8 @@ fn one_page_per_artboard_with_sizes() {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
d.artboards.push(Artboard {
@@ -91,6 +93,8 @@ fn one_page_per_artboard_with_sizes() {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
let bytes = export(&d, &PdfOptions::default()).unwrap();
@@ -109,6 +113,8 @@ fn artboard_range_and_bad_index() {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
let bytes = export(&d, &PdfOptions { artboards: Some(vec![1]), ..Default::default() }).unwrap();
@@ -756,6 +762,8 @@ fn import_max_pages_and_multi_page() {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
add(&mut d, rect_node(Rect::new(10.0, 10.0, 50.0, 50.0), Color::BLACK));
+2
View File
@@ -30,6 +30,8 @@ fn artboard(id: u32, x: f64) -> Artboard {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
}
}
+2
View File
@@ -28,6 +28,8 @@ fn doc(n: usize, image: bool, on_first: bool) -> Document {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
}
+2
View File
@@ -342,6 +342,8 @@ fn inkscape_pages(xml: &roxmltree::Document, user: Affine) -> Option<Vec<vectorc
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
})
.collect();
+4
View File
@@ -602,6 +602,8 @@ mod tests {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
d.guides = vec![Guide { artboard: Some(id), ..Guide::new(false, 150.0) }];
@@ -916,6 +918,8 @@ mod tests {
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
doc.ruler_origin = Point::new(5.0, 5.0);
+2
View File
@@ -461,6 +461,8 @@ impl Tool for ArtboardTool {
v["name"] = json!(a.name);
v["showCenterMark"] = json!(a.show_center_mark);
v["showCrossHairs"] = json!(a.show_cross_hairs);
v["showVideoSafeAreas"] = json!(a.show_video_safe_areas);
v["videoRulerPixelAspectRatio"] = json!(a.video_ruler_pixel_aspect_ratio);
vec![Action::Dialog("artboardOptions".into(), v)]
}
None => vec![],
+150 -44
View File
@@ -567,8 +567,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
if doc.setup.has_bleed() {
painter.add(Shape::closed_line(xf.quad(doc.setup.bleed_rect(ab.rect)), Stroke::new(1.0, t.bleed)));
}
// Show Center Mark and Show Cross Hairs (Artboard Options). Only the canvas draws them,
// as it does the edges: exports and print leave them out.
// Show Center Mark, Show Cross Hairs and Show Video Safe Areas (Artboard Options). Only the
// canvas draws them, as it does the edges: exports and print leave them out.
artboard_marks(&painter, &xf, ab, t.artboard_mark);
if editing_name != Some(i) && (artboard_tool || doc.artboards.len() > 1) && tool_labelled != u64::try_from(i).ok() {
// At the artboard's own top-left corner, which a rotated view turns away from its
@@ -659,7 +659,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
painter.line_segment([pos2(at.x, rect.top()), pos2(at.x, rect.bottom())], Stroke::new(1.0, t.text_dim));
painter.line_segment([pos2(rect.left(), at.y), pos2(rect.right(), at.y)], Stroke::new(1.0, t.text_dim));
}
rulers(ui, full, &xf, app.hover_doc, app.session.general_unit(), origin_preview.unwrap_or_else(|| app.ruler_origin()), &t);
// View › Rulers › Show Video Rulers counts the active artboard in video pixels.
let video = app.ui.view.video_rulers.then(|| crate::dialogs::artboard_options::active_index(app));
let scale = match video.and_then(|i| app.session.active()?.doc.artboards.get(i)) {
Some(ab) => RulerScale::video(ab),
None => RulerScale::document(app.session.general_unit(), origin_preview.unwrap_or_else(|| app.ruler_origin())),
};
rulers(ui, full, &xf, app.hover_doc, scale, &t);
}
// The task bar is not drawn while the Simplify slider shows.
if app.ui.task_bar && !revolve_edit && !app.session.tool_busy() && app.ui.screen_mode < 3 && !crate::dialogs::simplify_slider::showing(app) {
@@ -1931,7 +1937,46 @@ pub(crate) fn ruler_origin_dragging(ctx: &egui::Context) -> bool {
ctx.data(|d| d.get_temp::<(Option<u64>, bool)>(ruler_origin_drag_id()).is_some())
}
fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit, origin: Point, t: &Tokens) {
/// What the rulers count, and from where: `unit`, from `origin` (document points), with each
/// unit stretched `stretch` times along the document's x and y, which a video ruler's pixel aspect
/// ratio makes differ.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct RulerScale {
pub(crate) unit: Unit,
pub(crate) origin: Point,
pub(crate) stretch: [f64; 2],
}
impl RulerScale {
/// The rulers in `unit` (the General unit), from `origin` (the ruler origin).
pub(crate) fn document(unit: Unit, origin: Point) -> Self {
Self { unit, origin, stretch: [1.0, 1.0] }
}
/// View › Rulers › Show Video Rulers on artboard `ab`: pixels from its top-left corner, each
/// pixel as many points wide as its Video Ruler Pixel Aspect Ratio and one point tall, so the
/// rulers read the artboard's size in video pixels
/// (https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html).
pub(crate) fn video(ab: &vectorcraft_doc::Artboard) -> Self {
Self { unit: Unit::Pixels, origin: Point::new(ab.rect.x0, ab.rect.y0), stretch: [ab.video_ruler_pixel_aspect_ratio, 1.0] }
}
/// How far document point `p` lies from the origin along `axis` (a unit direction in the
/// document), in points of this scale: each document coordinate divided by its stretch.
/// Divided by the unit's points, it is the value the ruler reads there.
pub(crate) fn along(&self, p: Point, axis: vectorcraft_geom::Vec2) -> f64 {
let d = p - self.origin;
d.x / self.stretch[0] * axis.x + d.y / self.stretch[1] * axis.y
}
/// The screen points one point of this scale spans along `axis` at `zoom` (screen points per
/// document point): `zoom` itself when nothing is stretched.
pub(crate) fn zoom_along(&self, zoom: f64, axis: vectorcraft_geom::Vec2) -> f64 {
zoom / (axis.x * axis.x / self.stretch[0] + axis.y * axis.y / self.stretch[1])
}
}
fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, scale: RulerScale, t: &Tokens) {
let p = ui.painter();
let [top, left, corner] = ruler_rects(full);
for r in [top, left, corner] {
@@ -1942,43 +1987,40 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit,
// Crosshair in the origin box.
p.line_segment([corner.center() - vec2(4.0, 0.0), corner.center() + vec2(4.0, 0.0)], Stroke::new(1.0, t.ruler_tick));
p.line_segment([corner.center() - vec2(0.0, 4.0), corner.center() + vec2(0.0, 4.0)], Stroke::new(1.0, t.ruler_tick));
// Pick a label step (in `unit`) that gives ≥ 50 px between labels; positions below are in `unit`
// (`per` points each).
let per = unit.points();
let step = unit.ruler_step(xf.zoom);
let minor = unit.ruler_minor(step);
let font = egui::FontId::proportional(9.5);
let near = |v: f64, every: f64| ((v / every).round() * every - v).abs() < minor * 0.01;
// A tick's length: the labelled ones longest, the halves between them shorter.
let tick = |v: f64| {
if near(v, step) {
RULER
} else if near(v, step / 2.0) {
7.0
} else {
4.0
}
};
// Each ruler measures along its own edge, in the canvas's coordinates turned with the view
// (#826): at 0° the top one reads x and the left one y. `draw` gets each tick's distance
// (screen points) from `from` along the ruler, and its value in `unit`.
let ticks = |from: Pos2, to: Pos2, axis: vectorcraft_geom::Vec2, draw: &mut dyn FnMut(f32, f64)| {
let along = |s: Pos2| (xf.to_doc(s) - origin).dot(axis);
let (a, b) = (along(from), along(to));
// (screen points) from `from` along the ruler, its length, and the label of a labelled one.
let ticks = |from: Pos2, to: Pos2, axis: vectorcraft_geom::Vec2, draw: &mut dyn FnMut(f32, f32, Option<String>)| {
// Pick a label step (in the unit) that gives ≥ 50 px between labels on this ruler, at its
// own scale; positions below are in the unit (`per` points of the scale each).
let zoom = scale.zoom_along(xf.zoom, axis);
let (per, step) = (scale.unit.points(), scale.unit.ruler_step(zoom));
let minor = scale.unit.ruler_minor(step);
let near = |v: f64, every: f64| ((v / every).round() * every - v).abs() < minor * 0.01;
let (a, b) = (scale.along(xf.to_doc(from), axis), scale.along(xf.to_doc(to), axis));
let mut v = (a / per / minor).floor() * minor;
while v * per <= b {
draw(((v * per - a) * xf.zoom) as f32, v);
// A tick's length: the labelled ones longest, the halves between them shorter.
let (len, label) = if near(v, step) {
(RULER, Some(ruler_label(v, step)))
} else if near(v, step / 2.0) {
(7.0, None)
} else {
(4.0, None)
};
draw(((v * per - a) * zoom) as f32, len, label);
v += minor;
}
};
// The canvas's directions along the screen's x and y.
let (sn, cs) = xf.rot.sin_cos();
let clip_top = p.with_clip_rect(top);
ticks(top.left_top(), top.right_top(), vectorcraft_geom::Vec2::new(cs, -sn), &mut |d, x| {
ticks(top.left_top(), top.right_top(), vectorcraft_geom::Vec2::new(cs, -sn), &mut |d, len, label| {
let sx = top.left() + d;
clip_top.line_segment([pos2(sx, top.bottom() - tick(x)), pos2(sx, top.bottom())], Stroke::new(1.0, t.ruler_tick));
if near(x, step) {
clip_top.text(pos2(sx + 2.0, top.top() + 1.0), egui::Align2::LEFT_TOP, ruler_label(x, step), font.clone(), t.ruler_tick);
clip_top.line_segment([pos2(sx, top.bottom() - len), pos2(sx, top.bottom())], Stroke::new(1.0, t.ruler_tick));
if let Some(text) = label {
clip_top.text(pos2(sx + 2.0, top.top() + 1.0), egui::Align2::LEFT_TOP, text, font.clone(), t.ruler_tick);
}
});
let clip_left = p.with_clip_rect(left);
@@ -1986,12 +2028,11 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit,
// "-0.0625" is seven characters, 60 pt of ruler, where a sixteenth-inch step leaves 54 pt at
// zoom 12. The ticks all stay; a label that would run into the one above it is left out.
let mut filled_to = f32::NEG_INFINITY;
ticks(left.left_top(), left.left_bottom(), vectorcraft_geom::Vec2::new(sn, cs), &mut |d, y| {
ticks(left.left_top(), left.left_bottom(), vectorcraft_geom::Vec2::new(sn, cs), &mut |d, len, label| {
let sy = left.top() + d;
clip_left.line_segment([pos2(left.right() - tick(y), sy), pos2(left.right(), sy)], Stroke::new(1.0, t.ruler_tick));
if near(y, step) {
clip_left.line_segment([pos2(left.right() - len, sy), pos2(left.right(), sy)], Stroke::new(1.0, t.ruler_tick));
if let Some(text) = label {
// Vertical labels read top-to-bottom, one digit per line like Illustrator.
let text = ruler_label(y, step);
let top = sy + 2.0;
if top < filled_to {
return;
@@ -2930,7 +2971,10 @@ const CENTER_MARK_ARM: f32 = 6.0;
/// turned with the view: with Show Cross Hairs, a line from the middle of each side through the
/// center to the middle of the opposite side; with Show Center Mark, a cross at the center whose
/// arms run along the artboard's sides, [`CENTER_MARK_ARM`] long on screen
/// (https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/modify-display-settings-of-artboards.html).
/// (https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/modify-display-settings-of-artboards.html);
/// with Show Video Safe Areas, the outlines of its action-safe and title-safe areas
/// ([`vectorcraft_doc::Artboard::video_safe_areas`],
/// https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html).
fn artboard_marks(painter: &egui::Painter, xf: &Xf, ab: &vectorcraft_doc::Artboard, color: Color32) {
let (r, stroke) = (ab.rect, Stroke::new(1.0, color));
let c = r.center();
@@ -2946,6 +2990,11 @@ fn artboard_marks(painter: &egui::Painter, xf: &Xf, ab: &vectorcraft_doc::Artboa
painter.line_segment([mid - arm, mid + arm], stroke);
}
}
if ab.show_video_safe_areas {
for area in ab.video_safe_areas() {
painter.add(Shape::closed_line(xf.quad(area), stroke));
}
}
}
/// Artboard `name` above its top-left `corner` on screen, no wider than the artboard (`width` on
@@ -3709,7 +3758,8 @@ mod tests {
let [top, left, _] = ruler_rects(full);
for deg in [0.0_f64, 30.0, 90.0, 135.0, 200.0] {
let xf = Xf { rect: full, zoom: 1.0, center: Point::new(300.0, 200.0), rot: deg.to_radians() };
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, Unit::Points, Point::ZERO, &Tokens::get(ui.ctx())));
let scale = RulerScale::document(Unit::Points, Point::ZERO);
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, scale, &Tokens::get(ui.ctx())));
out.textures_delta.clear();
// The ticks' positions along each ruler.
let along = |ruler: egui::Rect, x: bool| -> Vec<f32> {
@@ -3750,7 +3800,8 @@ mod tests {
for deg in [0.0_f64, 30.0, 90.0, 200.0] {
for unit in [Unit::Points, Unit::Millimeters] {
let xf = Xf { rect: full, zoom: 1.7, center: Point::new(300.0, 200.0), rot: deg.to_radians() };
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, unit, origin, &Tokens::get(ui.ctx())));
let scale = RulerScale::document(unit, origin);
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, scale, &Tokens::get(ui.ctx())));
out.textures_delta.clear();
let at = xf.to_screen(origin);
for (r, horizontal) in [(top, true), (left, false)] {
@@ -3776,6 +3827,66 @@ mod tests {
}
}
/// View › Rulers › Show Video Rulers counts the artboard in video pixels from its top-left
/// corner, a pixel as wide as the artboard's Video Ruler Pixel Aspect Ratio. At 0.9 a
/// 720 × 480 pt artboard is 800 pixels wide and 480 tall: the top ruler reads 0 at its left
/// edge and 800 at its right, labelled every 100 pixels (90 points on screen at 100%), and the
/// left ruler counts a pixel a point, from 0 at the top edge to a tick at the bottom one. At 1
/// (square pixels) the artboard is 720 pixels wide.
#[test]
fn video_rulers_count_the_artboard_in_pixels_of_its_aspect_ratio() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 720, "height": 480})).unwrap();
s.execute("artboard.setProps", &json!({"index": 0, "videoRulerPixelAspectRatio": 0.9})).unwrap();
let ab = s.active().unwrap().doc.artboards[0].clone();
let r = ab.rect;
let scale = RulerScale::video(&ab);
let (x, y) = (vectorcraft_geom::Vec2::new(1.0, 0.0), vectorcraft_geom::Vec2::new(0.0, 1.0));
let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
assert_eq!((scale.unit, scale.origin), (Unit::Pixels, Point::new(r.x0, r.y0)));
assert!(close(scale.along(Point::new(r.x1, r.y1), x), 800.0) && close(scale.along(Point::new(r.x1, r.y1), y), 480.0));
assert!(close(scale.along(Point::new(r.x0 + 9.0, r.y0), x), 10.0), "a pixel is 0.9 pt wide");
assert!(close(scale.zoom_along(2.0, x), 1.8) && close(scale.zoom_along(2.0, y), 2.0));
let square = RulerScale { stretch: [1.0, 1.0], ..scale };
assert!(close(square.along(Point::new(r.x1, r.y0), x), 720.0));
// Drawn at 100%.
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let full = egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
let [top, left, _] = ruler_rects(full);
let xf = Xf { rect: full, zoom: 1.0, center: r.center(), rot: 0.0 };
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, scale, &Tokens::get(ui.ctx())));
out.textures_delta.clear();
let labels = |ruler: egui::Rect| -> Vec<(String, Pos2)> {
out.shapes
.iter()
.filter(|c| c.clip_rect == ruler)
.filter_map(|c| match &c.shape {
Shape::Text(t) => Some((t.galley.text().to_string(), t.pos)),
_ => None,
})
.collect()
};
let (l, rgt) = (xf.to_screen(Point::new(r.x0, r.y0)), xf.to_screen(Point::new(r.x1, r.y1)));
let at = |labels: &[(String, Pos2)], text: &str, x: f32| labels.iter().any(|(t, p)| t == text && (p.x - 2.0 - x).abs() < 0.02);
let tops = labels(top);
assert!(at(&tops, "0", l.x) && at(&tops, "800", rgt.x), "{tops:?} from {l:?} to {rgt:?}");
assert!(at(&tops, "100", l.x + 90.0) && !tops.iter().any(|(t, _)| t == "50"), "{tops:?}");
assert!(!tops.iter().any(|(t, _)| t == "720"), "points aren't pixels: {tops:?}");
let lefts = labels(left);
assert!(lefts.iter().any(|(t, p)| t == "0" && (p.y - 2.0 - l.y).abs() < 0.02), "{lefts:?}");
let ticks: Vec<f32> = out
.shapes
.iter()
.filter(|c| c.clip_rect == left)
.filter_map(|c| match &c.shape {
Shape::LineSegment { points: [a, b], .. } if a.y == b.y => Some(a.y),
_ => None,
})
.collect();
assert!(ticks.iter().any(|y| (y - rgt.y).abs() < 0.02), "a tick at 480 px: {ticks:?}");
}
#[test]
fn shaper_selection_highlights_only_the_visible_result() {
let mut s = Session::new();
@@ -9570,15 +9681,11 @@ mod tests {
let hover_shapes = shapes(&mut app, &ctx);
// There should be a 1.5px path outline for the hover highlight.
let hover_outlines: Vec<_> = hover_shapes
.iter()
.filter(|s| matches!(s, Shape::Path(ps) if ps.stroke.width == 1.5 && ps.stroke.color == layer_mode))
.collect();
let hover_outlines: Vec<_> =
hover_shapes.iter().filter(|s| matches!(s, Shape::Path(ps) if ps.stroke.width == 1.5 && ps.stroke.color == layer_mode)).collect();
assert!(!hover_outlines.is_empty(), "hover highlight shows baseline");
let is_handle = |s: &Shape| {
matches!(s, Shape::Rect(r) if (r.rect.width() - 6.0).abs() < 1.0 && (r.rect.height() - 6.0).abs() < 1.0 && r.fill == Color32::WHITE)
};
let is_handle = |s: &Shape| matches!(s, Shape::Rect(r) if (r.rect.width() - 6.0).abs() < 1.0 && (r.rect.height() - 6.0).abs() < 1.0 && r.fill == Color32::WHITE);
// There should be NO bounding box resize handles (size 6x6 white squares) while just hovering.
let handles: Vec<_> = hover_shapes.iter().filter(|s| is_handle(s)).collect();
@@ -9602,4 +9709,3 @@ mod tests {
assert!(!baselines.is_empty(), "selected text shows baseline alongside bounding box");
}
}
+36 -12
View File
@@ -1,9 +1,12 @@
//! 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), and Show Center Mark and Show Cross Hairs (`showCenterMark`,
//! it, 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).
//! (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
//! (`showVideoSafeAreas`, `videoRulerPixelAspectRatio`)
//! (https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html).
//! Sizing choices also go to the other selected artboards; fitting changes only the active
//! artboard around stationary artwork.
@@ -35,7 +38,8 @@ 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, and the Show Center Mark and Show Cross Hairs checkboxes.
/// that picks a color of its own, the Show Center Mark, Show Cross Hairs and Show Video Safe Areas
/// checkboxes, and the Video Ruler Pixel Aspect Ratio field.
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let (unit, origin, refi) = (app.session.general_unit(), app.ruler_origin(), reference_index(ui));
let (was, w0, h0) = (placed(d), d.f64("width", 0.0), d.f64("height", 0.0));
@@ -98,9 +102,21 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
ui.add_space(8.0);
form::check(ui, d, "showCenterMark", tl!("Show Center Mark"));
form::check(ui, d, "showCrossHairs", tl!("Show Cross Hairs"));
form::check(ui, d, "showVideoSafeAreas", tl!("Show Video Safe Areas"));
ui.horizontal(|ui| {
form::row_label(ui, tl!("Video Ruler Pixel Aspect Ratio:"));
let range = vectorcraft_doc::VIDEO_PIXEL_ASPECT_RANGE;
let now = d.f64(PIXEL_ASPECT, 1.0);
if let Some(v) = crate::widgets::range_field(ui, ("dlg-num", PIXEL_ASPECT), now, range, "", 4, 60.0) {
d.fields.insert(PIXEL_ASPECT.into(), json!(v));
}
});
false
}
/// The field of the Video Ruler Pixel Aspect Ratio, as `artboard.setProps` takes it.
pub(super) const PIXEL_ASPECT: &str = "videoRulerPixelAspectRatio";
/// Store document-coordinate draft bounds, retaining an editable minimum for zero-area art.
fn set_rect(d: &mut Dialog, r: Rect) {
for (key, value) in [("x", r.x0), ("y", r.y0), ("width", r.width().max(1.0)), ("height", r.height().max(1.0))] {
@@ -188,14 +204,18 @@ fn position_rows(ui: &mut egui::Ui, d: &mut Dialog, refi: usize, unit: Unit, ori
/// The dialog kind of Artboard Options.
pub const KIND: &str = "artboardOptions";
/// Open Artboard Options for the active artboard: the Artboard tool's while it is in use, else the
/// window's.
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
let index = if app.session.tool_id() == "artboard" {
/// The active artboard's index: the Artboard tool's while it is in use, else the window's.
pub(crate) fn active_index(app: &VectorcraftApp) -> usize {
if app.session.tool_id() == "artboard" {
app.session.tool_options()["active"].as_u64().and_then(|i| usize::try_from(i).ok()).unwrap_or(0)
} else {
app.view().map_or(0, |v| v.artboard)
};
}
}
/// Open Artboard Options for the active artboard ([`active_index`]).
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
let index = active_index(app);
let st = app.session.active().ok_or("no document")?;
let a = st.doc.artboards.get(index).ok_or("no artboard")?;
let r = a.rect;
@@ -203,6 +223,7 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
let fields = json!({
"index": index, "name": a.name, "x": r.x0, "y": r.y0, "width": r.width(), "height": r.height(), "color": color,
"showCenterMark": a.show_center_mark, "showCrossHairs": a.show_cross_hairs,
"showVideoSafeAreas": a.show_video_safe_areas, PIXEL_ASPECT: a.video_ruler_pixel_aspect_ratio,
});
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(json!({ "dialog": KIND }))
@@ -219,7 +240,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
return confirm_new(app, d);
}
let mut p = Value::Object(d.fields.clone());
for k in ["x", "y", "width", "height"] {
for k in ["x", "y", "width", "height", PIXEL_ASPECT] {
if d.fields.contains_key(k) {
p[k] = json!(d.f64(k, 0.0));
}
@@ -275,8 +296,8 @@ pub fn open_new(app: &mut VectorcraftApp) -> Result<Value, String> {
}
/// OK for a new artboard: `artboard.new` with the fields (an empty name gives the default one),
/// the background color picked and the display marks ticked, and the new artboard becomes the
/// active one.
/// the background color 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<Value, String> {
let mut p = json!({});
for k in ["x", "y", "width", "height"] {
@@ -289,11 +310,14 @@ fn confirm_new(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
if let Some(c) = d.fields.get("color") {
p["color"] = c.clone();
}
for k in ["showCenterMark", "showCrossHairs"] {
for k in ["showCenterMark", "showCrossHairs", "showVideoSafeAreas"] {
if d.bool(k) {
p[k] = json!(true);
}
}
if d.fields.contains_key(PIXEL_ASPECT) {
p[PIXEL_ASPECT] = json!(d.f64(PIXEL_ASPECT, 1.0));
}
let r = run_and_close(app, "artboard.new", p)?;
if let Some(i) = r["index"].as_u64().and_then(|i| usize::try_from(i).ok()) {
crate::panels::artboards::select(app, i);
+1 -1
View File
@@ -83,7 +83,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"],
"artboardOptions" => &["color", super::artboard_options::PIXEL_ASPECT],
// The grids' Reference Point is their 9-point locator.
"rectangularGrid" | "polarGrid" => &["reference"],
_ => &[],
+56
View File
@@ -1152,6 +1152,62 @@ fn artboard_options_show_center_mark_and_cross_hairs() {
}
}
/// Artboard Options shows Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (one row of
/// it), set 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 (a typed ratio is read as a
/// number), an out-of-range ratio keeps the dialog open and changes nothing, and a new artboard's
/// Artboard Options gives it the ones set.
#[test]
fn artboard_options_show_video_display_options() {
use vectorcraft_tools::{PointerEvent, PointerKind};
let mut app = app();
let video = |app: &VectorcraftApp, i: usize| {
let a = &app.session.doc().unwrap().doc.artboards[i];
(a.show_video_safe_areas, a.video_ruler_pixel_aspect_ratio)
};
crate::dialogs::artboard_options::open(&mut app).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
assert!(text.contains("Show Video Safe Areas"), "{text}");
assert_eq!(text.matches("Video Ruler Pixel Aspect Ratio").count(), 1, "one row: {text}");
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((d.bool("showVideoSafeAreas"), d.f64("videoRulerPixelAspectRatio", 0.0)), (false, 1.0));
d.fields.insert("showVideoSafeAreas".into(), json!(true));
d.fields.insert("videoRulerPixelAspectRatio".into(), json!("0.9"));
let undo = app.session.doc().unwrap().history.undo.len();
super::confirm(&mut app).unwrap();
assert_eq!(video(&app, 0), (true, 0.9));
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
// Opened again, it shows them; a ratio out of range is refused.
crate::dialogs::artboard_options::open(&mut app).unwrap();
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((d.bool("showVideoSafeAreas"), d.f64("videoRulerPixelAspectRatio", 0.0)), (true, 0.9));
d.fields.insert("videoRulerPixelAspectRatio".into(), json!(20));
assert!(super::confirm(&mut app).is_err());
assert_eq!(video(&app, 0), (true, 0.9), "nothing changed");
cancel(&mut app);
// A double-click with the Artboard tool on the artboard's empty space.
app.select_tool("artboard");
let view = app.view_info();
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick] {
crate::canvas::dispatch(&mut app, &PointerEvent::new(kind, 100.0, 100.0), view);
}
let d = app.ui.dialog.clone().expect("Artboard Options opened");
assert_eq!((d.bool("showVideoSafeAreas"), d.f64("videoRulerPixelAspectRatio", 0.0)), (true, 0.9));
cancel(&mut app);
// A new artboard takes the ones set, and the defaults otherwise.
for (set, want) in [(true, (true, 2.0)), (false, (false, 1.0))] {
app.run("ui.newArtboard", json!({})).unwrap();
if set {
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("showVideoSafeAreas".into(), json!(true));
d.fields.insert("videoRulerPixelAspectRatio".into(), json!(2));
}
super::confirm(&mut app).unwrap();
let last = app.session.doc().unwrap().doc.artboards.len() - 1;
assert_eq!(video(&app, last), want);
}
}
/// #1482: with Artboards 1 and 3 selected with the Artboard tool, a new Width in Artboard Options
/// goes to both, each keeping its top-left corner and its height, in one undo step. The new name
/// goes only to the artboard the dialog shows, and Artboard 2 keeps its size. A name alone resizes
+4
View File
@@ -268,6 +268,9 @@ font-technology Variable Променливи
Background Color: Цвят на фона:
Show Center Mark Показване на централен знак
Show Cross Hairs Показване на кръстосани линии
Show Video Safe Areas Показване на безопасните видеозони
Video Ruler Pixel Aspect Ratio: Пропорция на пиксела на видеолинийките:
Hide Video Rulers Скриване на видеолинийките
Background Contents Съдържание на фона
Background Contents: Съдържание на фона:
Background: Фон:
@@ -3866,6 +3869,7 @@ axis Both И двете
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` оста трябва да бъде "vertical", "horizontal" или ъгъл в градуси, а не `{v}`
@msg background {other}: transparent, white, black or a colour such as "#ff8800" фон {other}: прозрачен, бял, черен или цвят като "#ff8800"
@msg bad color {v} грешен цвят {v}
@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 повредено дърво на страниците
@msg bad shortcut грешен клавишен бърз път
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2438,6 +2438,9 @@ lighting Light Světlo
Background Color: Barva pozadí:
Show Center Mark Zobrazit středovou značku
Show Cross Hairs Zobrazit nitkový kříž
Show Video Safe Areas Zobrazit bezpečné oblasti videa
Video Ruler Pixel Aspect Ratio: Poměr stran pixelu video pravítek:
Hide Video Rulers Skrýt video pravítka
Client-side (.html) Na straně klienta (.html)
Color Reduction: Redukce barev:
Lossless (lossy WebP isn't available yet) Bezeztrátový (ztrátový WebP zatím není k dispozici)
@@ -4255,6 +4258,7 @@ imageTrace Outline Obrys
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` osa 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 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ý
@msg the screen color sampler is already open nástroj pro nabírání barvy z obrazovky už je otevřený
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2237,6 +2237,9 @@ lighting Light Lys
Background Color: Baggrundsfarve:
Show Center Mark Vis midtermærke
Show Cross Hairs Vis trådkors
Show Video Safe Areas Vis sikre videoområder
Video Ruler Pixel Aspect Ratio: Pixelformat for videolinealer:
Hide Video Rulers Skjul videolinealer
Client-side (.html) Klientbaseret (.html)
Color Reduction: Farvereduktion:
Lossless (lossy WebP isn't available yet) Tabsfri (WebP med tab er ikke tilgængelig endnu)
@@ -3982,6 +3985,7 @@ imageTrace Outline Kontur
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` aksen skal være "vertical", "horizontal" eller en vinkel i grader, ikke `{v}`
@msg background {other}: transparent, white, black or a colour such as "#ff8800" baggrund {other}: gennemsigtig, hvid, sort eller en farve som "#ff8800"
@msg bad color {v} ugyldig farve {v}
@msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio skal være et tal fra 0,1 til 10
@msg bad colour `{hex}` (#rrggbb) ugyldig farve `{hex}` (#rrggbb)
@msg bad page tree ugyldigt sidetræ
@msg bad shortcut ugyldig genvej
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -256,6 +256,9 @@ font-technology Variable Variabel
Background Color: Hintergrundfarbe:
Show Center Mark Mittelpunktmarke einblenden
Show Cross Hairs Fadenkreuz einblenden
Show Video Safe Areas Videosichere Bereiche einblenden
Video Ruler Pixel Aspect Ratio: Pixel-Seitenverhältnis der Videolineale:
Hide Video Rulers Videolineale ausblenden
Background Contents Hintergrundinhalt
Background Contents: Hintergrundinhalt:
Background: Hintergrund:
@@ -3876,6 +3879,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 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
@msg the screen color sampler is already open die Farbaufnahme vom Bildschirm ist bereits geöffnet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -259,6 +259,9 @@ font-technology Variable Μεταβλητές
Background Color: Χρώμα φόντου:
Show Center Mark Εμφάνιση σημαδιού κέντρου
Show Cross Hairs Εμφάνιση σταυρονήματος
Show Video Safe Areas Εμφάνιση ασφαλών περιοχών βίντεο
Video Ruler Pixel Aspect Ratio: Αναλογία διαστάσεων pixel χαράκων βίντεο:
Hide Video Rulers Απόκρυψη χαράκων βίντεο
Background Contents Περιεχόμενα φόντου
Background Contents: Περιεχόμενο φόντου:
Background: Φόντο:
@@ -3935,6 +3938,7 @@ theme Appearance Εμφάνιση
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` ο άξονας πρέπει να είναι "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 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 μη έγκυρο δέντρο σελίδων
@msg bad shortcut μη έγκυρη συντόμευση
Can't render this file because it contains an unexpected character in line 2052 and column 19.
+4
View File
@@ -281,6 +281,9 @@ font-technology Variable Variables
Background Color: Color de fondo:
Show Center Mark Mostrar marca central
Show Cross Hairs Mostrar líneas en cruz
Show Video Safe Areas Mostrar áreas seguras de video
Video Ruler Pixel Aspect Ratio: Proporción de píxel de las reglas de video:
Hide Video Rulers Ocultar reglas de video
Background Contents Contenido de fondo
Background Contents: Contenido de fondo:
Background: Fondo:
@@ -3952,6 +3955,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 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
@msg the screen color sampler is already open el muestreador de color de la pantalla ya está abierto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2384,6 +2384,9 @@ lighting Light Valo
Background Color: Taustaväri:
Show Center Mark Näytä keskimerkki
Show Cross Hairs Näytä hiusristikko
Show Video Safe Areas Näytä videon turva-alueet
Video Ruler Pixel Aspect Ratio: Videoviivainten kuvapisteen kuvasuhde:
Hide Video Rulers Piilota videoviivaimet
Client-side (.html) Asiakaspuoli (.html)
Color Reduction: Värien vähennys:
Lossless (lossy WebP isn't available yet) Häviötön (häviöllinen WebP ei ole vielä käytettävissä)
@@ -4172,6 +4175,7 @@ imageTrace Outline Ääriviiva
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` akselin 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 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
@msg bad shortcut virheellinen pikanäppäin
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -282,6 +282,9 @@ font-technology Variable Variables
Background Color: Couleur d’arrière-plan :
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
Video Ruler Pixel Aspect Ratio: Format des pixels des règles vidéo :
Hide Video Rulers Masquer les règles vidéo
Background Contents Contenu de l’arrière-plan
Background Contents: Contenu de l’arrière-plan :
Background: Arrière-plan :
@@ -3949,6 +3952,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 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
@msg the screen color sampler is already open l’outil de prélèvement de couleur à l’écran est déjà ouvert
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -377,6 +377,9 @@ font-technology Variable Változó
Background Color: Háttérszín:
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
Video Ruler Pixel Aspect Ratio: Videovonalzók képpont-méretaránya:
Hide Video Rulers Videovonalzók elrejtése
Background Contents Háttértartalom
Background Contents: Háttértartalom:
Background: Háttér:
@@ -4508,6 +4511,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 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
@msg the screen color sampler is already open a képernyős színmintavevő már nyitva van
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2237,6 +2237,9 @@ lighting Light Cahaya
Background Color: Warna latar belakang:
Show Center Mark Tampilkan tanda tengah
Show Cross Hairs Tampilkan garis silang
Show Video Safe Areas Tampilkan area aman video
Video Ruler Pixel Aspect Ratio: Rasio aspek piksel penggaris video:
Hide Video Rulers Sembunyikan penggaris video
Client-side (.html) Sisi klien (.html)
Color Reduction: Reduksi warna:
Lossless (lossy WebP isn't available yet) Tanpa kehilangan (WebP lossy belum tersedia)
@@ -3982,6 +3985,7 @@ imageTrace Outline Kontur
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` sumbu harus "vertical", "horizontal", atau sudut dalam derajat, bukan `{v}`
@msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparan, putih, hitam, atau warna seperti "#ff8800"
@msg bad color {v} warna tidak valid {v}
@msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio harus berupa angka dari 0,1 sampai 10
@msg bad colour `{hex}` (#rrggbb) warna tidak valid `{hex}` (#rrggbb)
@msg bad page tree pohon halaman tidak valid
@msg bad shortcut pintasan tidak valid
Can't render this file because it contains an unexpected character in line 3823 and column 19.
+4
View File
@@ -281,6 +281,9 @@ font-technology Variable Variabili
Background Color: Colore di sfondo:
Show Center Mark Mostra segno centrale
Show Cross Hairs Mostra mirino
Show Video Safe Areas Mostra aree sicure video
Video Ruler Pixel Aspect Ratio: Proporzioni pixel dei righelli video:
Hide Video Rulers Nascondi righelli video
Background Contents Contenuto sfondo
Background Contents: Contenuto sfondo:
Background: Sfondo:
@@ -3992,6 +3995,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 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
@msg the screen color sampler is already open il campionatore di colore dello schermo è già aperto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -940,6 +940,9 @@ font-technology Variable バリアブル
Background Color: 背景色:
Show Center Mark 中心マークを表示
Show Cross Hairs 十字線を表示
Show Video Safe Areas ビデオセーフエリアを表示
Video Ruler Pixel Aspect Ratio: ビデオ定規のピクセル縦横比:
Hide Video Rulers ビデオ定規を隠す
Background Contents 背景のカラー
Background Contents: 背景のカラー:
Background: 背景:
@@ -4315,6 +4318,7 @@ swatch Bright Violet ビビッドバイオレット
@msg {label}… {label}…
@msg no blend options ブレンドオプションがありません
@msg bad color {v} カラー {v} が正しくありません
@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 単純化スライダーが開いていません
@msg the screen color sampler is already open 画面のカラーサンプラーはすでに開いています
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2132,6 +2132,9 @@ lighting Light სინათლე
Background Color: ფონის ფერი:
Show Center Mark ცენტრის ნიშნის ჩვენება
Show Cross Hairs ჯვარედინი ხაზების ჩვენება
Show Video Safe Areas ვიდეოს უსაფრთხო არეების ჩვენება
Video Ruler Pixel Aspect Ratio: ვიდეოს სახაზავების პიქსელის თანაფარდობა:
Hide Video Rulers ვიდეოს სახაზავების დამალვა
Client-side (.html) კლიენტის მხარე (.html)
Color Reduction: ფერების შემცირება:
Lossless (lossy WebP isn't available yet) უდანაკარგო (დანაკარგიანი WebP ჯერ მიუწვდომელია)
@@ -3743,6 +3746,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 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 გვერდების არასწორი ხე
@msg bad shortcut არასწორი მალსახმობი
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -935,6 +935,9 @@ font-technology Variable 가변
Background Color: 배경색:
Show Center Mark 중심 표시 보기
Show Cross Hairs 십자선 보기
Show Video Safe Areas 비디오 안전 영역 보기
Video Ruler Pixel Aspect Ratio: 비디오 눈금자 픽셀 종횡비:
Hide Video Rulers 비디오 눈금자 숨기기
Background Contents 배경 내용
Background Contents: 배경 내용:
Background: 배경:
@@ -4247,6 +4250,7 @@ theme Appearance 외관
@msg {label}… {label}…
@msg no blend options 혼합 옵션이 없습니다
@msg bad color {v} 잘못된 색상 {v}
@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 단순화 슬라이더가 열려 있지 않습니다
@msg the screen color sampler is already open 화면 색상 추출기가 이미 열려 있습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2251,6 +2251,9 @@ lighting Light Licht
Background Color: Achtergrondkleur:
Show Center Mark Middenmarkering tonen
Show Cross Hairs Dradenkruis tonen
Show Video Safe Areas Veilige videogebieden tonen
Video Ruler Pixel Aspect Ratio: Pixelverhouding van videolinialen:
Hide Video Rulers Videolinialen verbergen
Client-side (.html) Client-side (.html)
Color Reduction: Kleurreductie:
Lossless (lossy WebP isn't available yet) Verliesvrij (WebP met verlies is nog niet beschikbaar)
@@ -3996,6 +3999,7 @@ imageTrace Outline Contour
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` as moet "vertical", "horizontal" of een hoek in graden zijn, niet `{v}`
@msg background {other}: transparent, white, black or a colour such as "#ff8800" achtergrond {other}: transparant, wit, zwart of een kleur zoals "#ff8800"
@msg bad color {v} onjuiste kleur {v}
@msg videoRulerPixelAspectRatio must be a number from 0.1 to 10 videoRulerPixelAspectRatio moet een getal van 0,1 tot 10 zijn
@msg bad colour `{hex}` (#rrggbb) onjuiste kleur `{hex}` (#rrggbb)
@msg bad page tree onjuiste paginaboom
@msg bad shortcut onjuiste sneltoets
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2351,6 +2351,9 @@ lighting Light Światło
Background Color: Kolor tła:
Show Center Mark Pokaż znacznik środka
Show Cross Hairs Pokaż krzyż nitkowy
Show Video Safe Areas Pokaż bezpieczne obszary wideo
Video Ruler Pixel Aspect Ratio: Proporcje piksela linijek wideo:
Hide Video Rulers Ukryj linijki wideo
Client-side (.html) Po stronie klienta (.html)
Color Reduction: Redukcja kolorów:
Lossless (lossy WebP isn't available yet) Bezstratny (stratny WebP nie jest jeszcze dostępny)
@@ -4077,6 +4080,7 @@ imageTrace Outline Kontur
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` oś 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 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
@msg the screen color sampler is already open narzędzie do pobierania koloru z ekranu jest już otwarte
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -266,6 +266,9 @@
Background Color: Cor do Plano de Fundo:
Show Center Mark Mostrar Marca Central
Show Cross Hairs Mostrar Linhas Cruzadas
Show Video Safe Areas Mostrar Áreas Seguras de Vídeo
Video Ruler Pixel Aspect Ratio: Proporção de Pixel das Réguas de Vídeo:
Hide Video Rulers Ocultar Réguas de Vídeo
Background Contents Conteúdo do Plano de Fundo
Background Contents: Conteúdo do Plano de Fundo:
Background: Plano de Fundo:
@@ -3532,6 +3535,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 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
@msg bad shortcut atalho inválido
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -279,6 +279,9 @@ font-technology Variable Вариативные
Background Color: Цвет фона:
Show Center Mark Показать центральную метку
Show Cross Hairs Показать перекрестие
Show Video Safe Areas Показать безопасные зоны видео
Video Ruler Pixel Aspect Ratio: Соотношение сторон пикселя видео-линеек:
Hide Video Rulers Скрыть видео-линейки
Background Contents Содержимое фона
Background Contents: Содержимое фона:
Background: Фон:
@@ -3988,6 +3991,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 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 ползунок «Упростить» не открыт
@msg the screen color sampler is already open средство выбора цвета с экрана уже открыто
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -270,6 +270,9 @@ font-technology Variable Variabla
Background Color: Bakgrundsfärg:
Show Center Mark Visa mittmarkering
Show Cross Hairs Visa hårkors
Show Video Safe Areas Visa videosäkra områden
Video Ruler Pixel Aspect Ratio: Pixelproportion för videolinjaler:
Hide Video Rulers Dölj videolinjaler
Background Contents Bakgrundsinnehåll
Background Contents: Bakgrundsinnehåll:
Background: Bakgrund:
@@ -3871,6 +3874,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 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
@msg bad shortcut ogiltigt kortkommando
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -944,6 +944,7 @@ fn toggled_labels() -> Vec<String> {
"view.artboards",
"view.template",
"view.rulers",
"view.videoRulers",
"view.boundingBox",
"view.transparencyGrid",
"view.guides",
+4
View File
@@ -266,6 +266,9 @@ font-technology Variable Değişken
Background Color: Arka Plan Rengi:
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
Video Ruler Pixel Aspect Ratio: Video Cetveli Piksel En Boy Oranı:
Hide Video Rulers Video Cetvellerini Gizle
Background Contents Arka Plan İçeriği
Background Contents: Arkaplan İçeriği:
Background: Arka plan:
@@ -3734,6 +3737,7 @@ axis Both Her İkisi
@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{v}` eksen "{v}" değil, "dikey", "yatay" veya derece cinsinden bir açı olmalıdır
@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 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
@msg this system has no screen color sampler bu sistemin ekran rengi örnekleyicisi yok
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -275,6 +275,9 @@ font-technology Variable Варіативні
Background Color: Колір тла:
Show Center Mark Показати центральну позначку
Show Cross Hairs Показати перехрестя
Show Video Safe Areas Показати безпечні зони відео
Video Ruler Pixel Aspect Ratio: Співвідношення сторін пікселя відео-лінійок:
Hide Video Rulers Приховати відео-лінійки
Background Contents Вміст фону
Background Contents: Вміст фону:
Background: Фон:
@@ -3976,6 +3979,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 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 повзунок «Спростити» не відкрито
@msg the screen color sampler is already open засіб вибору кольору з екрана вже відкритий
Can't render this file because it contains an unexpected character in line 740 and column 40.
+4
View File
@@ -304,6 +304,9 @@ font-technology Variable Biến thiên
Background Color: Màu nề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
Video Ruler Pixel Aspect Ratio: Tỷ lệ khung điểm ảnh của thước video:
Hide Video Rulers Ẩn thước video
Background Contents Nội dung nền
Background Contents: Nội dung nền:
Background: Nền:
@@ -4157,6 +4160,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 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ở
@msg the screen color sampler is already open công cụ lấy mẫu màu trên màn hình đã được mở
Can't render this file because it contains an unexpected character in line 4037 and column 10.
+4
View File
@@ -227,6 +227,9 @@
Background Color: 背景颜色:
Show Center Mark 显示中心标记
Show Cross Hairs 显示十字线
Show Video Safe Areas 显示视频安全区域
Video Ruler Pixel Aspect Ratio: 视频标尺像素长宽比:
Hide Video Rulers 隐藏视频标尺
Background Contents 背景内容
Background Contents: 背景内容:
Background: 背景:
@@ -4043,6 +4046,7 @@ imageTrace Outline 轮廓
@msg {label}… {label}…
@msg no blend options 没有混合选项
@msg bad color {v} 颜色 {v} 无效
@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 简化滑块未打开
@msg the screen color sampler is already open 屏幕取色器已打开
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -240,6 +240,9 @@ font-technology Variable 可變
Background Color: 背景顏色:
Show Center Mark 顯示中心標記
Show Cross Hairs 顯示十字線
Show Video Safe Areas 顯示視訊安全區域
Video Ruler Pixel Aspect Ratio: 視訊尺標像素長寬比:
Hide Video Rulers 隱藏視訊尺標
Background Contents 背景內容
Background Contents: 背景內容:
Background: 背景:
@@ -2031,6 +2034,7 @@ file-filter Templates 範本
@msg {label}… {label}…
@msg no blend options 沒有漸變選項
@msg bad color {v} 顏色 {v} 無效
@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 「簡化」滑桿未開啟
@msg the screen color sampler is already open 螢幕取色器已開啟
Can't render this file because it contains an unexpected character in line 6 and column 47.
+24 -1
View File
@@ -353,6 +353,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"{} template layers left off the canvas (Show Template draws them again, dimmed as before) → shown",
),
("view.rulers", "Show Rulers", "Cmd+R", "{}"),
(
"view.videoRulers",
"Show Video Rulers",
"",
"{} the rulers show (as Show Rulers shows them) and count the active artboard in video pixels from its top-left corner, a pixel as wide as its Video Ruler Pixel Aspect Ratio says (artboard.setProps videoRulerPixelAspectRatio) and one point tall; again (Hide Video Rulers), they count in the General unit from the ruler origin as before → whether video rulers show",
),
("view.boundingBox", "Hide Bounding Box", "Cmd+Shift+B", "{}"),
("view.guides", "Hide Guides", "Cmd+;", "{}"),
("view.smartGuides", "Smart Guides", "Cmd+U", "{}"),
@@ -1401,6 +1407,13 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
"view.artboards" => flag(&mut app.ui.view.artboards),
"view.template" => flag(&mut app.ui.view.templates),
"view.rulers" => flag(&mut app.ui.view.rulers),
"view.videoRulers" => {
let v = &mut app.ui.view;
let show = !(v.video_rulers && v.rulers);
v.video_rulers = show;
v.rulers |= show;
Ok(json!(show))
}
"view.boundingBox" => flag(&mut app.ui.view.bounding_box),
"view.guides" => {
let r = flag(&mut app.ui.view.guides);
@@ -2040,6 +2053,7 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String {
id if saved_selection_slot(id).is_some() => saved_selection_name(app, id).unwrap_or_else(|| "—".into()),
"view.artboards" => if v.artboards { "Hide Artboards" } else { "Show Artboards" }.into(),
"view.rulers" => if v.rulers { "Hide Rulers" } else { "Show Rulers" }.into(),
"view.videoRulers" => if v.video_rulers && v.rulers { "Hide Video Rulers" } else { "Show Video Rulers" }.into(),
"view.globalRulers" => if app.session.active().is_some_and(|d| d.doc.rulers == vectorcraft_doc::RulerMode::Global) {
"Change to Artboard Rulers"
} else {
@@ -3054,7 +3068,14 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
c("Lock Slices", "view.slices.lock"),
c("Hide Template", "view.template"),
Sep,
sub("Rulers", vec![c("Show Rulers", "view.rulers"), c("Change to Global Rulers", "view.globalRulers"), todo("Show Video Rulers")]),
sub(
"Rulers",
vec![
c("Show Rulers", "view.rulers"),
c("Change to Global Rulers", "view.globalRulers"),
c("Show Video Rulers", "view.videoRulers"),
],
),
c("Hide Bounding Box", "view.boundingBox"),
c("Show Transparency Grid", "view.transparencyGrid"),
c("Hide Text Threads", "view.textThreads"),
@@ -4138,6 +4159,8 @@ pub const DYNAMIC_LABELS: &[&str] = &[
"Show Artboards",
"Hide Rulers",
"Show Rulers",
"Hide Video Rulers",
"Show Video Rulers",
"Change to Global Rulers",
"Change to Artboard Rulers",
"Hide Bounding Box",
+5
View File
@@ -214,6 +214,10 @@ pub const ICON_PANEL_GROUPS: &[&[&str]] = &[
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ViewFlags {
pub rulers: bool,
/// View › Rulers › Show Video Rulers: while the rulers show, they count the active artboard in
/// video pixels, honouring its Video Ruler Pixel Aspect Ratio.
#[serde(default)]
pub video_rulers: bool,
pub grid: bool,
pub guides: bool,
pub smart_guides: bool,
@@ -252,6 +256,7 @@ impl Default for ViewFlags {
fn default() -> Self {
Self {
rulers: false,
video_rulers: false,
grid: false,
guides: true,
smart_guides: true,
+35 -1
View File
@@ -1,7 +1,7 @@
//! The canvas draws Document Setup: the red bleed outline and the transparency grid in the
//! document's colours and cell size, also in a rotated view; a white Background Contents hides
//! the grid and Simulate Colored Paper tints the artboard. It also draws each artboard's Show
//! Center Mark and Show Cross Hairs (Artboard Options).
//! Center Mark, Show Cross Hairs and Show Video Safe Areas (Artboard Options).
use serde_json::json;
use vectorcraft_engine::Session;
@@ -189,6 +189,40 @@ fn an_artboard_shows_its_center_mark_and_cross_hairs() {
}
}
/// Show Video Safe Areas outlines the artboard's action-safe area 3.5% and its title-safe area 5%
/// in from each edge (EBU R 95), in the theme's artboard mark colour, 1 point wide: only on the
/// artboard that has it on, and turned with the view.
#[test]
fn an_artboard_shows_its_video_safe_areas() {
let mut app = app();
app.run("artboard.new", json!({})).unwrap();
let mark = Tokens::for_brightness(Default::default()).artboard_mark;
assert!(outlines(&frame(&mut app).0, mark).is_empty(), "off by default");
app.run("artboard.setProps", json!({"index": 0, "showVideoSafeAreas": true})).unwrap();
for rotation in [0.0, 30.0] {
app.view_mut().unwrap().rotation = rotation;
let (shapes, _, _) = frame(&mut app);
// Artboard 1's corners on screen: top-left, top-right, bottom-right, bottom-left (it is the
// active one, outlined in black).
let q = outlines(&shapes, egui::Color32::from_gray(0)).into_iter().next().unwrap();
let at = |u: f32, v: f32| q[0] + (q[1] - q[0]) * u + (q[3] - q[0]) * v;
let areas = outlines(&shapes, mark);
assert_eq!(areas.len(), 2, "two areas, on artboard 1 only: {areas:?}");
for (area, k) in areas.iter().zip([0.035, 0.05]) {
let want = [at(k, k), at(1.0 - k, k), at(1.0 - k, 1.0 - k), at(k, 1.0 - k)];
assert!(area.iter().zip(want).all(|(a, b)| (*a - b).length() < 0.01), "{rotation}°: {area:?}, want {want:?}");
}
}
let mut all = vec![];
flatten(&frame(&mut app).0, &mut all);
assert!(
all.iter().all(|s| !matches!(s, egui::Shape::Path(p) if p.stroke.color == egui::epaint::ColorMode::Solid(mark) && p.stroke.width != 1.0)),
"1 point wide"
);
app.run("artboard.setProps", json!({"index": 0, "showVideoSafeAreas": false})).unwrap();
assert!(outlines(&frame(&mut app).0, mark).is_empty(), "off again");
}
#[test]
fn white_background_hides_the_grid_and_paper_tints_the_artboard() {
let mut app = app();
+60
View File
@@ -284,3 +284,63 @@ fn change_to_global_rulers_menu_item_toggles_and_names_the_other_mode() {
let spec = vectorcraft_engine::command_specs().iter().find(|c| c.id == "view.globalRulers").unwrap();
assert_eq!(spec.shortcut, Some("Cmd+Alt+R"));
}
/// The top ruler's labels and where each starts on screen.
fn top_labels(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<(String, f32)> {
frame(app, ctx, vec![], canvas::show).into_iter().filter(|(_, r)| r.min.y < 4.0 && r.min.x > 16.0).map(|(t, r)| (t, r.min.x)).collect()
}
/// Where the top ruler's label `text` starts, if it shows.
fn label_x(labels: &[(String, f32)], text: &str) -> Option<f32> {
labels.iter().find(|(t, _)| t == text).map(|(_, x)| *x)
}
/// #2849: View › Rulers › Show Video Rulers shows the rulers counting the active artboard in video
/// pixels from its top-left corner, whatever the ruler origin, a pixel as wide as the artboard's
/// Video Ruler Pixel Aspect Ratio: at 0.5 the 400 pt artboard reads 800 at its right edge. The
/// item then reads Hide Video Rulers, which puts the rulers back in points from the ruler origin;
/// they follow the active artboard, and hiding the rulers names the item Show Video Rulers again.
#[test]
fn show_video_rulers_counts_the_active_artboard_in_pixels() {
let (mut app, _) = two_boards();
let ctx = context();
let tree = format!("{:?}", crate::menus::menu_tree().iter().find(|(title, _)| *title == "View").unwrap().1);
assert!(tree.contains("\"Show Video Rulers\", \"view.videoRulers\"") && !tree.contains("Todo(\"Show Video Rulers\""), "{tree}");
app.run("artboard.setProps", json!({"index": 0, "videoRulerPixelAspectRatio": 0.5})).unwrap();
app.run("document.setRulerOrigin", json!({"x": 100, "y": 50, "artboard": 0})).unwrap();
app.ui.view.rulers = false;
let label = |app: &VectorcraftApp| crate::menus::dynamic_label(app, "view.videoRulers", "Show Video Rulers");
assert_eq!(label(&app), "Show Video Rulers");
assert_eq!(app.run("view.videoRulers", json!({})).unwrap(), json!(true));
assert!(app.ui.view.rulers && app.ui.view.video_rulers, "the rulers show");
assert_eq!(label(&app), "Hide Video Rulers");
let at_100 = |app: &mut VectorcraftApp, center: Point| {
let v = app.view_mut().unwrap();
(v.zoom, v.center, v.fitted, v.rotation) = (1.0, center, true, 0.0);
};
at_100(&mut app, Point::new(200.0, 150.0));
// How far label `text` of `labels` starts right of `zero`, to within a hundredth of a point.
let from = |labels: &[(String, f32)], text: &str, zero: f32, want: f32| label_x(labels, text).is_some_and(|x| (x - zero - want).abs() < 0.01);
let video = top_labels(&mut app, &ctx);
let zero = label_x(&video, "0").unwrap();
assert!(from(&video, "800", zero, 400.0), "800 pixels at the right edge: {video:?}");
assert!(from(&video, "100", zero, 50.0), "{video:?}");
// Hide Video Rulers: points from the ruler origin, 100 pt in from the artboard's left edge.
assert_eq!(app.run("view.videoRulers", json!({})).unwrap(), json!(false));
assert!(app.ui.view.rulers && !app.ui.view.video_rulers, "the rulers stay");
assert_eq!(label(&app), "Show Video Rulers");
let points = top_labels(&mut app, &ctx);
assert!(from(&points, "0", zero, 100.0), "{points:?}");
assert!(from(&points, "300", zero, 400.0), "{points:?}");
// The second artboard (square pixels, 200 pt wide at x = 700) once it is the active one.
app.run("view.videoRulers", json!({})).unwrap();
app.view_mut().unwrap().artboard = 1;
at_100(&mut app, Point::new(800.0, -175.0));
let second = top_labels(&mut app, &ctx);
let zero = label_x(&second, "0").unwrap();
assert!(from(&second, "200", zero, 200.0), "{second:?}");
// Hidden with Show Rulers, the item offers to show them again.
app.run("view.rulers", json!({})).unwrap();
assert_eq!(label(&app), "Show Video Rulers");
assert!(top_labels(&mut app, &ctx).is_empty());
}
+6 -2
View File
File diff suppressed because one or more lines are too long
+17 -1
View File
@@ -1203,6 +1203,12 @@ the document's (global rulers) or that artboard's (artboard rulers, kept relativ
{"name":"run_command","arguments":{"command":"view.globalRulers","params":{"global":true}}}
```
`view.videoRulers {}` (View → Rulers → Show / Hide Video Rulers) is a UI view flag, not saved with the document and not
undoable. Show Video Rulers shows the rulers and has them count the active artboard in video pixels from its top-left
corner, whatever the ruler origin: a pixel is as many points wide as the artboard's `videoRulerPixelAspectRatio`
(`artboard.setProps`) and one point tall, so a 720 × 480 pt artboard at 0.9 reads 800 × 480. Hide Video Rulers puts the
rulers back in the General unit from the ruler origin; they stay shown. It returns whether video rulers show.
## Clipping masks
`object.clippingMask.make` clips the selected objects by the topmost one, which may be a path, a compound path or a
@@ -3020,7 +3026,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?, x?, y?, width?, height?}…], scaleArt?, moveArt?, …}` sets several
`artboard.setProps {artboards: [{index, name?, color?, 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
@@ -3079,6 +3085,16 @@ hairs as lines from the middle of each side through the center, 1 point wide on
leave them out. Duplicates, copies and the native format keep them, and `inspect_document` adds `showCenterMark: true`
and `showCrossHairs: true` to an artboard that shows them.
Artboard Options' video display options: `artboard.setProps {index, showVideoSafeAreas?: bool,
videoRulerPixelAspectRatio?: number}` (or each entry of `artboards`) sets them for that artboard in one undo step, and
`artboard.new` takes them too. Show Video Safe Areas outlines the artboard's action-safe area 3.5% and its title-safe
area 5% in from each edge, as EBU R 95 sets them (https://tech.ebu.ch/docs/r/r095.pdf), 1 point wide on screen and only
on the canvas. The Video Ruler Pixel Aspect Ratio, from 0.1 to 10 (default 1, square pixels; the commands refuse anything
else, and a file's outside the range opens brought into it),
is the width of one video pixel over its height, which `view.videoRulers` counts the artboard in. Duplicates, copies and
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.
```json
{"name":"run_command","arguments":{"command":"artboard.copy","params":{"index":0}}}
{"name":"run_command","arguments":{"command":"edit.paste","params":{}}}
File diff suppressed because one or more lines are too long
+5 -2
View File
File diff suppressed because one or more lines are too long