mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 21:44:57 +00:00
Merge main into fix/export-bakes-symbol-art-effects
This commit is contained in:
@@ -3255,7 +3255,8 @@ fn images_are_written_again_as_the_same_bytes() {
|
||||
|
||||
/// The program of `doc` with LF line ends, and each image's samples (from after `XI` and its line
|
||||
/// end to `%%EndData`, a `%_` before it kept) and each preview's rows (from after `32 17 ` to the
|
||||
/// line end before `%AI26_EndPlacedObjectPreview`) shown as `<n bytes>`.
|
||||
/// line end before `%AI26_EndPlacedObjectPreview` or `%_%AI26_EndPlacedObjectPreview`) shown as
|
||||
/// `<n bytes>`.
|
||||
fn program_shown(doc: &Document) -> String {
|
||||
shown(&export_ai_editing(doc, &opts()).unwrap().program)
|
||||
}
|
||||
@@ -3267,10 +3268,13 @@ fn shown(p: &[u8]) -> String {
|
||||
let mut at = 0;
|
||||
loop {
|
||||
let xi = find(at, b"\r\nXI\r\n").map(|i| (i + 6, b"%%EndData".as_slice()));
|
||||
let pv = find(at, b" 32 17 ").map(|i| (i + 7, b"\r\n%AI26_EndPlacedObjectPreview".as_slice()));
|
||||
let pv = find(at, b" 32 17 ").map(|i| (i + 7, b"%AI26_EndPlacedObjectPreview".as_slice()));
|
||||
let Some((start, end)) = [xi, pv].into_iter().flatten().min_by_key(|(s, _)| *s) else { break };
|
||||
let mut stop = find(start, end).unwrap();
|
||||
if end.starts_with(b"%%") && p.get(stop - 2..stop) == Some(b"%_") {
|
||||
if p.get(stop - 2..stop) == Some(b"%_") {
|
||||
stop -= 2;
|
||||
}
|
||||
if end.starts_with(b"%AI26") && p.get(stop - 2..stop) == Some(b"\r\n") {
|
||||
stop -= 2;
|
||||
}
|
||||
out.extend_from_slice(&p[at..start]);
|
||||
@@ -7354,18 +7358,147 @@ fn an_image_on_hidden_lines_does_not_come_in_beside_its_look() {
|
||||
assert_eq!(art[0].name.as_deref(), Some("Photo"));
|
||||
}
|
||||
|
||||
/// A linked image, an image with an opacity mask, and an image with a Warp and another effect are
|
||||
/// written as their drawn art, with no art style.
|
||||
/// [`warped_photo`] linked to `/Users/someone/Job/art/photo.png`, in folder `art` next to the
|
||||
/// document's.
|
||||
fn linked_warped_photo(doc: &mut Document, effects: Vec<vectorcraft_doc::Effect>) -> Node {
|
||||
let mut n = warped_photo(doc, effects);
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
let mut link = vectorcraft_doc::LinkInfo::new("/Users/someone/Job/art/photo.png");
|
||||
link.relative = Some("art/photo.png".into());
|
||||
im.link = Some(link);
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// A linked image with one Warp is written with an art style of that Warp, its bent pieces as its
|
||||
/// drawn look and the image itself as a placed file on `%_` lines, with its path, folder and
|
||||
/// preview: it reads back as the linked image, its pixels, placement, name, opacity and link as
|
||||
/// they were, with its Warp live and its settings kept.
|
||||
#[test]
|
||||
fn a_linked_image_with_a_warp_reads_back_linked_with_its_warp_live() {
|
||||
// GUARDS: a linked image with a Warp was written as its drawn pieces alone, and read back
|
||||
// without its Warp or its link.
|
||||
let params = serde_json::json!({"bend": 40.0});
|
||||
let doc = photo_doc(|d| linked_warped_photo(d, vec![fx("warp.arc", params.clone())]));
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
|
||||
let text = program_shown(&doc);
|
||||
assert!(text.contains("\n%_%AI5_BeginPlace\n"), "{text}");
|
||||
assert!(text.contains("\n%_(/Users/someone/Job/art/photo.png)`\n"), "{text}");
|
||||
assert!(text.contains("\n%_(art/)\n"), "{text}");
|
||||
assert!(text.contains("\n%_8 8 32 32 17 <256 bytes>\n"), "{text}");
|
||||
assert!(text.contains("\n%_%AI5_EndPlace\n") && text.contains("\n1 (Anon 1) XW\n"), "{text}");
|
||||
assert!(!text.contains("BeginRaster\n%_"), "no embedded copy: {text}");
|
||||
let back = read_back(&doc);
|
||||
let art = back.layers[0].children().unwrap();
|
||||
assert_eq!(art.len(), 1, "{:#?}", lines(&back));
|
||||
let (mut want, mut got) = (vec![], vec![]);
|
||||
pictures(&doc, doc.layers[0].children().unwrap(), "", &mut want);
|
||||
pictures(&back, art, "", &mut got);
|
||||
assert_eq!(got, want);
|
||||
let [warp] = art[0].appearance.effects.as_slice() else { panic!("{:?}", art[0].appearance) };
|
||||
assert_eq!(warp.id, "warp.arc");
|
||||
let (w, g) = (vectorcraft_effects::merged_params("warp.arc", ¶ms), vectorcraft_effects::merged_params(&warp.id, &warp.params));
|
||||
for key in ["bend", "horizontal", "vertical"] {
|
||||
assert!((g[key].as_f64().unwrap() - w[key].as_f64().unwrap()).abs() < 6e-3, "{key}: {g} for {w}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A linked image with one Warp larger than a placed file's preview: its copy's preview and its drawn
|
||||
/// look's pieces (placed files of the same image) hold the same smaller preview, so fine detail
|
||||
/// reads back with the Warp live, the link kept and the preview's pixels, as a linked image without
|
||||
/// a Warp does.
|
||||
#[test]
|
||||
fn a_large_linked_image_with_a_warp_reads_back_from_its_preview() {
|
||||
// GUARDS: a linked image with a Warp was written as its drawn pieces alone.
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let checks =
|
||||
image::RgbaImage::from_fn(600, 300, |x, y| if (x + y) % 2 == 0 { image::Rgba([255, 0, 0, 255]) } else { image::Rgba([0, 0, 255, 255]) });
|
||||
let mut link = vectorcraft_doc::LinkInfo::new("/Users/someone/Job/art/checks.png");
|
||||
link.relative = Some("art/checks.png".into());
|
||||
let xf = Affine::new([0.25, 0.0, 0.0, 0.25, 20.0, 10.0]);
|
||||
let mut n = image_node(&mut doc, ImageBlob::png(png_of(checks.into())), (600, 300), xf, Some(link));
|
||||
n.appearance = Appearance { effects: vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))], ..Appearance::default() }.into();
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
|
||||
let text = program_shown(&doc);
|
||||
assert!(text.contains("\n%_256 128 1024 32 17 <131072 bytes>\n"), "the copy's preview");
|
||||
assert!(text.contains("\n256 128 1024 32 17 <131072 bytes>\n"), "the look's pieces' previews");
|
||||
let back = read_back(&doc);
|
||||
let art = back.layers[0].children().unwrap();
|
||||
assert_eq!(art.len(), 1, "{:#?}", lines(&back));
|
||||
let NodeKind::Image(im) = &art[0].kind else { panic!("{:?}", art[0].kind) };
|
||||
assert_eq!((im.width, im.height), (256, 128));
|
||||
assert_eq!(
|
||||
im.link.as_ref().map(|l| (l.path.as_str(), l.relative.as_deref())),
|
||||
Some(("/Users/someone/Job/art/checks.png", Some("art/checks.png")))
|
||||
);
|
||||
assert_eq!(art[0].appearance.effects.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), ["warp.arc"]);
|
||||
}
|
||||
|
||||
/// A hidden or locked image with one Warp, embedded or linked, keeps its state, its link and its
|
||||
/// live Warp.
|
||||
#[test]
|
||||
fn a_hidden_or_locked_image_with_a_warp_keeps_its_state() {
|
||||
// GUARDS: a linked image with a Warp was written as its drawn pieces alone, and a hidden image's
|
||||
// Warp was compared with a look that drew nothing, so it came in as its drawn pieces.
|
||||
for (what, make) in [("embedded", warped_photo as fn(&mut Document, Vec<vectorcraft_doc::Effect>) -> Node), ("linked", linked_warped_photo)] {
|
||||
for (shown, locked) in [(true, true), (false, false), (false, true)] {
|
||||
let doc = photo_doc(|d| {
|
||||
let mut n = make(d, vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))]);
|
||||
(n.visible, n.locked) = (shown, locked);
|
||||
n
|
||||
});
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert!(out.warnings.is_empty(), "{what} {shown} {locked}: {:?}", out.warnings);
|
||||
let back = read_back(&doc);
|
||||
let art = back.layers[0].children().unwrap();
|
||||
assert_eq!(art.len(), 1, "{what} {shown} {locked}: {:#?}", lines(&back));
|
||||
assert_eq!(images_of(&back).len(), 1, "{what} {shown} {locked}");
|
||||
assert_eq!(images_of(&back), images_of(&doc), "{what} {shown} {locked}");
|
||||
assert_eq!(art[0].appearance.effects.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), ["warp.arc"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A linked image with one Warp in an opacity mask's art keeps its live Warp; its copy on `%_` lines
|
||||
/// is embedded, as linked images in masks are, with that warning.
|
||||
#[test]
|
||||
fn a_linked_image_with_a_warp_in_a_mask_is_embedded_with_its_warp_live() {
|
||||
// GUARDS: the placed file of a styled image written in an opacity mask's art.
|
||||
let doc = photo_doc(|d| {
|
||||
let art = linked_warped_photo(d, vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))]);
|
||||
let mut square =
|
||||
Node::path(d.alloc_id(), shapes::rectangle(Rect::new(20.0, 10.0, 100.0, 90.0)), Appearance::basic(solid(Color::BLACK), Paint::None, 0.0));
|
||||
square.extras_mut().mask = Some(Box::new(OpacityMask::new(art, false)));
|
||||
square
|
||||
});
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert_eq!(out.warnings, [writer::LINKS_EMBEDDED]);
|
||||
let text = program_shown(&doc);
|
||||
assert!(!text.contains("BeginPlace") && text.contains("1 (Anon 1) XW"), "{text}");
|
||||
let back = read_back(&doc);
|
||||
let mask = back.layers[0].children().unwrap()[0].extras().mask.as_ref().map(|m| m.art.clone()).expect("the mask");
|
||||
let NodeKind::Image(im) = &mask.kind else { panic!("{:?}", mask.kind) };
|
||||
assert!(im.link.is_none());
|
||||
assert_eq!(mask.appearance.effects.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), ["warp.arc"]);
|
||||
}
|
||||
|
||||
/// An image with an opacity mask and an image with a Warp and another effect are written as their
|
||||
/// drawn art, with no art style, linked or embedded.
|
||||
#[test]
|
||||
fn other_warped_images_are_written_as_their_drawn_art() {
|
||||
let arc = || fx("warp.arc", serde_json::json!({"bend": 40.0}));
|
||||
let linked = photo_doc(|d| {
|
||||
let mut n = warped_photo(d, vec![arc()]);
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
im.link = Some(vectorcraft_doc::LinkInfo::new("/art/photo.png"));
|
||||
}
|
||||
let linked_masked = photo_doc(|d| {
|
||||
let mut n = linked_warped_photo(d, vec![arc()]);
|
||||
let white =
|
||||
Node::path(d.alloc_id(), shapes::rectangle(Rect::new(20.0, 10.0, 100.0, 90.0)), Appearance::basic(solid(Color::WHITE), Paint::None, 0.0));
|
||||
n.extras_mut().mask = Some(Box::new(OpacityMask::new(white, false)));
|
||||
n
|
||||
});
|
||||
let linked_stacked = photo_doc(|d| linked_warped_photo(d, vec![arc(), fx("stylize.feather", serde_json::json!({"radius": 2.0}))]));
|
||||
let masked = photo_doc(|d| {
|
||||
let mut n = warped_photo(d, vec![arc()]);
|
||||
let white =
|
||||
@@ -7374,7 +7507,7 @@ fn other_warped_images_are_written_as_their_drawn_art() {
|
||||
n
|
||||
});
|
||||
let stacked = photo_doc(|d| warped_photo(d, vec![arc(), fx("stylize.feather", serde_json::json!({"radius": 2.0}))]));
|
||||
for (what, doc) in [("linked", linked), ("masked", masked), ("stacked", stacked)] {
|
||||
for (what, doc) in [("linked masked", linked_masked), ("linked stacked", linked_stacked), ("masked", masked), ("stacked", stacked)] {
|
||||
assert!(!program_shown(&doc).contains("1 (Anon"), "{what}");
|
||||
let back = read_back(&doc);
|
||||
let art = back.layers[0].children().unwrap();
|
||||
|
||||
@@ -206,6 +206,9 @@ pub(super) struct Writer<'a> {
|
||||
mask_level: Option<(u32, u32)>,
|
||||
/// The image just written was written as a placed file.
|
||||
placed: bool,
|
||||
/// The image being written is the copy of a styled object on the layers, on `%_` lines: linked,
|
||||
/// it is written as a placed file there ([`Self::styled_object`]).
|
||||
styled_copy: bool,
|
||||
/// The type just written was written as type, not as its outlines.
|
||||
typed: bool,
|
||||
/// The objects bound to variables so far, in the order they are written: each one's id, its XML
|
||||
@@ -299,6 +302,7 @@ impl<'a> Writer<'a> {
|
||||
hidden: 0,
|
||||
mask_level: None,
|
||||
placed: false,
|
||||
styled_copy: false,
|
||||
typed: false,
|
||||
bound: vec![],
|
||||
texts: texts::TextDoc::default(),
|
||||
|
||||
@@ -94,7 +94,8 @@ impl Samples {
|
||||
impl Writer<'_> {
|
||||
/// Image object `im` of transparency `xy`: embedded, a raster ([`Self::raster`]); linked, a
|
||||
/// placed file with a preview ([`Self::placed_file`]), except in a part of the data written on
|
||||
/// `%_` lines and in a symbol's art. One whose pixels can't be read is its outline filled mid gray. Where the art is
|
||||
/// `%_` lines (other than a styled object's copy on the layers, [`Self::styled_object`]) and in
|
||||
/// a symbol's art. One whose pixels can't be read is its outline filled mid gray. Where the art is
|
||||
/// cut to a clipping path, the image is in a clipping group of that path.
|
||||
pub(super) fn image(&mut self, im: &ImageObject, xy: Xy) {
|
||||
let doc = self.doc;
|
||||
@@ -125,7 +126,7 @@ impl Writer<'_> {
|
||||
let fit =
|
||||
Affine::scale_non_uniform(f64::from(im.width.max(1)) / f64::from(samples.width), f64::from(im.height.max(1)) / f64::from(samples.height));
|
||||
match &im.link {
|
||||
Some(link) if self.hidden == 0 && !self.in_symbol => {
|
||||
Some(link) if (self.hidden == 0 || self.styled_copy) && !self.in_symbol => {
|
||||
// Cut to a region, the placed file is in a clipping group.
|
||||
self.placed = cut.is_none();
|
||||
self.placed_file(link, &samples, (im.width.max(1), im.height.max(1)), im.xf);
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(in crate::ai_editing) fn style_links(doc: &Document) -> usize {
|
||||
/// The objects of `doc`'s layers and of their opacity masks' art that may be written with an art
|
||||
/// style ([`Writer::styled_object`]), by id (a mask's art root shown, as [`Writer::mask`] writes
|
||||
/// it): paths and compound paths with points and groups, with live effects, linked to a graphic
|
||||
/// style or (paths and compound paths) with a brush on a stroke, and embedded images with one Warp
|
||||
/// style or (paths and compound paths) with a brush on a stroke, and images with one Warp
|
||||
/// ([`warped_image`]), without Opacity & Mask Define
|
||||
/// Knockout Shape (the reader takes an object's transparency
|
||||
/// from its look, which doesn't carry it). A group's members are paths, compound paths or such
|
||||
@@ -100,12 +100,11 @@ fn candidate(n: &Node) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `n` an embedded image without an opacity mask whose appearance is one Warp, with no fills or
|
||||
/// strokes that paint and no effects on them: an image the reader reads with its Warp live
|
||||
/// ([`crate::import::read_art_style`]'s `image_warp`)?
|
||||
/// Is `n` an image, embedded or linked, without an opacity mask whose appearance is one Warp, with
|
||||
/// no fills or strokes that paint and no effects on them: an image the reader reads with its Warp
|
||||
/// live ([`crate::import::read_art_style`]'s `image_warp`)?
|
||||
fn warped_image(n: &Node) -> bool {
|
||||
let NodeKind::Image(im) = &n.kind else { return false };
|
||||
im.link.is_none()
|
||||
matches!(n.kind, NodeKind::Image(_))
|
||||
&& n.extras().mask.is_none()
|
||||
&& matches!(n.appearance.effects.as_slice(), [e] if e.id.starts_with("warp."))
|
||||
&& n.appearance.items.iter().all(|i| i.paint().is_none() && i.effects().is_empty())
|
||||
@@ -725,7 +724,8 @@ impl Writer<'_> {
|
||||
/// glows or feathers, the object's transparency
|
||||
/// after the group's end, `0 0 Xd`, `6 () XW`); the object itself on `%_` lines (a path's or
|
||||
/// compound path's points painted with nothing, or with its gradient fill when its style has
|
||||
/// one; a group with its members; an image with its pixels), its name and opacity mask; then
|
||||
/// one; a group with its members; an image with its pixels, or on the layers a linked image as
|
||||
/// a placed file with its path, folder and preview), its name and opacity mask; then
|
||||
/// `1 (name) XW`.
|
||||
/// The look's group and the object itself are counted first ([`Self::take_objects`]): without
|
||||
/// room for them, nothing is written, and the object counts neither as written with its graphic
|
||||
@@ -772,6 +772,9 @@ impl Writer<'_> {
|
||||
// The object, on `%_` lines, in its own state (the look's members may have changed it).
|
||||
self.state(orig.locked, !orig.visible);
|
||||
let saved = std::mem::take(&mut self.body);
|
||||
// On the layers, a linked image's copy is a placed file; in an opacity mask's art, it is
|
||||
// embedded, as other linked images there are.
|
||||
let on_layers = self.hidden == 0;
|
||||
self.hidden += 1;
|
||||
let fit = drawn_outline(n, orig).map(|p| p.bounding_box());
|
||||
// A path's or an image's objects were kept for it above; it counts them itself.
|
||||
@@ -781,7 +784,11 @@ impl Writer<'_> {
|
||||
match &orig.kind {
|
||||
NodeKind::Path { path, rule, .. } => self.styled_outline(orig, path, *rule, fit, xy),
|
||||
NodeKind::Compound { rule, .. } => self.styled_outline(orig, &compound_path(orig), *rule, fit, xy),
|
||||
NodeKind::Image(im) => self.image(&self.doc.shown_image(orig, im), xy),
|
||||
NodeKind::Image(im) => {
|
||||
let was = std::mem::replace(&mut self.styled_copy, on_layers);
|
||||
self.image(&self.doc.shown_image(orig, im), xy);
|
||||
self.styled_copy = was;
|
||||
}
|
||||
NodeKind::Group { children, .. } => {
|
||||
self.line("u");
|
||||
for c in children {
|
||||
|
||||
@@ -1141,7 +1141,8 @@ fn image_warps(object: &Node, st: &crate::import::ai::style::ArtStyle) -> bool {
|
||||
|
||||
/// Does image `object` with `appearance` (its style's Warp) draw as `look`, its drawn look, compared
|
||||
/// over all that the warp draws ([`crate::import::ai::envelope::warped_alike`]) on `layer`, a layer
|
||||
/// id no other node has? Both are drawn shown, opaque and in the Normal mode. Unless the look is on
|
||||
/// id no other node has? Both are drawn shown, opaque and in the Normal mode; a hidden look is drawn
|
||||
/// with every object in it shown, as the hidden object's look holds them. Unless the look is on
|
||||
/// `%_` lines itself (`hidden`), the copies in it that are on `%_` lines (`hidden_ids`) are left out.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn image_warp_alike(
|
||||
@@ -1161,12 +1162,24 @@ fn image_warp_alike(
|
||||
if !hidden {
|
||||
strip(&mut plain, hidden_ids);
|
||||
}
|
||||
if !plain.visible {
|
||||
show_members(&mut plain);
|
||||
}
|
||||
for n in [&mut live, &mut plain] {
|
||||
(n.visible, n.opacity, n.blend) = (true, 1.0, BlendMode::Normal);
|
||||
}
|
||||
crate::import::ai::envelope::warped_alike(&live, &plain, layer, shared, page, spent)
|
||||
}
|
||||
|
||||
/// Show every object in container `n`, at any depth.
|
||||
fn show_members(n: &mut Node) {
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
let c = Arc::make_mut(c);
|
||||
c.visible = true;
|
||||
show_members(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`placed_paints`] keys a pattern's name with, apart from the gradients' names.
|
||||
const PATTERN_KEY: &str = "\u{1}pattern ";
|
||||
|
||||
|
||||
@@ -1465,7 +1465,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"ui.action.new",
|
||||
"New Action",
|
||||
"",
|
||||
"{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), which the panel does not show. Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
|
||||
"{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), the color of its button in Button Mode (ui.action.buttonMode). Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
|
||||
),
|
||||
(
|
||||
"ui.action.options",
|
||||
@@ -1479,6 +1479,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{action: name, set?: name} play the Actions panel's action named action at the speed ui.action.playbackOptions sets; set names the set to look in (default: every set). Accelerated plays it as one undoable batch → what command.batch returns. Step by Step and Pause For play its first step now and the others one at a time, each its own undo step, until the last, ui.action.stop, or a step that comes due while a different document is active than after the step before → {results: [the first step's result], pending: the steps left}. An action with a stop, at any speed, plays up to the stop and waits until ui.action.continue or ui.action.stop; at Accelerated the steps between stops are one undoable batch each → {results, pending, atStop?: true when it already waits}. A name no action has, or one that more than one action in the sets looked in has, or an action played while another is still playing step by step or with pauses, is an error and nothing plays",
|
||||
),
|
||||
(
|
||||
"ui.action.playAt",
|
||||
"Play Action",
|
||||
"",
|
||||
"{set: index, action: index} play the Actions panel's action at action in set (0 is the top set) as ui.action.play plays it, and return what ui.action.play returns. A click on an action's button in Button Mode runs this. A missing index, or one that is not a whole number or is past the last set or action, is an error and nothing plays",
|
||||
),
|
||||
(
|
||||
"ui.action.buttonMode",
|
||||
"Button Mode",
|
||||
"",
|
||||
"{enabled?: bool} with true, the Actions panel shows every action of every set, in set order and then action order, as a button with its name in its Action Options color (no color: an ordinary button), without set names, steps or checkmarks, and a click on a button plays its action (ui.action.playAt); with false, it shows its list again; left out or null, it switches to the other view. The panel menu's Button Mode runs this and shows a check mark while it is on. The view is kept with the UI preferences. Turning it on while an action records, or an enabled that is not true, false or null, is an error and the view stays → {enabled}",
|
||||
),
|
||||
(
|
||||
"ui.action.playbackOptions",
|
||||
"Playback Options",
|
||||
|
||||
@@ -719,7 +719,8 @@ pub fn confirm(app: &mut VectorcraftApp) -> DialogResult {
|
||||
/// After a dialog closes, and each frame: a Missing Fonts dialog that another dialog replaced waits
|
||||
/// for its turn again, the search a Missing Fonts or Find Linked Files dialog started stops once
|
||||
/// that dialog is gone, and with no dialog open the choice about a stopped plug-in that another
|
||||
/// dialog replaced opens again, else the next document's Missing Fonts dialog (one dialog at a
|
||||
/// dialog replaced opens again, else the questions about the next restored document's linked files
|
||||
/// ([`crate::recovery::ask_next`]), else the next document's Missing Fonts dialog (one dialog at a
|
||||
/// time).
|
||||
pub(crate) fn settle(app: &mut VectorcraftApp) {
|
||||
crate::placing::settle_swatch_paste(app);
|
||||
@@ -727,6 +728,7 @@ pub(crate) fn settle(app: &mut VectorcraftApp) {
|
||||
missing_fonts::stop_when_closed(app);
|
||||
find_links::stop_when_closed(app);
|
||||
safe_mode::reopen(app);
|
||||
crate::recovery::ask_next(app);
|
||||
missing_fonts::open_next(app);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,14 +54,7 @@ fn after_open_for(app: &mut VectorcraftApp, r: &Value, notes: Vec<String>, web:
|
||||
let rows = |k: &str| r[k].as_array().cloned().unwrap_or_default();
|
||||
let (missing, modified) = (rows("missingLinks"), ids(&rows("modifiedLinks")));
|
||||
let notice = if missing.is_empty() || web {
|
||||
let unreadable = (!missing.is_empty()).then(|| {
|
||||
crate::i18n::msg_n(
|
||||
missing.len() as u64,
|
||||
"{n} linked file can't be read here: its preview shows",
|
||||
"{n} linked files can't be read here: their previews show",
|
||||
&[],
|
||||
)
|
||||
});
|
||||
let unreadable = (!missing.is_empty()).then(|| unreadable_status(missing.len()));
|
||||
ask_update(app, modified).or(unreadable)
|
||||
} else {
|
||||
open(app, missing, modified, ¬es);
|
||||
@@ -71,6 +64,40 @@ fn after_open_for(app: &mut VectorcraftApp, r: &Value, notes: Vec<String>, web:
|
||||
super::settle(app);
|
||||
}
|
||||
|
||||
/// The status bar's line for `n` linked files the web can't read.
|
||||
fn unreadable_status(n: usize) -> String {
|
||||
crate::i18n::msg_n(
|
||||
n as u64,
|
||||
"{n} linked file can't be read here: its preview shows",
|
||||
"{n} linked files can't be read here: their previews show",
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
/// What the linked files of a document that opened (`document.open`'s result `r`) need, without
|
||||
/// asking yet → the status bar's lines about them, the missing files to ask about and the ids of
|
||||
/// the modified images to offer for update ([`ask_next`]). The web reads no linked files, so the
|
||||
/// missing ones get a line there; without Update Links: Ask When Modified the modified ones get a
|
||||
/// line instead of the offer.
|
||||
pub(crate) fn questions(app: &VectorcraftApp, r: &Value) -> (Vec<String>, Vec<Value>, Vec<Value>) {
|
||||
questions_for(app, r, cfg!(target_arch = "wasm32"))
|
||||
}
|
||||
|
||||
fn questions_for(app: &VectorcraftApp, r: &Value, web: bool) -> (Vec<String>, Vec<Value>, Vec<Value>) {
|
||||
let rows = |k: &str| r[k].as_array().cloned().unwrap_or_default();
|
||||
let (mut missing, mut modified) = (rows("missingLinks"), ids(&rows("modifiedLinks")));
|
||||
let mut lines = vec![];
|
||||
if web && !missing.is_empty() {
|
||||
lines.push(unreadable_status(missing.len()));
|
||||
missing.clear();
|
||||
}
|
||||
if !modified.is_empty() && app.session.prefs.update_links != "askWhenModified" {
|
||||
lines.push(changed_on_disk_status(modified.len()));
|
||||
modified.clear();
|
||||
}
|
||||
(lines, missing, modified)
|
||||
}
|
||||
|
||||
fn open(app: &mut VectorcraftApp, missing: Vec<Value>, modified: Vec<Value>, notes: &[String]) {
|
||||
let fields = json!({ "missing": missing, "modified": modified, "notes": notes, "applyToAll": false, "path": "" });
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
@@ -85,7 +112,7 @@ pub(super) fn notes(d: &Dialog) -> Vec<String> {
|
||||
/// Ask about the missing files `rest`, one at a time, then offer the modified ones `modified` for
|
||||
/// update. A line about the modified files is followed by the open's `notes`
|
||||
/// ([`crate::io::show_opened`]).
|
||||
pub(super) fn ask_next(app: &mut VectorcraftApp, rest: Vec<Value>, modified: Vec<Value>, notes: Vec<String>) {
|
||||
pub(crate) fn ask_next(app: &mut VectorcraftApp, rest: Vec<Value>, modified: Vec<Value>, notes: Vec<String>) {
|
||||
if rest.is_empty() {
|
||||
app.ui.dialog = None;
|
||||
if let Some(notice) = ask_update(app, modified) {
|
||||
@@ -309,6 +336,23 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A restored document's linked files: the web gets a line about the missing ones, and
|
||||
/// without Update Links: Ask When Modified the modified ones get a line; the rest are asked
|
||||
/// about.
|
||||
#[test]
|
||||
fn the_questions_about_linked_files_wait_or_become_lines() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let r = json!({ "missingLinks": [{"name": "a.png", "path": "/x/a.png", "ids": [1]}], "modifiedLinks": [{"ids": [2]}, {"ids": [3]}] });
|
||||
app.session.prefs.update_links = "askWhenModified".into();
|
||||
assert_eq!(questions_for(&app, &r, false), (vec![], r["missingLinks"].as_array().unwrap().clone(), vec![json!(2), json!(3)]));
|
||||
let changed = "2 linked images changed on disk: Update Links shows the new versions".to_string();
|
||||
app.session.prefs.update_links = "manually".into();
|
||||
assert_eq!(questions_for(&app, &r, false), (vec![changed.clone()], r["missingLinks"].as_array().unwrap().clone(), vec![]));
|
||||
let unreadable = "1 linked file can't be read here: its preview shows".to_string();
|
||||
assert_eq!(questions_for(&app, &r, true), (vec![unreadable, changed], vec![], vec![]));
|
||||
assert!(app.ui.dialog.is_none(), "nothing is asked yet");
|
||||
}
|
||||
|
||||
/// The question about modified linked files uses the form the language's plural rule picks for
|
||||
/// the count.
|
||||
#[test]
|
||||
|
||||
@@ -4702,11 +4702,11 @@ axis Both И двете
|
||||
@msg unknown effect `{id}` непознат ефект `{id}`
|
||||
@msg unknown format непознат формат
|
||||
@msg unknown format `{f}` (Save writes {_1}) непознат формат `{f}` (Запазването записва {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) непознат режим на свободна форма `{m}` (точки, линии)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) непознат вид градиент `{k}` (линеен, радиален, конусен, ромбовиден, свободна форма)
|
||||
@msg unknown freeform mode `{m}` (points, lines) непознат режим на свободна форма `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) непознат вид градиент `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` непознат акцент `{h}`
|
||||
@msg unknown option `{path}.{k}` непозната опция `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) непознати надписи `{o}` (запазване, симулиране или отхвърляне)
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) непознати надписи `{o}` (preserve, simulate или discard)
|
||||
@msg unknown panel `{raw}` непознат панел `{raw}`
|
||||
@msg unknown preference `{key}` непозната настройка `{key}`
|
||||
@msg Preferences imported. Настройките са импортирани.
|
||||
@@ -4724,7 +4724,7 @@ axis Both И двете
|
||||
@msg `{key}` must be an object `{key}` трябва да бъде обект
|
||||
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` се изпълнява самостоятелно, а не като стъпка от пакет
|
||||
@msg unknown preset (one of {_1}) непозната предварителна настройка (една от {_1})
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) непозната предварителна настройка `{name}` (висока, средна, ниска или запазена: вижте flattener.presets.list)
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) непозната предварителна настройка `{name}` (high, medium, low или запазена: вижте flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see flattener.presets.list) непозната предварителна настройка `{name}` (вижте flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see webExport.presets.list) непозната предварителна настройка `{name}` (вижте webExport.presets.list)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) непозната страна `{other}` (left|right|top|bottom)
|
||||
@@ -6058,6 +6058,8 @@ axis Y Axis Y ос
|
||||
Action Options… Опции на действието…
|
||||
Action Options Опции на действието
|
||||
Playback Options… Опции за възпроизвеждане…
|
||||
Button Mode Режим с бутони
|
||||
No actions. Няма действия.
|
||||
Insert Stop… Вмъкване на спиране…
|
||||
Insert Stop Вмъкване на спиране
|
||||
Record Stop Записване на спиране
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6100,6 +6100,8 @@ axis Y Axis Y osa
|
||||
Action Options… Opcije akcije…
|
||||
Action Options Opcije akcije
|
||||
Playback Options… Opcije reprodukcije…
|
||||
Button Mode Način dugmadi
|
||||
No actions. Nema akcija.
|
||||
Insert Stop… Umetni zaustavljanje…
|
||||
Insert Stop Umetni zaustavljanje
|
||||
Record Stop Snimi zaustavljanje
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4095,6 +4095,8 @@ digits Malayalam Malaiàlam
|
||||
Action Options… Opcions de l’acció…
|
||||
Action Options Opcions de l’acció
|
||||
Playback Options… Opcions de reproducció…
|
||||
Button Mode Mode de botons
|
||||
No actions. No hi ha accions.
|
||||
Insert Stop… Insereix una aturada…
|
||||
Insert Stop Insereix una aturada
|
||||
Record Stop Enregistra una aturada
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6601,6 +6601,8 @@ axis Y Axis osa Y
|
||||
Action Options… Možnosti akce…
|
||||
Action Options Možnosti akce
|
||||
Playback Options… Možnosti přehrávání…
|
||||
Button Mode Režim tlačítek
|
||||
No actions. Žádné akce.
|
||||
Insert Stop… Vložit zastavení…
|
||||
Insert Stop Vložit zastavení
|
||||
Record Stop Zaznamenat zastavení
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4796,11 +4796,11 @@ imageTrace Outline Kontur
|
||||
@msg unknown effect `{id}` ukendt effekt `{id}`
|
||||
@msg unknown format ukendt format
|
||||
@msg unknown format `{f}` (Save writes {_1}) ukendt format `{f}` (Gem skriver {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) ukendt freeform-tilstand `{m}` (punkter, linjer)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) ukendt gradienttype `{k}` (lineær, radial, konisk, diamant, freeform)
|
||||
@msg unknown freeform mode `{m}` (points, lines) ukendt freeform-tilstand `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) ukendt gradienttype `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` ukendt fremhævning `{h}`
|
||||
@msg unknown option `{path}.{k}` ukendt indstilling `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) ukendt overprint `{o}` (bevar, simulér eller forkast)
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) ukendt overprint `{o}` (preserve, simulate eller discard)
|
||||
@msg unknown panel `{raw}` ukendt panel `{raw}`
|
||||
@msg unknown preference `{key}` ukendt præference `{key}`
|
||||
@msg Preferences imported. Præferencerne er importeret.
|
||||
@@ -4818,10 +4818,10 @@ imageTrace Outline Kontur
|
||||
@msg `{key}` must be an object `{key}` skal være et objekt
|
||||
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` køres alene, ikke som et trin i en batch
|
||||
@msg unknown preset (one of {_1}) ukendt forudindstilling (én af {_1})
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) ukendt forudindstilling `{name}` (høj, mellem, lav eller en gemt: se flattener.presets.list)
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) ukendt forudindstilling `{name}` (high, medium, low eller en gemt: se flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see flattener.presets.list) ukendt forudindstilling `{name}` (se flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see webExport.presets.list) ukendt forudindstilling `{name}` (se webExport.presets.list)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) ukendt side `{other}` (venstre|højre|top|bund)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) ukendt side `{other}` (left|right|top|bottom)
|
||||
@msg unknown tool `{tool}` ukendt værktøj `{tool}`
|
||||
@msg unknown tool `{t}` ukendt værktøj `{t}`
|
||||
@msg unknown tool or command `{key}` ukendt værktøj eller kommando `{key}`
|
||||
@@ -6074,6 +6074,8 @@ axis Y Axis Y-akse
|
||||
Action Options… Indstillinger for handling…
|
||||
Action Options Indstillinger for handling
|
||||
Playback Options… Afspilningsindstillinger…
|
||||
Button Mode Knaptilstand
|
||||
No actions. Ingen handlinger.
|
||||
Insert Stop… Indsæt stop…
|
||||
Insert Stop Indsæt stop
|
||||
Record Stop Optag stop
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6193,6 +6193,8 @@ axis Y Axis Y-Achse
|
||||
Action Options… Aktionsoptionen…
|
||||
Action Options Aktionsoptionen
|
||||
Playback Options… Wiedergabeoptionen…
|
||||
Button Mode Schaltflächenmodus
|
||||
No actions. Keine Aktionen.
|
||||
Insert Stop… Stopp einfügen…
|
||||
Insert Stop Stopp einfügen
|
||||
Record Stop Stopp aufzeichnen
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6553,6 +6553,8 @@ axis Y Axis Άξονας Y
|
||||
Action Options… Επιλογές ενέργειας…
|
||||
Action Options Επιλογές ενέργειας
|
||||
Playback Options… Επιλογές αναπαραγωγής…
|
||||
Button Mode Λειτουργία κουμπιών
|
||||
No actions. Δεν υπάρχουν ενέργειες.
|
||||
Insert Stop… Εισαγωγή διακοπής…
|
||||
Insert Stop Εισαγωγή διακοπής
|
||||
Record Stop Καταγραφή διακοπής
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6544,6 +6544,8 @@ axis Y Axis Eje Y
|
||||
Action Options… Opciones de acción…
|
||||
Action Options Opciones de acción
|
||||
Playback Options… Opciones de reproducción…
|
||||
Button Mode Modo de botones
|
||||
No actions. No hay acciones.
|
||||
Insert Stop… Insertar parada…
|
||||
Insert Stop Insertar parada
|
||||
Record Stop Grabar parada
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6631,6 +6631,8 @@ axis Y Axis Y-akseli
|
||||
Action Options… Toiminnon asetukset…
|
||||
Action Options Toiminnon asetukset
|
||||
Playback Options… Toistoasetukset…
|
||||
Button Mode Painiketila
|
||||
No actions. Ei toimintoja.
|
||||
Insert Stop… Lisää pysäytys…
|
||||
Insert Stop Lisää pysäytys
|
||||
Record Stop Nauhoita pysäytys
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6596,6 +6596,8 @@ axis Y Axis Axe Y
|
||||
Action Options… Options de script…
|
||||
Action Options Options de script
|
||||
Playback Options… Options d’exécution…
|
||||
Button Mode Mode Bouton
|
||||
No actions. Aucun script.
|
||||
Insert Stop… Insérer un arrêt…
|
||||
Insert Stop Insérer un arrêt
|
||||
Record Stop Enregistrer un arrêt
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3560,6 +3560,8 @@ antiAlias Sharp शार्प
|
||||
@msg the action set would be larger than 8 MiB एक्शन सेट 8 MiB से बड़ा हो जाएगा
|
||||
Playback Options प्लेबैक ऑप्शन्स
|
||||
Playback Options… प्लेबैक ऑप्शन्स…
|
||||
Button Mode बटन मोड
|
||||
No actions. कोई कार्रवाई नहीं।
|
||||
Plug-in Info प्लगइन जानकारी
|
||||
Plug-in… प्लग-इन…
|
||||
Plug-ins प्लग-इन्स
|
||||
@@ -5927,7 +5929,7 @@ antiAlias Sharp शार्प
|
||||
@msg `{key}` is longer than {MAX_NAME} characters `{key}` {MAX_NAME} अक्षरों से अधिक लंबा है
|
||||
@msg `{key}` is out of range `{key}` रेंज से बाहर है
|
||||
@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` को प्रतिशत में [min, desired, max] या {{min?, desired?, max?}} होना चाहिए
|
||||
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` होना चाहिए [x, y, चौड़ाई, ऊंचाई]: चार परिमित संख्या, चौड़ाई और ऊंचाई नकारात्मक नहीं है
|
||||
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` होना चाहिए [x, y, width, height]: चार परिमित संख्या, चौड़ाई और ऊंचाई नकारात्मक नहीं है
|
||||
@msg `{key}` must be [x, y] `{key}` होना चाहिए [x, y]
|
||||
@msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` को `auto` या `en` या `zh-hant` जैसे भाषा कोड होना चाहिए।
|
||||
@msg `{key}` must be 0..{max} `{key}` होना चाहिए 0.{max}
|
||||
@@ -6012,7 +6014,7 @@ antiAlias Sharp शार्प
|
||||
@msg `a` and `b` must be two different paths `a` और `b` को दो अलग-अलग पाथ होना चाहिए
|
||||
@msg `align` is start, center or end `align` शुरू, केंद्र या अंत है
|
||||
@msg `align` must be "left", "center" or "right", not {other} `align` होना चाहिए "left", "center" या "right", नहीं {other}
|
||||
@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` को [subpath, एंकर] जोड़े की एक सारणी होना चाहिए
|
||||
@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` को [subpath, anchor] जोड़े की एक सारणी होना चाहिए
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` में एंकर इंडेक्स (0, 1, 2…) की सूची होनी चाहिए, {k} नहीं
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` में एंकर इंडेक्स (0, 1, 2…) की सूची होनी चाहिए, {v} नहीं
|
||||
@msg `antiAlias` must be "none", "sharp", "crisp" or "strong" `antiAlias` "none", "sharp", "crisp" या "strong" होना चाहिए
|
||||
@@ -6398,7 +6400,7 @@ antiAlias Sharp शार्प
|
||||
@msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise अपने फ़ॉन्ट की एक लाइन के लिए बहुत छोटे फ़्रेम वाले एरिया टाइप को लंबे फ़्रेम दिए गए ({_1}): वरना उनका टाइप नहीं दिखता
|
||||
@msg area type needs a path that encloses an area एरिया टाइप को ऐसा पाथ चाहिए जो किसी क्षेत्र को घेरता हो
|
||||
@msg arrowAlign must be extend|tip, got {o} arrowAlign को extend|tip होना चाहिए, {o} मिला
|
||||
@msg arrowScale must be [start, end] arrowScale होना चाहिए [शुरू, अंत]
|
||||
@msg arrowScale must be [start, end] arrowScale होना चाहिए [start, end]
|
||||
@msg artboard {0} does not exist आर्टबोर्ड {0} मौजूद नहीं है
|
||||
@msg artboard {i} doesn't print आर्टबोर्ड {i} प्रिंट नहीं करता है
|
||||
@msg artboard is a 0-based index artboard 0 से शुरू होने वाला इंडेक्स है
|
||||
@@ -6459,14 +6461,14 @@ antiAlias Sharp शार्प
|
||||
@msg bad edit बुरा संपादन
|
||||
@msg bad fill {v} बुरा भर {v}
|
||||
@msg bad fit: {e} बुरा फिट: {e}
|
||||
@msg bad model {other} (keep, new or document) बुरा मॉडल {other} (keep, नया या डॉक्यूमेंट)
|
||||
@msg bad model {other} (keep, new or document) बुरा मॉडल {other} (keep, new या document)
|
||||
@msg bad new colour {v} बुरा नया रंग {v}
|
||||
@msg bad object बुरा ऑब्जेक्ट
|
||||
@msg bad object number खराब ऑब्जेक्ट संख्या
|
||||
@msg bad offset बुरा ऑफसेट
|
||||
@msg bad page tree बुरा पेज पेड़
|
||||
@msg bad path data: {e} बुरा पाथ डेटा: {e}
|
||||
@msg bad point {v}: expected [x, y] or [x, y, pressure] बुरा बिंदु {v}: उम्मीद [x, y] या [x, y, दबाव]
|
||||
@msg bad point {v}: expected [x, y] or [x, y, pressure] बुरा बिंदु {v}: उम्मीद [x, y] या [x, y, pressure]
|
||||
@msg bad reference बुरा संदर्भ
|
||||
@msg bad shortcut बुरा शॉर्टकट
|
||||
@msg bad stream length खराब धारा की लंबाई
|
||||
@@ -6996,7 +6998,7 @@ antiAlias Sharp शार्प
|
||||
@msg Internal error: {msg} (please report this bug) आंतरिक त्रुटि: {msg} (कृपया इस बग की रिपोर्ट करें)
|
||||
@msg invalid `color` अमान्य `color`
|
||||
@msg invalid anchor index {v}: expected a non-negative integer अमान्य एंकर इंडेक्स {v}: एक गैर-नकारात्मक पूर्णांक की उम्मीद
|
||||
@msg invalid anchor reference {v}: expected [subpath, anchor] अमान्य एंकर संदर्भ {v}: उम्मीद [subpath, एंकर]
|
||||
@msg invalid anchor reference {v}: expected [subpath, anchor] अमान्य एंकर संदर्भ {v}: उम्मीद [subpath, anchor]
|
||||
@msg invalid angle अमान्य कोण
|
||||
@msg invalid brush: {e} अमान्य ब्रश: {e}
|
||||
@msg invalid color अमान्य रंग
|
||||
@@ -7173,7 +7175,7 @@ antiAlias Sharp शार्प
|
||||
@msg missing `value` लापता `value`
|
||||
@msg missing anchor लापता एंकर
|
||||
@msg missing anchors लापता एंकर
|
||||
@msg missing anchors [[subpath, anchor]…] लापता एंकर [[subpath, एंकर]...]
|
||||
@msg missing anchors [[subpath, anchor]…] लापता एंकर [[subpath, anchor]...]
|
||||
@msg missing array `{key}` लापता सरणी `{key}`
|
||||
@msg missing case लापता मामला
|
||||
@msg missing color लापता रंग
|
||||
@@ -8385,7 +8387,7 @@ antiAlias Sharp शार्प
|
||||
@msg Unembedded to {_1} एम्बेड हटाकर {_1} में रखा गया
|
||||
@msg unexpected host trap होस्ट में अप्रत्याशित ट्रैप
|
||||
@msg unexpected resources अप्रत्याशित संसाधन
|
||||
@msg unknown {k} key `{key}` (size, position) अज्ञात {k} कुंजी `{key}` (आकार, स्थिति)
|
||||
@msg unknown {k} key `{key}` (size, position) अज्ञात {k} कुंजी `{key}` (size, position)
|
||||
@msg unknown {key} `{v}` (none, short or full) अज्ञात {key} `{v}` (none, short या full)
|
||||
@msg unknown {key} profile `{n}` अज्ञात {key} प्रोफ़ाइल `{n}`
|
||||
@msg unknown align `{other}` अज्ञात संरेखण `{other}`
|
||||
@@ -8418,9 +8420,9 @@ antiAlias Sharp शार्प
|
||||
@msg unknown format अज्ञात स्वरूप
|
||||
@msg unknown format `{f}` (Save writes {_1}) अज्ञात फ़ॉर्मैट `{f}` (सेव {_1} लिखता है)
|
||||
@msg unknown format `{f}` (vcswatches, gpl, ase or css) अज्ञात फ़ॉर्मैट `{f}` (vcswatches, gpl, ase या css)
|
||||
@msg unknown freeform mode `{m}` (points, lines) अज्ञात फ्रीफॉर्म मोड `{m}` (बिंदुओं, लाइनों)
|
||||
@msg unknown freeform mode `{m}` (points, lines) अज्ञात फ्रीफॉर्म मोड `{m}` (points, lines)
|
||||
@msg unknown from {f} {f} से अज्ञात
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) अज्ञात ग्रेडिएंट `{k}` (रैखिक, रेडियल, शंक्वाकार, डायमंड, फ्रीफॉर्म)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) अज्ञात ग्रेडिएंट `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown graph type `{t}` अज्ञात ग्राफ प्रकार `{t}`
|
||||
@msg unknown grid colours `{name}` (one of {_1}) अज्ञात ग्रिड रंग `{name}` ({_1} में से एक)
|
||||
@msg unknown gridType `{t}` अज्ञात gridType `{t}`
|
||||
@@ -8435,20 +8437,20 @@ antiAlias Sharp शार्प
|
||||
@msg unknown method `{m}` ({_1}) अज्ञात विधि `{m}` ({_1})
|
||||
@msg unknown mode {m} अज्ञात मोड {m}
|
||||
@msg unknown mode `{o}` अज्ञात मोड `{o}`
|
||||
@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) अज्ञात मोड `{other}` (ग्रे, RGB, hsb, लैब, cmyk या वेब)
|
||||
@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) अज्ञात मोड `{other}` (gray, rgb, hsb, lab, cmyk या web)
|
||||
@msg unknown mode `{other}` (mono|cmyk) अज्ञात मोड `{other}` (mono|cmyk)
|
||||
@msg unknown mode `{other}` (rgb|cmyk|gray|global) अज्ञात मोड `{other}` (rgb|cmyk|gray|global)
|
||||
@msg unknown mode `{other}` (set, add, subtract) अज्ञात मोड `{other}` (सेट, ऐड, घटाना)
|
||||
@msg unknown mode `{other}` (set, add, subtract) अज्ञात मोड `{other}` (set, add, subtract)
|
||||
@msg unknown model `{other}` अज्ञात मॉडल `{other}`
|
||||
@msg unknown model `{other}` (keep, new or document) अज्ञात मॉडल `{other}` (keep, नया या डॉक्यूमेंट)
|
||||
@msg unknown model `{other}` (keep, new or document) अज्ञात मॉडल `{other}` (keep, new या document)
|
||||
@msg unknown op {o} अज्ञात op {o}
|
||||
@msg unknown OpenType feature `{t}` अज्ञात ओपन टाइप सुविधा `{t}`
|
||||
@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom; prefix - to turn off) अज्ञात OpenType फ़ीचर `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom; बंद करने के लिए आगे - लगाएँ)
|
||||
@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) featureChanges में अज्ञात OpenType फ़ीचर `{t}` (वे टैग जो `features` लेता है)
|
||||
@msg unknown option `{path}.{k}` अज्ञात ऑप्शन `{path}.{k}`
|
||||
@msg unknown order `{o}`: toggle, front, forward, backward or back अज्ञात आदेश `{o}`: टॉगल, फ्रंट, फॉरवर्ड, बैकवर्ड या बैक
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{o}` (preserve, अनुकरण या discard)
|
||||
@msg unknown overprints `{v}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{v}` (प्ररक्षित, अनुकरण या त्यागपत्र)
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{o}` (preserve, simulate या discard)
|
||||
@msg unknown overprints `{v}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{v}` (preserve, simulate या discard)
|
||||
@msg unknown panel अज्ञात पैनल
|
||||
@msg unknown panel `{raw}` अज्ञात पैनल `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode प्रस्तुति मोड में पैनल्स नहीं दिखाए जा सकते
|
||||
@@ -8463,7 +8465,7 @@ antiAlias Sharp शार्प
|
||||
@msg `{path}` is nested too deeply `{path}` बहुत गहराई तक नेस्ट किया गया है
|
||||
@msg unknown PDF preset `{name}` (presets: {_1}) अज्ञात PDF प्रीसेट `{name}` (presets: {_1})
|
||||
@msg unknown pieLegend `{v}` (none, standard or wedges) अज्ञात pieLegend `{v}` (none, standard या wedges)
|
||||
@msg unknown piePosition `{v}` (even, ratio or stacked) अज्ञात piePosition `{v}` (यहां तक कि अनुपात या स्टैक्ड)
|
||||
@msg unknown piePosition `{v}` (even, ratio or stacked) अज्ञात piePosition `{v}` (even, ratio या stacked)
|
||||
@msg unknown pieSort `{v}` (none, all or first) अज्ञात pieSort `{v}` (none, all या first)
|
||||
@msg unknown plate `{n}` अज्ञात प्लेट `{n}`
|
||||
@msg unknown preference `{k}` अज्ञात वरीयता `{k}`
|
||||
@@ -8489,7 +8491,7 @@ antiAlias Sharp शार्प
|
||||
@msg unknown preset `{name}`: not a built-in or saved preset (see imageTrace.presets) अज्ञात प्रीसेट `{name}`: एक अंतर्निहित या सेव किए गए प्रीसेट नहीं (imageTrace.presets देखें)
|
||||
@msg unknown profile {id} (see stroke.widthProfile.list) अज्ञात प्रोफ़ाइल {id} (stroke.widthProfile.list देखें)
|
||||
@msg unknown proof target `{t}` अज्ञात प्रमाण लक्ष्य `{t}`
|
||||
@msg unknown quotes key `{k}` (double, single) अज्ञात उद्धरण कुंजी `{k}` (डबल, सिंगल)
|
||||
@msg unknown quotes key `{k}` (double, single) अज्ञात उद्धरण कुंजी `{k}` (double, single)
|
||||
@msg unknown resolution `{v}` अज्ञात रिज़ॉल्यूशन `{v}`
|
||||
@msg unknown rule `{r}` (see color.harmony) अज्ञात नियम `{r}` (देखें color.harmony)
|
||||
@msg unknown rule `{rule}` ({_1}) अज्ञात नियम `{rule}` ({_1})
|
||||
@@ -8505,7 +8507,7 @@ antiAlias Sharp शार्प
|
||||
@msg Unknown unit अज्ञात इकाई
|
||||
@msg unknown units अज्ञात इकाइयों
|
||||
@msg unknown units `{u}` अज्ञात इकाइयों `{u}`
|
||||
@msg unknown value axis `{v}` (left, right or both) अज्ञात मान अक्ष `{v}` (बाएं, दाएं या दोनों)
|
||||
@msg unknown value axis `{v}` (left, right or both) अज्ञात मान अक्ष `{v}` (left, right या both)
|
||||
@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) अज्ञात variation `{v}` (tintsShades|warmCool|vividMuted)
|
||||
@msg unknown Warp style अज्ञात ताना स्टाइल
|
||||
@msg unknown warp style `{s}` अज्ञात ताना स्टाइल `{s}`
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6584,6 +6584,8 @@ axis Y Axis Y tengely
|
||||
Action Options… Művelet beállításai…
|
||||
Action Options Művelet beállításai
|
||||
Playback Options… Lejátszási beállítások…
|
||||
Button Mode Gomb mód
|
||||
No actions. Nincs művelet.
|
||||
Insert Stop… Megállás beszúrása…
|
||||
Insert Stop Megállás beszúrása
|
||||
Record Stop Megállás rögzítése
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4796,11 +4796,11 @@ imageTrace Outline Kontur
|
||||
@msg unknown effect `{id}` efek `{id}` tidak dikenal
|
||||
@msg unknown format format tidak dikenal
|
||||
@msg unknown format `{f}` (Save writes {_1}) format `{f}` tidak dikenal (Simpan menulis {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) mode bentuk bebas `{m}` tidak dikenal (titik, garis)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) jenis gradien `{k}` tidak dikenal (linear, radial, kerucut, wajik, bentuk bebas)
|
||||
@msg unknown freeform mode `{m}` (points, lines) mode bentuk bebas `{m}` tidak dikenal (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) jenis gradien `{k}` tidak dikenal (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` sorotan `{h}` tidak dikenal
|
||||
@msg unknown option `{path}.{k}` opsi `{path}.{k}` tidak dikenal
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) overprint `{o}` tidak dikenal (pertahankan, simulasikan, atau buang)
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) overprint `{o}` tidak dikenal (preserve, simulate, atau discard)
|
||||
@msg unknown panel `{raw}` panel `{raw}` tidak dikenal
|
||||
@msg unknown preference `{key}` preferensi `{key}` tidak dikenal
|
||||
@msg Preferences imported. Preferensi diimpor.
|
||||
@@ -4818,10 +4818,10 @@ imageTrace Outline Kontur
|
||||
@msg `{key}` must be an object `{key}` harus berupa objek
|
||||
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` dijalankan sendiri, bukan sebagai langkah batch
|
||||
@msg unknown preset (one of {_1}) preset tidak dikenal (salah satu dari {_1})
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) preset `{name}` tidak dikenal (tinggi, sedang, rendah, atau yang tersimpan: lihat flattener.presets.list)
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) preset `{name}` tidak dikenal (high, medium, low, atau yang tersimpan: lihat flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see flattener.presets.list) preset `{name}` tidak dikenal (lihat flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see webExport.presets.list) preset `{name}` tidak dikenal (lihat webExport.presets.list)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) sisi `{other}` tidak dikenal (kiri|kanan|atas|bawah)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) sisi `{other}` tidak dikenal (left|right|top|bottom)
|
||||
@msg unknown tool `{tool}` alat `{tool}` tidak dikenal
|
||||
@msg unknown tool `{t}` alat `{t}` tidak dikenal
|
||||
@msg unknown tool or command `{key}` alat atau perintah `{key}` tidak dikenal
|
||||
@@ -6074,6 +6074,8 @@ axis Y Axis Sumbu Y
|
||||
Action Options… Opsi tindakan…
|
||||
Action Options Opsi tindakan
|
||||
Playback Options… Opsi pemutaran…
|
||||
Button Mode Mode tombol
|
||||
No actions. Tidak ada aksi.
|
||||
Insert Stop… Sisipkan henti…
|
||||
Insert Stop Sisipkan henti
|
||||
Record Stop Rekam henti
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2430 and column 5.
|
@@ -6227,6 +6227,8 @@ axis Y Axis Asse Y
|
||||
Action Options… Opzioni azione…
|
||||
Action Options Opzioni azione
|
||||
Playback Options… Opzioni di riproduzione…
|
||||
Button Mode Modalità pulsante
|
||||
No actions. Nessuna azione.
|
||||
Insert Stop… Inserisci interruzione…
|
||||
Insert Stop Inserisci interruzione
|
||||
Record Stop Registra interruzione
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6293,6 +6293,8 @@ axis Y Axis Y 軸
|
||||
Action Options… アクションオプション…
|
||||
Action Options アクションオプション
|
||||
Playback Options… 再生オプション…
|
||||
Button Mode ボタンモード
|
||||
No actions. アクションがありません。
|
||||
Insert Stop… 停止を挿入…
|
||||
Insert Stop 停止を挿入
|
||||
Record Stop 停止を記録
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6552,6 +6552,8 @@ axis Y Axis Y ღერძი
|
||||
Action Options… მოქმედების პარამეტრები…
|
||||
Action Options მოქმედების პარამეტრები
|
||||
Playback Options… შესრულების პარამეტრები…
|
||||
Button Mode ღილაკების რეჟიმი
|
||||
No actions. მოქმედებები არ არის.
|
||||
Insert Stop… გაჩერების ჩასმა…
|
||||
Insert Stop გაჩერების ჩასმა
|
||||
Record Stop გაჩერების ჩაწერა
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6188,6 +6188,8 @@ axis Y Axis Y축
|
||||
Action Options… 액션 옵션…
|
||||
Action Options 액션 옵션
|
||||
Playback Options… 재생 옵션…
|
||||
Button Mode 버튼 모드
|
||||
No actions. 동작이 없습니다.
|
||||
Insert Stop… 중지 삽입…
|
||||
Insert Stop 중지 삽입
|
||||
Record Stop 중지 기록
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5313,11 +5313,11 @@ imageTrace Outline Contour
|
||||
@msg unknown effect `{id}` onbekend effect `{id}`
|
||||
@msg unknown format onbekende indeling
|
||||
@msg unknown format `{f}` (Save writes {_1}) onbekende indeling `{f}` (Opslaan schrijft {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) onbekende vrijvormmodus `{m}` (punten, lijnen)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) onbekend verlooptype `{k}` (lineair, radiaal, conisch, ruit, vrijvorm)
|
||||
@msg unknown freeform mode `{m}` (points, lines) onbekende vrijvormmodus `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) onbekend verlooptype `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` onbekende markering `{h}`
|
||||
@msg unknown option `{path}.{k}` onbekende optie `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) onbekende overdruk `{o}` (behouden, simuleren of verwerpen)
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) onbekende overdruk `{o}` (preserve, simulate of discard)
|
||||
@msg unknown panel `{raw}` onbekend paneel `{raw}`
|
||||
@msg unknown preference `{key}` onbekende voorkeur `{key}`
|
||||
@msg Preferences imported. Voorkeuren geïmporteerd.
|
||||
@@ -5335,10 +5335,10 @@ imageTrace Outline Contour
|
||||
@msg `{key}` must be an object `{key}` moet een object zijn
|
||||
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` wordt los uitgevoerd, niet als stap van een batch
|
||||
@msg unknown preset (one of {_1}) onbekende voorinstelling (één van {_1})
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) onbekende voorinstelling `{name}` (hoog, middel, laag of een opgeslagen voorinstelling: zie flattener.presets.list)
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) onbekende voorinstelling `{name}` (high, medium, low of een opgeslagen voorinstelling: zie flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see flattener.presets.list) onbekende voorinstelling `{name}` (zie flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see webExport.presets.list) onbekende voorinstelling `{name}` (zie webExport.presets.list)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) onbekende zijde `{other}` (links|rechts|boven|onder)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) onbekende zijde `{other}` (left|right|top|bottom)
|
||||
@msg unknown tool `{tool}` onbekend gereedschap `{tool}`
|
||||
@msg unknown tool `{t}` onbekend gereedschap `{t}`
|
||||
@msg unknown tool or command `{key}` onbekend gereedschap of opdracht `{key}`
|
||||
@@ -6696,6 +6696,8 @@ axis Y Axis Y-as
|
||||
Action Options… Actieopties…
|
||||
Action Options Actieopties
|
||||
Playback Options… Afspeelopties…
|
||||
Button Mode Knopmodus
|
||||
No actions. Geen acties.
|
||||
Insert Stop… Stop invoegen…
|
||||
Insert Stop Stop invoegen
|
||||
Record Stop Stop opnemen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6456,6 +6456,8 @@ axis Y Axis Oś Y
|
||||
Action Options… Opcje operacji…
|
||||
Action Options Opcje operacji
|
||||
Playback Options… Opcje odtwarzania…
|
||||
Button Mode Tryb przycisków
|
||||
No actions. Brak operacji.
|
||||
Insert Stop… Wstaw zatrzymanie…
|
||||
Insert Stop Wstaw zatrzymanie
|
||||
Record Stop Nagraj zatrzymanie
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6516,6 +6516,8 @@ axis Y Axis Eixo Y
|
||||
Action Options… Opções da Ação…
|
||||
Action Options Opções da Ação
|
||||
Playback Options… Opções de Reprodução…
|
||||
Button Mode Modo de Botão
|
||||
No actions. Nenhuma ação.
|
||||
Insert Stop… Inserir Parada…
|
||||
Insert Stop Inserir Parada
|
||||
Record Stop Gravar Parada
|
||||
@@ -7642,7 +7644,7 @@ swatch Yellow Green Verde-amarelado
|
||||
@msg `alignOn` must be a string `alignOn` deve ser uma string
|
||||
@msg `align` is start, center or end `align` é start, center ou end
|
||||
@msg `align` must be a string `align` deve ser uma string
|
||||
@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` deve ser uma matriz de pares [subcaminho, ponto de ancoragem]
|
||||
@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` deve ser uma matriz de pares [subpath, anchor]
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` deve listar índices de pontos de ancoragem (0, 1, 2…), não {k}
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` deve listar índices de pontos de ancoragem (0, 1, 2…), não {v}
|
||||
@msg `answer` for this {kind} dialog must be one of: {_1} `answer` para esta caixa de diálogo {kind} deve ser um de: {_1}
|
||||
@@ -7707,7 +7709,7 @@ swatch Yellow Green Verde-amarelado
|
||||
@msg `fidelity` must be from {lo} to {hi} pt `fidelity` deve estar entre {lo} e {hi} pt
|
||||
@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` deve ter de 1 a 1000 caminhos de arquivos de fonte encontrados pela última pesquisa de text.findFontFiles
|
||||
@msg `focal` and `radius` cannot be used together `focal` e `radius` não podem ser usados juntos
|
||||
@msg `focal` must be an array of [subpath, anchor] pairs `focal` deve ser uma matriz de pares [subcaminho, ponto de ancoragem]
|
||||
@msg `focal` must be an array of [subpath, anchor] pairs `focal` deve ser uma matriz de pares [subpath, anchor]
|
||||
@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` indica [{si}, {ai}], que não é um ponto de ancoragem do caminho
|
||||
@msg `folder` must be an absolute folder path `folder` deve ser um caminho absoluto de pasta
|
||||
@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` deve ser um objeto de tags de eixo e valores, como {"wght"}
|
||||
@@ -8372,7 +8374,7 @@ swatch Yellow Green Verde-amarelado
|
||||
@msg instruction {pc} reached with stack depths {seen} and {d} instrução {pc} alcançada com profundidades de pilha {seen} e {d}
|
||||
@msg invalid `color` `color` inválido
|
||||
@msg invalid anchor index {v}: expected a non-negative integer índice de ponto de ancoragem inválido {v}: esperado um inteiro não negativo
|
||||
@msg invalid anchor reference {v}: expected [subpath, anchor] referência de ponto de ancoragem inválida {v}: esperado [subcaminho, ponto de ancoragem]
|
||||
@msg invalid anchor reference {v}: expected [subpath, anchor] referência de ponto de ancoragem inválida {v}: esperado [subpath, anchor]
|
||||
@msg invalid angle ângulo inválido
|
||||
@msg invalid array length comprimento de matriz inválido
|
||||
@msg invalid brush: {e} pincel inválido: {e}
|
||||
@@ -8481,7 +8483,7 @@ swatch Yellow Green Verde-amarelado
|
||||
@msg missing `{k}` falta `{k}`
|
||||
@msg missing anchor falta o ponto de ancoragem
|
||||
@msg missing anchors faltam os pontos de ancoragem
|
||||
@msg missing anchors [[subpath, anchor]…] faltam os pontos de ancoragem [[subcaminho, ponto de ancoragem]…]
|
||||
@msg missing anchors [[subpath, anchor]…] faltam os pontos de ancoragem [[subpath, anchor]…]
|
||||
@msg missing array `{key}` falta a matriz `{key}`
|
||||
@msg missing case falta case
|
||||
@msg missing color falta a cor
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6212,6 +6212,8 @@ axis Y Axis Ось Y
|
||||
Action Options… Параметры действия…
|
||||
Action Options Параметры действия
|
||||
Playback Options… Параметры воспроизведения…
|
||||
Button Mode Режим кнопок
|
||||
No actions. Нет действий.
|
||||
Insert Stop… Вставить остановку…
|
||||
Insert Stop Вставить остановку
|
||||
Record Stop Записать остановку
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6581,6 +6581,8 @@ axis Y Axis Y-axel
|
||||
Action Options… Alternativ för åtgärd…
|
||||
Action Options Alternativ för åtgärd
|
||||
Playback Options… Uppspelningsalternativ…
|
||||
Button Mode Knappläge
|
||||
No actions. Inga åtgärder.
|
||||
Insert Stop… Infoga stopp…
|
||||
Insert Stop Infoga stopp
|
||||
Record Stop Spela in stopp
|
||||
@@ -7546,7 +7548,7 @@ digits Hindi Hindi
|
||||
@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` är längre än {MAX_TEXT} byte
|
||||
@msg `{what}` must be text `{what}` måste vara text
|
||||
@msg `align` is start, center or end `align` är start, center eller end
|
||||
@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` måste vara en array av par [delbana, ankarpunkt]
|
||||
@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` måste vara en array av par [subpath, anchor]
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` måste lista ankarpunktsindex (0, 1, 2…), inte {k}
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` måste lista ankarpunktsindex (0, 1, 2…), inte {v}
|
||||
@msg `artboards` must be a list of artboard indices `artboards` måste vara en lista med ritytsindex
|
||||
@@ -8071,7 +8073,7 @@ digits Hindi Hindi
|
||||
@msg instruction budget instruktionsbudget
|
||||
@msg invalid `color` ogiltig `color`
|
||||
@msg invalid anchor index {v}: expected a non-negative integer ogiltigt ankarindex {v}: ett icke-negativt heltal förväntades
|
||||
@msg invalid anchor reference {v}: expected [subpath, anchor] ogiltig ankarreferens {v}: [delbana, ankare] förväntades
|
||||
@msg invalid anchor reference {v}: expected [subpath, anchor] ogiltig ankarreferens {v}: [subpath, anchor] förväntades
|
||||
@msg invalid angle ogiltig vinkel
|
||||
@msg invalid brush: {e} ogiltig pensel: {e}
|
||||
@msg invalid color ogiltig färg
|
||||
@@ -8160,7 +8162,7 @@ digits Hindi Hindi
|
||||
@msg missing `value` `value` saknas
|
||||
@msg missing anchor ankare saknas
|
||||
@msg missing anchors ankare saknas
|
||||
@msg missing anchors [[subpath, anchor]…] ankare saknas [[delbana, ankare]…]
|
||||
@msg missing anchors [[subpath, anchor]…] ankare saknas [[subpath, anchor]…]
|
||||
@msg missing array `{key}` arrayen `{key}` saknas
|
||||
@msg missing case skiftläge saknas
|
||||
@msg missing color färg saknas
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4030,6 +4030,87 @@ fn translated_messages_keep_the_command_ids_their_english_names() {
|
||||
assert!(missing.is_empty(), "{} rows drop a command id their English names:\n{}", missing.len(), missing.join("\n"));
|
||||
}
|
||||
|
||||
/// Whether `word` is an ASCII name a command reads: a letter followed by letters, digits or
|
||||
/// underscores.
|
||||
fn is_ascii_name(word: &str) -> bool {
|
||||
word.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
/// The field names and accepted values that `s` spells out of its backticked spans: each name of a
|
||||
/// comma-separated list in square brackets, such as `[x, y, width, height]` or
|
||||
/// `[[subpath, anchor]…]`; each `a|b|c` list of values, whole; and the values an `unknown …` or
|
||||
/// `bad model …` message lists in parentheses, such as `(none, standard or wedges)`, up to the first
|
||||
/// item that is not a single name, so `(high, medium, low or a saved one: see …)` gives `high`,
|
||||
/// `medium` and `low`, and `(or give its ids)` gives none.
|
||||
fn field_names_and_values(s: &str) -> Vec<String> {
|
||||
let outside = s.split('`').step_by(2).collect::<Vec<_>>().join(" ");
|
||||
let mut found = Vec::new();
|
||||
for piece in outside.split('[').skip(1) {
|
||||
let Some(end) = piece.find(']') else { continue };
|
||||
let names: Vec<&str> = piece[..end].split(',').map(str::trim).collect();
|
||||
if names.len() > 1 && names.iter().all(|name| is_ascii_name(name)) {
|
||||
found.extend(names.into_iter().map(str::to_string));
|
||||
}
|
||||
}
|
||||
let lists = outside.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '|'));
|
||||
found.extend(lists.filter(|w| w.contains('|') && w.split('|').all(is_ascii_name)).map(str::to_string));
|
||||
for (at, _) in outside.match_indices('(') {
|
||||
let before = &outside[..at];
|
||||
if !(holds_ascii_word(before, "unknown") || before.contains("bad model")) {
|
||||
continue;
|
||||
}
|
||||
let Some(end) = outside[at..].find(')') else { continue };
|
||||
let items = outside[at + 1..at + end].split(", ").flat_map(|item| item.split(" or "));
|
||||
let values: Vec<&str> = items.map(str::trim).take_while(|item| is_ascii_name(item)).collect();
|
||||
if values.len() > 1 {
|
||||
found.extend(values.into_iter().map(str::to_string));
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_names_and_values_finds_bracketed_names_and_listed_values() {
|
||||
assert_eq!(field_names_and_values("`{key}` must be [x, y, width, height]: four numbers"), ["x", "y", "width", "height"]);
|
||||
assert_eq!(field_names_and_values("missing anchors [[subpath, anchor]…]"), ["subpath", "anchor"]);
|
||||
assert_eq!(field_names_and_values("arrowScale must be [start, end]"), ["start", "end"]);
|
||||
assert_eq!(field_names_and_values("unknown pieLegend {v} (none, standard or wedges)"), ["none", "standard", "wedges"]);
|
||||
assert_eq!(field_names_and_values("unknown side `{other}` (left|right|top|bottom)"), ["left|right|top|bottom"]);
|
||||
assert_eq!(field_names_and_values("bad model {other} (keep, new or document)"), ["keep", "new", "document"]);
|
||||
assert_eq!(
|
||||
field_names_and_values("unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list)"),
|
||||
["high", "medium", "low"]
|
||||
);
|
||||
assert_eq!(field_names_and_values("unknown object (or give its ids)"), Vec::<String>::new());
|
||||
assert_eq!(field_names_and_values("unknown color `{c}` (#rrggbb or a preset name)"), Vec::<String>::new());
|
||||
assert_eq!(field_names_and_values("unknown resolution (72 screen, 150 medium, 300 high)"), Vec::<String>::new());
|
||||
assert_eq!(field_names_and_values("the art is [1, 2] (left, right) and `[a, b]`"), Vec::<String>::new());
|
||||
}
|
||||
|
||||
/// A translated status or error message keeps each field name and accepted value its English text
|
||||
/// spells out (see [`field_names_and_values`]), as the English spells it: the message tells a user
|
||||
/// what to pass, and a command reads only the English names and values.
|
||||
#[test]
|
||||
fn translated_messages_keep_the_field_names_and_values_their_english_lists() {
|
||||
let mut missing = Vec::new();
|
||||
for l in &LANGUAGES {
|
||||
let (entries, _) = parse_entries(l.source);
|
||||
for (ctx, src, tr) in entries.iter().filter(|(ctx, _, _)| ctx == "@msg" || ctx == "@msgplural") {
|
||||
let plural = ctx == "@msgplural";
|
||||
let english = if plural { src.rsplit('|').next().unwrap_or(src) } else { src.as_str() };
|
||||
let forms: Vec<&str> = if plural { tr.split('|').collect() } else { vec![tr.as_str()] };
|
||||
for form in forms {
|
||||
for name in field_names_and_values(english) {
|
||||
if !holds_ascii_word(form, &name) {
|
||||
missing.push(format!("{}: {name} in {src:?} -> {form:?}", l.code));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(missing.is_empty(), "{} rows drop a field name or value their English lists:\n{}", missing.len(), missing.join("\n"));
|
||||
}
|
||||
|
||||
/// Languages whose catalogs translate status and error messages; [`untranslated_report`] lists
|
||||
/// the messages each one lacks.
|
||||
const COMPLETE_MESSAGES: &[&str] = &[
|
||||
|
||||
@@ -4702,11 +4702,11 @@ axis Both Her İkisi
|
||||
@msg unknown effect `{id}` bilinmeyen etki `{id}`
|
||||
@msg unknown format bilinmeyen format
|
||||
@msg unknown format `{f}` (Save writes {_1}) bilinmeyen format `{f}` (Kaydet, {_1} yazar)
|
||||
@msg unknown freeform mode `{m}` (points, lines) bilinmeyen serbest biçimli mod `{m}` (noktalar, çizgiler)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) bilinmeyen gradyan türü `{k}` (doğrusal, radyal, konik, elmas, serbest biçimli)
|
||||
@msg unknown freeform mode `{m}` (points, lines) bilinmeyen serbest biçimli mod `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) bilinmeyen gradyan türü `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` bilinmeyen vurgu `{h}`
|
||||
@msg unknown option `{path}.{k}` bilinmeyen seçenek `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) bilinmeyen üst baskılar `{o}` (koru, benzet veya at)
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) bilinmeyen üst baskılar `{o}` (preserve, simulate veya discard)
|
||||
@msg unknown panel `{raw}` bilinmeyen panel `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode Sunum Modunda paneller gösterilemez
|
||||
@msg unknown preference `{key}` bilinmeyen tercih `{key}`
|
||||
@@ -4725,11 +4725,11 @@ axis Both Her İkisi
|
||||
@msg `{key}` must be an object `{key}` bir nesne olmalıdır
|
||||
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` tek başına çalışır, toplu iş adımı olarak değil
|
||||
@msg unknown preset (one of {_1}) bilinmeyen ön ayar ({_1}'den biri)
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) bilinmeyen ön ayar `{name}` (yüksek, orta, düşük veya kaydedilmiş bir ayar: bkz. flattener.presets.list)
|
||||
@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) bilinmeyen ön ayar `{name}` (high, medium, low veya kaydedilmiş bir ayar: bkz. flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see flattener.presets.list) bilinmeyen ön ayar `{name}` (bkz. flattener.presets.list)
|
||||
@msg unknown preset `{name}` (see webExport.presets.list) bilinmeyen ön ayar `{name}` (bkz. webExport.presets.list)
|
||||
@msg unknown preset `{name}`: not a built-in or saved preset (see imageTrace.presets) bilinmeyen ön ayar `{name}`: yerleşik ya da kayıtlı bir ön ayar değil (bkz. imageTrace.presets)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) bilinmeyen taraf `{other}` (sol|sağ|üst|alt)
|
||||
@msg unknown side `{other}` (left|right|top|bottom) bilinmeyen taraf `{other}` (left|right|top|bottom)
|
||||
@msg unknown tool `{tool}` bilinmeyen araç `{tool}`
|
||||
@msg unknown tool `{t}` bilinmeyen araç `{t}`
|
||||
@msg unknown tool or command `{key}` bilinmeyen araç veya komut `{key}`
|
||||
@@ -6557,6 +6557,8 @@ axis Y Axis Y ekseni
|
||||
Action Options… Eylem Seçenekleri…
|
||||
Action Options Eylem Seçenekleri
|
||||
Playback Options… Oynatma Seçenekleri…
|
||||
Button Mode Düğme Modu
|
||||
No actions. Eylem yok.
|
||||
Insert Stop… Durak Ekle…
|
||||
Insert Stop Durak Ekle
|
||||
Record Stop Durak Kaydet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -6207,6 +6207,8 @@ axis Y Axis Вісь Y
|
||||
Action Options… Параметри дії…
|
||||
Action Options Параметри дії
|
||||
Playback Options… Параметри відтворення…
|
||||
Button Mode Режим кнопок
|
||||
No actions. Нема дій.
|
||||
Insert Stop… Вставити зупинку…
|
||||
Insert Stop Вставити зупинку
|
||||
Record Stop Записати зупинку
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6208,6 +6208,8 @@ axis Y Axis Trục Y
|
||||
Action Options… Tùy chọn hành động…
|
||||
Action Options Tùy chọn hành động
|
||||
Playback Options… Tùy chọn phát lại…
|
||||
Button Mode Chế độ nút
|
||||
No actions. Không có hành động.
|
||||
Insert Stop… Chèn điểm dừng…
|
||||
Insert Stop Chèn điểm dừng
|
||||
Record Stop Ghi điểm dừng
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -6440,6 +6440,8 @@ axis Y Axis Y 轴
|
||||
Action Options… 动作选项…
|
||||
Action Options 动作选项
|
||||
Playback Options… 播放选项…
|
||||
Button Mode 按钮模式
|
||||
No actions. 没有动作。
|
||||
Insert Stop… 插入停止…
|
||||
Insert Stop 插入停止
|
||||
Record Stop 记录停止
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6246,6 +6246,8 @@ axis Y Axis Y 軸
|
||||
Action Options… 動作選項…
|
||||
Action Options 動作選項
|
||||
Playback Options… 播放選項…
|
||||
Button Mode 按鈕模式
|
||||
No actions. 沒有動作。
|
||||
Insert Stop… 插入停止…
|
||||
Insert Stop 插入停止
|
||||
Record Stop 記錄停止
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -26,13 +26,13 @@ pub(crate) use export::{
|
||||
export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named,
|
||||
};
|
||||
pub(crate) use open::report_installed;
|
||||
pub(crate) use open::{EXPORT_PRESETS, import_presets, open_with_on, opened, show_opened};
|
||||
pub(crate) use open::{EXPORT_PRESETS, import_presets, open_notes, open_with_on, opened, opened_after_notice, show_opened};
|
||||
pub use open::{
|
||||
MAX_RECENT_FILES, OpensAs, ask_revert, install_module, install_plugin, new_from_template, note_recent, open_bytes, open_dialog, open_document,
|
||||
open_path, open_path_with, open_reporting, open_with, opens_as, place_bytes, recent_files, report_open_error,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use open::{open_file, opened_after_notice, opened_with_notes};
|
||||
pub(crate) use open::{open_file, opened_with_notes};
|
||||
pub(crate) use save::save_current;
|
||||
pub use save::{ask_format_options, ask_pdf_options, remember_active_artboard, remember_view, save, save_pdf};
|
||||
pub(crate) use save::{export_path, is_web, notes_of, report_saved, show_with_notes, target_path, write_named, write_reporting, write_to};
|
||||
|
||||
@@ -162,7 +162,7 @@ pub(crate) fn show_opened(app: &mut VectorcraftApp, notice: Option<String>, note
|
||||
|
||||
/// The notes of the open `r`, those of what reading the file left out (its document's
|
||||
/// `import_losses`) first, each group in the order the import gave them.
|
||||
fn open_notes(app: &VectorcraftApp, r: &Value) -> Vec<String> {
|
||||
pub(crate) fn open_notes(app: &VectorcraftApp, r: &Value) -> Vec<String> {
|
||||
let notes = notes_of(r);
|
||||
let index = r["index"].as_u64().and_then(|i| usize::try_from(i).ok());
|
||||
let losses = index.and_then(|i| app.session.documents().get(i)).map(|st| st.import_losses.as_slice()).unwrap_or_default();
|
||||
|
||||
@@ -985,6 +985,8 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result
|
||||
crate::panels::actions::NEW_ACTION => crate::panels::actions::new_action(app, p),
|
||||
crate::panels::actions::ACTION_OPTIONS => crate::panels::actions::action_options(app, p),
|
||||
crate::panels::actions::PLAY_ACTION => crate::panels::actions::play_action(app, p),
|
||||
crate::panels::actions::PLAY_AT => crate::panels::actions::play_at(app, p),
|
||||
crate::panels::actions::BUTTON_MODE => crate::panels::actions::button_mode(app, p),
|
||||
crate::panels::actions::PLAYBACK_OPTIONS => crate::panels::actions::playback_options(app, p),
|
||||
crate::panels::actions::START_RECORDING => crate::panels::actions::record(app, p),
|
||||
crate::panels::actions::STOP_RECORDING => crate::panels::actions::stop(app, p),
|
||||
|
||||
@@ -1466,7 +1466,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
crate::panels::actions::NEW_ACTION,
|
||||
"New Action",
|
||||
"",
|
||||
"{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), which the panel does not show. Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
|
||||
"{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), the color of its button in Button Mode (ui.action.buttonMode). Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
|
||||
),
|
||||
(
|
||||
crate::panels::actions::ACTION_OPTIONS,
|
||||
@@ -1480,6 +1480,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{action: name, set?: name} play the Actions panel's action named action at the speed ui.action.playbackOptions sets; set names the set to look in (default: every set). Accelerated plays it as one undoable batch → what command.batch returns. Step by Step and Pause For play its first step now and the others one at a time, each its own undo step, until the last, ui.action.stop, or a step that comes due while a different document is active than after the step before → {results: [the first step's result], pending: the steps left}. An action with a stop, at any speed, plays up to the stop and waits until ui.action.continue or ui.action.stop; at Accelerated the steps between stops are one undoable batch each → {results, pending, atStop?: true when it already waits}. A name no action has, or one that more than one action in the sets looked in has, or an action played while another is still playing step by step or with pauses, is an error and nothing plays",
|
||||
),
|
||||
(
|
||||
crate::panels::actions::PLAY_AT,
|
||||
"Play Action",
|
||||
"",
|
||||
"{set: index, action: index} play the Actions panel's action at action in set (0 is the top set) as ui.action.play plays it, and return what ui.action.play returns. A click on an action's button in Button Mode runs this. A missing index, or one that is not a whole number or is past the last set or action, is an error and nothing plays",
|
||||
),
|
||||
(
|
||||
crate::panels::actions::BUTTON_MODE,
|
||||
"Button Mode",
|
||||
"",
|
||||
"{enabled?: bool} with true, the Actions panel shows every action of every set, in set order and then action order, as a button with its name in its Action Options color (no color: an ordinary button), without set names, steps or checkmarks, and a click on a button plays its action (ui.action.playAt); with false, it shows its list again; left out or null, it switches to the other view. The panel menu's Button Mode runs this and shows a check mark while it is on. The view is kept with the UI preferences. Turning it on while an action records, or an enabled that is not true, false or null, is an error and the view stays → {enabled}",
|
||||
),
|
||||
(
|
||||
crate::panels::actions::PLAYBACK_OPTIONS,
|
||||
"Playback Options",
|
||||
|
||||
@@ -32,8 +32,9 @@ pub struct Action {
|
||||
/// saved value that is not a function key loads as none.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_key")]
|
||||
pub key: Option<FunctionKey>,
|
||||
/// The action's Button Mode color, one of [`COLORS`], if one is assigned. The panel does not
|
||||
/// show it. A saved value that is not one of them loads as none.
|
||||
/// The action's Button Mode color, one of [`COLORS`], if one is assigned: the color of its
|
||||
/// button while the panel is in Button Mode ([`button_colors`]). A saved value that is not one
|
||||
/// of them loads as none.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_color")]
|
||||
pub color: Option<String>,
|
||||
}
|
||||
@@ -144,6 +145,40 @@ impl FunctionKey {
|
||||
/// The Button Mode colors an action can have, as (value, label).
|
||||
pub const COLORS: [(&str, &str); 5] = [("red", "Red"), ("yellow", "Yellow"), ("green", "Green"), ("blue", "Blue"), ("violet", "Violet")];
|
||||
|
||||
/// The fill of a Button Mode button in color `color`, one of [`COLORS`]: VectorCraft's own shades.
|
||||
fn button_fill(color: &str) -> Option<egui::Color32> {
|
||||
let rgb = egui::Color32::from_rgb;
|
||||
Some(match color {
|
||||
"red" => rgb(0xd4, 0x3f, 0x3f),
|
||||
"yellow" => rgb(0xf0, 0xc8, 0x3c),
|
||||
"green" => rgb(0x3f, 0xa8, 0x5a),
|
||||
"blue" => rgb(0x3d, 0x7b, 0xd6),
|
||||
"violet" => rgb(0x8e, 0x5a, 0xd0),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The relative luminance of `c`, as WCAG 2 defines it.
|
||||
fn luminance(c: egui::Color32) -> f32 {
|
||||
let channel = |v: u8| {
|
||||
let v = f32::from(v) / 255.0;
|
||||
if v <= 0.040_45 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) }
|
||||
};
|
||||
0.2126 * channel(c.r()) + 0.7152 * channel(c.g()) + 0.0722 * channel(c.b())
|
||||
}
|
||||
|
||||
/// The (fill, text) colors of the Button Mode button of an action with color `color`: a colored
|
||||
/// fill with black or white text, whichever contrasts more with it, or the theme's ordinary button
|
||||
/// for an action without a color.
|
||||
fn button_colors(color: Option<&str>, t: &Tokens) -> (egui::Color32, egui::Color32) {
|
||||
match color.and_then(button_fill) {
|
||||
// Black text contrasts more than white above this luminance.
|
||||
Some(fill) if luminance(fill) > 0.179 => (fill, egui::Color32::BLACK),
|
||||
Some(fill) => (fill, egui::Color32::WHITE),
|
||||
None => (t.button, t.text_strong),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serde reader for [`Action::key`]: a value that is not a function key is none.
|
||||
fn deserialize_key<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<FunctionKey>, D::Error> {
|
||||
let v = <Value as serde::Deserialize>::deserialize(d)?;
|
||||
@@ -888,6 +923,37 @@ pub fn play_action(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
|
||||
}
|
||||
}
|
||||
|
||||
/// The command that plays an action by its place in the panel ([`play_at`]).
|
||||
pub const PLAY_AT: &str = "ui.action.playAt";
|
||||
|
||||
/// `ui.action.playAt {set, action}`: plays the action at index `action` of set `set` (0 is the top
|
||||
/// set) at the Playback Options speed, as [`play`] does; a click on an action's button in Button
|
||||
/// Mode runs this. A missing index, or one that is not a whole number or is past the last set or
|
||||
/// action, is an error, and then nothing plays. → what [`play`] returns
|
||||
pub fn play_at(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let index = |k: &str| p.get(k).and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok());
|
||||
let (set, at) = index("set").zip(index("action")).ok_or("no such action")?;
|
||||
play(app, set, at)
|
||||
}
|
||||
|
||||
/// The command behind the panel menu's Button Mode ([`button_mode`]).
|
||||
pub const BUTTON_MODE: &str = "ui.action.buttonMode";
|
||||
|
||||
/// `ui.action.buttonMode {enabled?}`: with `enabled: true` the Actions panel shows every action of
|
||||
/// every set as a button in its color ([`Action::color`]) that plays the action when clicked, and
|
||||
/// with `false` it shows its list of sets, actions and steps again; left out or null, it switches
|
||||
/// to the other view. The view is kept with the UI preferences. Turning Button Mode on while an
|
||||
/// action records is an error, and so is an `enabled` that is not true, false or null; then the
|
||||
/// view does not change. → {enabled}
|
||||
pub fn button_mode(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let enabled = crate::menus::opt_bool(p, "enabled")?.unwrap_or(!app.ui.action_button_mode);
|
||||
if enabled && !app.ui.action_button_mode && app.ui.recording.is_some() {
|
||||
return Err("an action is already recording".into());
|
||||
}
|
||||
app.ui.action_button_mode = enabled;
|
||||
Ok(json!({ "enabled": enabled }))
|
||||
}
|
||||
|
||||
/// The command that ends a recording ([`stop`]).
|
||||
pub const STOP_RECORDING: &str = "ui.action.stop";
|
||||
|
||||
@@ -2007,9 +2073,18 @@ pub fn duplicate(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
|
||||
/// The panel's (≡) menu.
|
||||
pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
// Button Mode is off while an action records, so the Stop button the list shows stays in reach.
|
||||
let buttons = app.ui.action_button_mode;
|
||||
if widgets::menu_item(ui, tl!("Button Mode"), buttons || app.ui.recording.is_none(), buttons)
|
||||
&& let Err(e) = app.run(BUTTON_MODE, json!({}))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
ui.separator();
|
||||
// A selected action counts only while its set is the current one, as the panel shows it: a set
|
||||
// deleted since leaves the stored pair pointing at another set's action.
|
||||
let selected = pstate::<Option<(usize, usize)>>(ui.ctx(), "actions-selected").filter(|&(s, _)| app.ui.action_set == Some(s));
|
||||
// deleted since leaves the stored pair pointing at another set's action. Button Mode shows no
|
||||
// selected action and no current set, so the items that act on them are off there.
|
||||
let selected = pstate::<Option<(usize, usize)>>(ui.ctx(), "actions-selected").filter(|&(s, _)| !buttons && app.ui.action_set == Some(s));
|
||||
let has = selected.and_then(|(s, a)| app.ui.action_sets.get(s).and_then(|set| set.actions.get(a))).is_some();
|
||||
if widgets::menu_item(ui, tl!("New Action…"), crate::dialogs::action_new::can_open(app), false)
|
||||
&& let Err(e) = crate::dialogs::action_new::open(app)
|
||||
@@ -2019,11 +2094,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
if widgets::menu_item(ui, tl!("New Set…"), true, false) {
|
||||
crate::dialogs::action_set_options::open_new(app);
|
||||
}
|
||||
let current = app.ui.action_set.filter(|&s| s < app.ui.action_sets.len());
|
||||
let current = app.ui.action_set.filter(|&s| !buttons && s < app.ui.action_sets.len());
|
||||
if widgets::menu_item(ui, tl!("Duplicate"), current.is_some(), false) {
|
||||
duplicate_selection(app, ui.ctx());
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Delete"), has || !selected_sets(app).is_empty(), false) {
|
||||
if widgets::menu_item(ui, tl!("Delete"), has || (!buttons && !selected_sets(app).is_empty()), false) {
|
||||
delete_selection(app, ui.ctx());
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Set Options…"), current.is_some(), false)
|
||||
@@ -2156,8 +2231,78 @@ enum InclusionChange {
|
||||
Step(usize, usize, usize, bool),
|
||||
}
|
||||
|
||||
/// Draw action sets, steps and playback controls, keeping the list above a pinned footer.
|
||||
/// Button Mode ([`button_mode`]): every action of every set, in set order and then action order,
|
||||
/// as a button with its name in its color ([`button_colors`]), with no set names, steps or
|
||||
/// checkmarks. A click on a button, or Enter or Space on the focused one, plays the action
|
||||
/// ([`PLAY_AT`]). The footer shows only while an action plays or records: Stop Playing/Recording,
|
||||
/// and Play Current Selection while a playback waits at a stop.
|
||||
fn show_buttons(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut play_req = None;
|
||||
let list_height = widgets::panel_list_height(ui, 260.0, 0.0).min(260.0);
|
||||
egui::ScrollArea::vertical().id_salt("actions-buttons").max_height(list_height).min_scrolled_height(list_height.min(64.0)).show(ui, |ui| {
|
||||
for (si, set) in app.ui.action_sets.iter().enumerate() {
|
||||
for (ai, a) in set.actions.iter().enumerate() {
|
||||
let (r, resp) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 24.0), egui::Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &a.name));
|
||||
let (fill, text) = button_colors(a.color.as_deref(), &t);
|
||||
let button = r.shrink2(egui::vec2(2.0, 1.0));
|
||||
ui.painter().rect_filled(button, 3.0, fill);
|
||||
// The pointer over a button, or the keyboard focus on it, rings it in its text color.
|
||||
let (width, ring) = if resp.hovered() || resp.has_focus() { (2.0, text) } else { (1.0, t.button_border) };
|
||||
ui.painter().rect_stroke(button, 3.0, egui::Stroke::new(width, ring), egui::StrokeKind::Inside);
|
||||
ui.painter().with_clip_rect(button.shrink2(egui::vec2(6.0, 0.0))).text(
|
||||
button.left_center() + egui::vec2(8.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
&a.name,
|
||||
egui::FontId::proportional(12.5),
|
||||
text,
|
||||
);
|
||||
if resp.on_hover_text(&a.name).clicked() {
|
||||
play_req = Some((si, ai));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if app.ui.action_sets.iter().all(|s| s.actions.is_empty()) {
|
||||
dim_label(ui, tl!("No actions."));
|
||||
}
|
||||
let waiting = waiting_at_stop(app);
|
||||
if waiting {
|
||||
dim_label(ui, tl!("Press Play Current Selection in the Actions panel to go on."));
|
||||
}
|
||||
let recording = app.ui.recording.is_some();
|
||||
if recording || app.ui.action_playing.is_some() {
|
||||
widgets::bottom_bar(ui, |ui| {
|
||||
if widgets::icon_button(ui, "square", tl!("Stop Playing/Recording"), false, 24.0).clicked()
|
||||
&& let Err(e) = app.run(STOP_RECORDING, json!({}))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
if waiting
|
||||
&& widgets::icon_button(ui, "dc-actions", tl!("Play Current Selection"), false, 24.0).clicked()
|
||||
&& let Err(e) = app.run(CONTINUE_PLAYBACK, json!({}))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
if recording {
|
||||
ui.label(egui::RichText::new(tl!("● Recording")).color(egui::Color32::from_rgb(0xe0, 0x30, 0x30)));
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some((si, ai)) = play_req
|
||||
&& let Err(e) = app.run(PLAY_AT, json!({"set": si, "action": ai}))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw action sets, steps and playback controls, keeping the list above a pinned footer, or in
|
||||
/// Button Mode the actions as buttons ([`show_buttons`]).
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
if app.ui.action_button_mode {
|
||||
return show_buttons(app, ui);
|
||||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
// The selected action as (set, action). Read as the type it is stored as: egui keeps a
|
||||
// value per type, so reading the bare pair never found it and no action stayed selected (#1629).
|
||||
@@ -2672,6 +2817,186 @@ mod tests {
|
||||
assert_eq!(names(&app), before[1..], "nothing selected after a delete: nothing more goes");
|
||||
}
|
||||
|
||||
/// The fills of the rectangles drawn in one headless frame of the panel.
|
||||
fn drawn_fills(ctx: &egui::Context, app: &mut VectorcraftApp) -> Vec<egui::Color32> {
|
||||
fn collect(s: &egui::Shape, out: &mut Vec<egui::Color32>) {
|
||||
match s {
|
||||
egui::Shape::Rect(r) => out.push(r.fill),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui));
|
||||
output.textures_delta.clear();
|
||||
let mut fills = vec![];
|
||||
output.shapes.iter().for_each(|c| collect(&c.shape, &mut fills));
|
||||
fills
|
||||
}
|
||||
|
||||
/// The panel menu's Button Mode switches the panel to buttons and back, with a check mark while
|
||||
/// it is on; `ui.action.buttonMode` does the same, and the view is kept with the UI preferences.
|
||||
/// Turning it on while an action records is refused, so Stop stays in reach.
|
||||
#[test]
|
||||
fn button_mode_switches_the_view_and_is_kept_with_the_preferences() {
|
||||
use super::super::tests_appearance::{click, frame_events};
|
||||
let mut app = app_with_sets();
|
||||
assert!(!app.ui.action_button_mode, "the list is the default");
|
||||
let ctx = egui::Context::default();
|
||||
let item = |ctx: &egui::Context, app: &mut VectorcraftApp| {
|
||||
let texts = frame_events(ctx, app, vec![], menu);
|
||||
texts.into_iter().find(|(s, _)| s.trim_start_matches('✓').trim() == "Button Mode").expect("Button Mode in the menu")
|
||||
};
|
||||
let (label, at) = item(&ctx, &mut app);
|
||||
assert_eq!(label, " Button Mode", "no check mark while off");
|
||||
click(&ctx, &mut app, at.center(), menu);
|
||||
assert!(app.ui.action_button_mode, "the menu item turns it on");
|
||||
assert_eq!(item(&ctx, &mut app).0, "✓ Button Mode", "a check mark while on");
|
||||
let at = item(&ctx, &mut app).1;
|
||||
click(&ctx, &mut app, at.center(), menu);
|
||||
assert!(!app.ui.action_button_mode, "choosing it again returns to the list");
|
||||
|
||||
assert_eq!(app.run(BUTTON_MODE, json!({})).unwrap(), json!({"enabled": true}), "left out: the other view");
|
||||
assert_eq!(app.run(BUTTON_MODE, json!({"enabled": true})).unwrap(), json!({"enabled": true}));
|
||||
assert_eq!(app.run(BUTTON_MODE, json!({"enabled": "no"})).unwrap_err(), "`enabled` must be true or false");
|
||||
assert!(app.ui.action_button_mode, "a bad value keeps the view");
|
||||
let saved = serde_json::to_value(&app.ui).unwrap();
|
||||
assert_eq!(saved["action_button_mode"], json!(true));
|
||||
assert!(serde_json::from_value::<crate::state::UiState>(saved.clone()).unwrap().action_button_mode, "kept after a restart");
|
||||
let mut older = saved;
|
||||
older.as_object_mut().unwrap().remove("action_button_mode");
|
||||
assert!(!serde_json::from_value::<crate::state::UiState>(older).unwrap().action_button_mode, "older preferences show the list");
|
||||
|
||||
assert_eq!(app.run(BUTTON_MODE, json!({"enabled": null})).unwrap(), json!({"enabled": false}));
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
app.run(START_RECORDING, json!({})).unwrap();
|
||||
assert_eq!(app.run(BUTTON_MODE, json!({})).unwrap_err(), "an action is already recording");
|
||||
let at = item(&ctx, &mut app).1;
|
||||
click(&ctx, &mut app, at.center(), menu);
|
||||
assert!(!app.ui.action_button_mode, "the menu item is off while an action records");
|
||||
assert_eq!(app.run(BUTTON_MODE, json!({"enabled": false})).unwrap(), json!({"enabled": false}), "turning it off is never refused");
|
||||
app.run(STOP_RECORDING, json!({})).unwrap();
|
||||
}
|
||||
|
||||
/// Button Mode shows every action of every set by its name, in set order and then action order,
|
||||
/// without set names, step counts or steps, each in its Action Options color; an action without
|
||||
/// a color is an ordinary button. The menu items for a selected action or the current set are
|
||||
/// off, since the view shows neither.
|
||||
#[test]
|
||||
fn button_mode_shows_every_action_as_a_button_in_its_color() {
|
||||
use super::super::tests_appearance::{click, frame_events, text_rect};
|
||||
let mut app = app_with_sets();
|
||||
app.run(ACTION_OPTIONS, json!({"set": 1, "action": 0, "color": "red"})).unwrap();
|
||||
app.run(ACTION_OPTIONS, json!({"set": 1, "action": 1, "color": "yellow"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let texts = frame_events(&ctx, &mut app, vec![], show);
|
||||
click(&ctx, &mut app, text_rect(&texts, "Nudge").center(), show);
|
||||
assert_eq!(pstate::<Option<(usize, usize)>>(&ctx, "actions-selected"), Some((1, 0)), "selected in the list");
|
||||
app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
|
||||
let texts = frame_events(&ctx, &mut app, vec![], show);
|
||||
let names: Vec<&str> = texts.iter().map(|(s, _)| s.as_str()).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
"Duplicate and Offset 10 pt",
|
||||
"Unite and Simplify",
|
||||
"Center on Artboard",
|
||||
"Outline Text",
|
||||
"Rotate 90° CW",
|
||||
"Reflect Horizontal",
|
||||
"Nudge",
|
||||
"Nudge Again"
|
||||
],
|
||||
"only the actions' names, in set and action order"
|
||||
);
|
||||
let tops: Vec<f32> = texts.iter().map(|(_, r)| r.top()).collect();
|
||||
assert!(tops.windows(2).all(|w| w[0] < w[1]), "one button under the other: {tops:?}");
|
||||
let t = Tokens::get(&ctx);
|
||||
let fills = drawn_fills(&ctx, &mut app);
|
||||
for color in ["red", "yellow"] {
|
||||
let fill = button_fill(color).unwrap();
|
||||
assert_eq!(fills.iter().filter(|f| **f == fill).count(), 1, "{color}: {fills:?}");
|
||||
}
|
||||
assert_eq!(fills.iter().filter(|f| **f == t.button).count(), 6, "the six actions without a color: {fills:?}");
|
||||
for (color, _) in COLORS {
|
||||
let (fill, text) = button_colors(Some(color), &t);
|
||||
let (hi, lo) = (luminance(fill).max(luminance(text)), luminance(fill).min(luminance(text)));
|
||||
assert!((hi + 0.05) / (lo + 0.05) >= 4.5, "{color}: its name is legible");
|
||||
}
|
||||
assert_eq!(button_colors(None, &t), (t.button, t.text_strong));
|
||||
let menu_texts = frame_events(&ctx, &mut app, vec![], menu);
|
||||
click(&ctx, &mut app, text_rect(&menu_texts, " Delete").center(), menu);
|
||||
click(&ctx, &mut app, text_rect(&menu_texts, " Action Options…").center(), menu);
|
||||
assert!(app.ui.dialog.is_none(), "Delete and Action Options… are off in Button Mode");
|
||||
assert_eq!(set_names(&app), ["Default Actions", "Mine", "Other"]);
|
||||
assert_eq!(action_names(&app, 1), ["Nudge", "Nudge Again"], "the action selected in the list stays");
|
||||
}
|
||||
|
||||
/// A click on a button plays that action at once, as one undo step at Accelerated, without its
|
||||
/// excluded steps, which stay excluded; `ui.action.playAt` refuses an index that names no action.
|
||||
#[test]
|
||||
fn a_click_on_a_button_plays_its_action_without_its_excluded_steps() {
|
||||
use super::super::tests_appearance::{click, frame_events, text_rect};
|
||||
let (mut app, x) = app_with_three_moves();
|
||||
app.run(INCLUDE_STEPS, json!({"set": 0, "action": 0, "step": 1, "included": false})).unwrap();
|
||||
app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let texts = frame_events(&ctx, &mut app, vec![], show);
|
||||
let undo = undo_steps(&app);
|
||||
click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show);
|
||||
assert_eq!((x(&app), undo_steps(&app)), (5.0, undo + 1), "1 and 4 pt as one undo step; the 2 pt step is skipped");
|
||||
assert_eq!(app.ui.action_sets[0].actions[0].excluded_steps, BTreeSet::from([1]), "the exclusion stays");
|
||||
for (p, want) in [
|
||||
(json!({"set": 0, "action": 1}), "no such action"),
|
||||
(json!({"set": 1, "action": 0}), "no such action"),
|
||||
(json!({"set": "0", "action": 0}), "no such action"),
|
||||
(json!({"set": 0}), "no such action"),
|
||||
] {
|
||||
assert_eq!(app.run(PLAY_AT, p.clone()).unwrap_err(), want, "{p}");
|
||||
}
|
||||
assert_eq!(x(&app), 5.0, "nothing more played");
|
||||
app.run(PLAY_AT, json!({"set": 0, "action": 0})).unwrap();
|
||||
assert_eq!(x(&app), 10.0);
|
||||
}
|
||||
|
||||
/// Button Mode shows no bottom bar while nothing plays or records. While an action plays step by
|
||||
/// step its Stop Playing/Recording button ends the playback, and while one waits at a stop its
|
||||
/// Play Current Selection button plays on.
|
||||
#[test]
|
||||
fn button_mode_shows_stop_and_continue_only_while_an_action_plays() {
|
||||
use super::super::tests_appearance::{click, frame_events, text_rect};
|
||||
let bar = |ctx: &egui::Context, app: &mut VectorcraftApp| {
|
||||
frame_events(ctx, app, vec![], show);
|
||||
let mut bar = crate::toolbar::tests::widget_rects(ctx, egui::vec2(24.0, 24.0));
|
||||
bar.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
// A button can be listed more than once in a pass.
|
||||
bar.dedup();
|
||||
bar
|
||||
};
|
||||
let (mut app, x) = app_with_three_moves();
|
||||
app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
|
||||
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
assert_eq!(bar(&ctx, &mut app), [], "no bottom bar while idle");
|
||||
let texts = frame_events(&ctx, &mut app, vec![], show);
|
||||
click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show);
|
||||
assert_eq!(x(&app), 1.0, "the first step played");
|
||||
let [stop] = bar(&ctx, &mut app)[..] else { panic!("only Stop while it plays") };
|
||||
click(&ctx, &mut app, stop.center(), show);
|
||||
assert!(app.ui.action_playing.is_none(), "Stop ends the playback");
|
||||
assert_eq!(x(&app), 1.0);
|
||||
|
||||
let (mut app, x) = app_with_a_stop(true);
|
||||
app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
|
||||
let texts = frame_events(&ctx, &mut app, vec![], show);
|
||||
click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show);
|
||||
assert!(waiting_at_stop(&app));
|
||||
assert_eq!(x(&app), 1.0, "played up to the stop");
|
||||
let [_, play] = bar(&ctx, &mut app)[..] else { panic!("Stop and Play Current Selection at a stop") };
|
||||
click(&ctx, &mut app, play.center(), show);
|
||||
assert!(app.ui.action_playing.is_none());
|
||||
assert_eq!(x(&app), 7.0, "Play Current Selection plays on");
|
||||
}
|
||||
|
||||
/// #3712: Create New Set asks for a name (Set 1 when free), the new set is the current one, so a
|
||||
/// recording goes into it, and a click on another set's name makes that one current. The sets
|
||||
/// are saved with the UI preferences.
|
||||
|
||||
@@ -29,6 +29,17 @@ pub struct Timer {
|
||||
/// The recovery folders that failure messages named, latest last, at most [`NAMED`]: replies to
|
||||
/// automation clients hide those outside their read roots.
|
||||
pub named: Vec<String>,
|
||||
/// The restored documents whose linked files are still to be asked about, next first
|
||||
/// ([`ask_next`]).
|
||||
links: Vec<LinkQuestions>,
|
||||
}
|
||||
|
||||
/// The questions about the linked files of a restored document (by uid): the missing files, then
|
||||
/// the modified images (ids) to offer for update ([`crate::dialogs::missing_links::ask_next`]).
|
||||
struct LinkQuestions {
|
||||
uid: u64,
|
||||
missing: Vec<Value>,
|
||||
modified: Vec<Value>,
|
||||
}
|
||||
|
||||
/// How many recovery folders [`Timer::named`] keeps.
|
||||
@@ -187,28 +198,91 @@ fn named(app: &mut VectorcraftApp, folder: &str) {
|
||||
}
|
||||
|
||||
/// The startup dialog's OK (Restore) or Discard: every copy it lists.
|
||||
///
|
||||
/// After Restore the status bar shows how many documents were restored, followed by the first of
|
||||
/// the restored documents' notes: for each document in the dialog's order, the lines about its
|
||||
/// linked files, then what reading the copy left out, then its other warnings, each named with
|
||||
/// its document's title when more than one was restored. Hovering the message lists them all. The
|
||||
/// questions about the documents' linked files are asked one document at a time ([`ask_next`]).
|
||||
/// When a copy can't be restored, the status bar shows the error, and its hover lists the count
|
||||
/// and the notes of the documents that were restored.
|
||||
pub fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
app.ui.dialog = None;
|
||||
let id = if d.bool("discard") { "file.recovery.discard" } else { "file.recovery.restore" };
|
||||
let discard = d.bool("discard");
|
||||
let id = if discard { "file.recovery.discard" } else { "file.recovery.restore" };
|
||||
let files: Vec<String> =
|
||||
d.fields.get("copies").and_then(Value::as_array).into_iter().flatten().filter_map(|c| c["file"].as_str()).map(str::to_string).collect();
|
||||
let (mut done, mut errors) = (vec![], vec![]);
|
||||
let (mut done, mut errors, mut opened) = (vec![], vec![], vec![]);
|
||||
for file in files {
|
||||
match app.run(id, json!({ "file": file })) {
|
||||
Ok(r) => {
|
||||
if let Some(restored) = r["restored"].as_array().and_then(|v| v.first()) {
|
||||
crate::dialogs::missing_links::after_open(app, restored, Vec::new());
|
||||
for restored in r["restored"].as_array().into_iter().flatten() {
|
||||
opened.push(restored_notes(app, restored));
|
||||
}
|
||||
let failed = r["failed"].as_array().into_iter().flatten();
|
||||
errors.extend(failed.filter_map(|f| f["error"].as_str()).map(str::to_string));
|
||||
done.push(r);
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.join("; "));
|
||||
if discard {
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.join("; "));
|
||||
}
|
||||
app.status(recovered_status(true, done.len()));
|
||||
return Ok(json!(done));
|
||||
}
|
||||
let summary = recovered_status(false, opened.len());
|
||||
let titled = opened.len() > 1;
|
||||
let restored = !opened.is_empty();
|
||||
let notes: Vec<String> = opened
|
||||
.into_iter()
|
||||
.flat_map(|(title, notes)| {
|
||||
notes.into_iter().map(move |n| if titled { crate::i18n::fill_once("{path}: {e}", &[("path", &title), ("e", &n)]) } else { n })
|
||||
})
|
||||
.collect();
|
||||
let result = if errors.is_empty() {
|
||||
let status = match notes.first() {
|
||||
Some(first) => crate::io::opened_after_notice(&summary, notes.len(), first),
|
||||
None => summary,
|
||||
};
|
||||
crate::io::show_with_notes(app, status, notes);
|
||||
Ok(json!(done))
|
||||
} else {
|
||||
let e = errors.join("; ");
|
||||
if restored {
|
||||
crate::io::show_with_notes(app, e.clone(), std::iter::once(summary).chain(notes).collect());
|
||||
}
|
||||
Err(e)
|
||||
};
|
||||
crate::dialogs::settle(app);
|
||||
result
|
||||
}
|
||||
|
||||
/// A document Restore just opened (`file.recovery.restore`'s entry `r`, the current document) →
|
||||
/// its title and notes ([`confirm`]). Its Missing Fonts dialog and the questions about its linked
|
||||
/// files wait for their turn.
|
||||
fn restored_notes(app: &mut VectorcraftApp, r: &Value) -> (String, Vec<String>) {
|
||||
crate::dialogs::missing_fonts::after_open(app, r);
|
||||
let (mut notes, missing, modified) = crate::dialogs::missing_links::questions(app, r);
|
||||
let uid = r["index"].as_u64().and_then(|i| usize::try_from(i).ok()).and_then(|i| app.session.documents().get(i)).map(|st| st.uid);
|
||||
if let Some(uid) = uid.filter(|_| !missing.is_empty() || !modified.is_empty()) {
|
||||
app.recovery.links.push(LinkQuestions { uid, missing, modified });
|
||||
}
|
||||
notes.extend(crate::io::open_notes(app, r));
|
||||
(r["title"].as_str().unwrap_or_default().to_string(), notes)
|
||||
}
|
||||
|
||||
/// With no dialog open: the questions about the linked files of the next restored document that
|
||||
/// is still open, with that document made the current one (the answers change it).
|
||||
pub(crate) fn ask_next(app: &mut VectorcraftApp) {
|
||||
while app.ui.dialog.is_none() && !app.recovery.links.is_empty() {
|
||||
let q = app.recovery.links.remove(0);
|
||||
let Some(i) = app.session.documents().iter().position(|st| st.uid == q.uid) else { continue };
|
||||
crate::chrome::show_document(app, i);
|
||||
crate::dialogs::missing_links::ask_next(app, q.missing, q.modified, Vec::new());
|
||||
}
|
||||
app.status(recovered_status(id == "file.recovery.discard", done.len()));
|
||||
Ok(json!(done))
|
||||
}
|
||||
|
||||
/// The status bar's line for `n` recovered documents discarded or restored.
|
||||
|
||||
@@ -618,6 +618,11 @@ pub struct UiState {
|
||||
/// Shift-click on a set's name added. Delete Selection deletes them. Not saved.
|
||||
#[serde(skip)]
|
||||
pub action_sets_selected: Vec<usize>,
|
||||
/// The Actions panel shows its actions as buttons (panel menu › Button Mode,
|
||||
/// [`crate::panels::actions::button_mode`]) instead of its list; kept with the UI preferences,
|
||||
/// and off in preferences saved without it.
|
||||
#[serde(default)]
|
||||
pub action_button_mode: bool,
|
||||
/// Recording in progress: (set index, action name, journal length when recording started).
|
||||
#[serde(skip)]
|
||||
pub recording: Option<(usize, String, usize)>,
|
||||
@@ -1171,6 +1176,7 @@ impl Default for UiState {
|
||||
action_sets: crate::panels::actions::default_sets(),
|
||||
action_set: None,
|
||||
action_sets_selected: vec![],
|
||||
action_button_mode: false,
|
||||
recording: None,
|
||||
recording_into: None,
|
||||
recording_at: None,
|
||||
|
||||
@@ -115,6 +115,118 @@ fn launching_after_a_crash_offers_the_copies() {
|
||||
assert!(next.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
/// A 40×30 PNG of one color.
|
||||
fn png(rgb: [u8; 3]) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
let [r, g, b] = rgb;
|
||||
image::RgbaImage::from_pixel(40, 30, image::Rgba([r, g, b, 255])).write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
/// The copies a crash left of `n` documents, each with an image linked to a file in `dir`. The
|
||||
/// files are deleted and the images' bytes are gone from the store, so each copy restores with a
|
||||
/// warning about its image and a missing linked file. → the store.
|
||||
fn crashed_with_lost_images(dir: &std::path::Path, n: u8) -> Arc<MemoryStore> {
|
||||
let store = Arc::new(MemoryStore::default());
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services { recovery_store: Some(store.clone()), ..Default::default() });
|
||||
app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string())));
|
||||
for k in 0..n {
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let p = dir.join(format!("{k}.png"));
|
||||
std::fs::write(&p, png([200, k * 100, 0])).unwrap();
|
||||
app.run("file.place", json!({"path": p.to_string_lossy()})).unwrap();
|
||||
}
|
||||
app.run("file.recovery.save", json!({})).unwrap();
|
||||
drop(app);
|
||||
for k in 0..n {
|
||||
std::fs::remove_file(dir.join(format!("{k}.png"))).unwrap();
|
||||
}
|
||||
for name in store.list().unwrap().into_iter().filter(|n| n.ends_with(".blob")) {
|
||||
store.remove(&name).unwrap();
|
||||
}
|
||||
store
|
||||
}
|
||||
|
||||
/// The app launched after the crash, with the Recover Documents dialog open → the app and the
|
||||
/// titles the restored documents get, in the dialog's order.
|
||||
fn launched(store: &Arc<MemoryStore>) -> (VectorcraftApp, Vec<String>) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services { recovery_store: Some(store.clone()), ..Default::default() });
|
||||
recovery::frame(&mut app, 0.0);
|
||||
let d = app.ui.dialog.as_ref().expect("the Recover Documents dialog");
|
||||
let titles = d.fields["copies"].as_array().unwrap().iter().map(|c| format!("{} [Recovered]", c["title"].as_str().unwrap())).collect();
|
||||
(app, titles)
|
||||
}
|
||||
|
||||
/// Is `note` the warning about a restored document's image that couldn't be read?
|
||||
fn lost_image(note: &str) -> bool {
|
||||
note.starts_with("the image “") && note.contains("” of the recovered document couldn't be read")
|
||||
}
|
||||
|
||||
/// Restore keeps the warnings each restored document opened with: the status bar names the first
|
||||
/// after the count, and its hover lists them all, each named with its document. The questions
|
||||
/// about the missing linked files come one document at a time, with that document current.
|
||||
#[test]
|
||||
fn restore_keeps_each_documents_notes_and_asks_about_its_links_in_turn() {
|
||||
let dir = scratch("restore-notes");
|
||||
let store = crashed_with_lost_images(&dir, 2);
|
||||
let (mut app, titles) = launched(&store);
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
let (status, notes) = app.ui.status_notes.clone().expect("the notes are kept");
|
||||
assert_eq!(status, app.ui.status);
|
||||
assert_eq!(notes.len(), 2, "{notes:?}");
|
||||
for (note, title) in notes.iter().zip(&titles) {
|
||||
let lost = note.strip_prefix(&format!("{title}: ")).unwrap_or_default();
|
||||
assert!(lost_image(lost), "{note}");
|
||||
}
|
||||
assert_eq!(status, format!("Restored 2 recovered documents: save them to keep them. Opened with 2 notes: {}", notes[0]));
|
||||
// The first document's missing file is asked about first, and the second's waits.
|
||||
for title in &titles {
|
||||
let d = app.ui.dialog.clone().expect("the missing file's question");
|
||||
assert_eq!(d.kind, dialogs::missing_links::KIND);
|
||||
assert_eq!(&app.session.doc().unwrap().title(), title);
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
}
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.ui.status, status, "answering the questions keeps the line");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// With one document restored, its notes are not named with its title.
|
||||
#[test]
|
||||
fn restoring_one_document_shows_its_notes_after_the_count() {
|
||||
let dir = scratch("restore-one");
|
||||
let store = crashed_with_lost_images(&dir, 1);
|
||||
let (mut app, _) = launched(&store);
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
let (status, notes) = app.ui.status_notes.clone().expect("the notes are kept");
|
||||
assert!(notes.len() == 1 && lost_image(¬es[0]), "{notes:?}");
|
||||
assert_eq!(status, format!("Restored 1 recovered document: save it to keep it. Opened with 1 note: {}", notes[0]));
|
||||
assert_eq!(app.ui.status, status);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A copy that can't be restored after another was: the status bar shows the error, and its hover
|
||||
/// still lists the count and the notes of the document that was restored.
|
||||
#[test]
|
||||
fn a_failed_restore_keeps_the_notes_of_the_documents_restored_before_it() {
|
||||
let dir = scratch("restore-partial");
|
||||
let store = crashed_with_lost_images(&dir, 2);
|
||||
let (mut app, _) = launched(&store);
|
||||
let second = app.ui.dialog.as_ref().unwrap().fields["copies"][1]["file"].as_str().unwrap().to_string();
|
||||
store.write(&format!("{second}.vectorcraft"), b"not a document").unwrap();
|
||||
let e = dialogs::confirm(&mut app).unwrap_err();
|
||||
assert_eq!(app.session.documents().len(), 1);
|
||||
assert_eq!(app.ui.status, e);
|
||||
let (status, notes) = app.ui.status_notes.clone().expect("the notes are kept");
|
||||
assert_eq!(status, e);
|
||||
assert_eq!(notes.len(), 2, "{notes:?}");
|
||||
assert_eq!(notes[0], "Restored 1 recovered document: save it to keep it");
|
||||
assert!(lost_image(¬es[1]), "{}", notes[1]);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::missing_links::KIND), "its missing file is asked about");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discard_at_launch_deletes_the_copies_and_cancel_keeps_them() {
|
||||
let store = Arc::new(MemoryStore::default());
|
||||
|
||||
@@ -817,7 +817,12 @@ instance keeps, so instances started side by side for screenshots don't get it).
|
||||
saved}]`, from `file.recovery.list`) and `discard`. `ui.dialog.confirm` restores them all (`file.recovery.restore`
|
||||
each, opening "<name> [Recovered]" documents), `discard: true` then confirm deletes them, `ui.dialog.cancel` keeps them
|
||||
for the next launch. While copies are written in the background `ui.inspect` lists them under `background`
|
||||
("Saving recovery data for <name>").
|
||||
("Saving recovery data for <name>"). After a restore the status bar shows the count followed by the first of the
|
||||
restored documents' notes: each document's lines about its linked files, then its warnings (what reading the copy left
|
||||
out first), named with the document's title when more than one was restored. Hovering the message lists them all. The
|
||||
questions about each document's missing or modified linked files open one document at a time, with that document made
|
||||
the current one. When a copy can't be restored, confirm returns the error and the status bar shows it; its hover lists
|
||||
the count and the notes of the documents that were restored.
|
||||
|
||||
Native save options: File → Save As to a native or `.ai` file opens the `saveOptions` dialog after the save panel
|
||||
(heading "VectorCraft Options" or "PDF-compatible .ai Options"). Fields: `separateArtboards`, `all` (true: every
|
||||
|
||||
+17
-1
@@ -3299,7 +3299,8 @@ already walks Image Trace. Short agent entry points:
|
||||
trimmed) after the last action of set `set` (0 is the top set), starts recording into it and returns
|
||||
`{set, action}`; Stop Playing/Recording (`ui.action.stop`) ends the recording. `key` 1 to 15 is the function key F1 to F15 that plays the
|
||||
action (0 or null for none), with `cmd` (Ctrl, Command on macOS) and `shift`. `color` is its Button Mode color
|
||||
(`red`, `yellow`, `green`, `blue` or `violet`; null or `""` for none), which the panel does not show. Action function
|
||||
(`red`, `yellow`, `green`, `blue` or `violet`; null or `""` for none), the color of its button in Button Mode
|
||||
(`ui.action.buttonMode`). Action function
|
||||
keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6
|
||||
cannot be used. While an action is recording, and for an index past the last set,
|
||||
an empty name, or a key or color that is not one of these, it is an error and nothing is added. So is an action that
|
||||
@@ -3355,6 +3356,21 @@ already walks Image Trace. Short agent entry points:
|
||||
result is `{results}` with every step's result in order. A step that fails ends the playback with its error: the
|
||||
batch it is in changes nothing, and the steps before that batch stay done. While another action records, the View
|
||||
menu step is recorded too. Batch… refuses an action with an included View menu step.
|
||||
- Actions: `ui.action.playAt {set, action}` plays the action at `action` in set `set` (0 is the top set) as
|
||||
`ui.action.play` plays it and returns what `ui.action.play` returns. A click on an action's button in Button Mode
|
||||
runs this. A missing index, or one that is not a whole number or is past the last set or action, is an error, and
|
||||
then nothing plays.
|
||||
- Actions: `ui.action.buttonMode {enabled?}` with `enabled: true` shows the Actions panel's actions as buttons, each
|
||||
with the action's name in its Action Options color (`ui.action.options`), without set names, steps or checkmarks, as
|
||||
[the help page](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=467)
|
||||
describes; a click on a button plays its action (`ui.action.playAt`) without its excluded steps. With `false` the
|
||||
panel shows its list again, and with `enabled` left out or null the command switches to the other view. It returns
|
||||
`{enabled}`. The panel menu's Button Mode runs this. VectorCraft's own rules: the menu item shows a check mark while
|
||||
Button Mode is on; the actions of every set show, in set order and then action order; an action without a color is
|
||||
an ordinary button; the bottom bar shows only Stop Playing/Recording while an action plays or records, and Play
|
||||
Current Selection while one waits at a stop; the panel menu's items for a selected action or the current set are
|
||||
off; and the mode is kept with the UI preferences. Turning Button Mode on while an action records, or an `enabled`
|
||||
that is not true, false or null, is an error, and then the view does not change.
|
||||
- Actions: `ui.action.insertStop {set?, action?, step?, message?, allowContinue?}` inserts a stop. With `set` and
|
||||
`action`, the stop goes into that action, after step `step`, else after its last step, and the command returns
|
||||
`{set, action, step}`. Without them it goes into the recording in progress, after the steps recorded so far, and
|
||||
|
||||
@@ -621,7 +621,8 @@ since 2026-10-01.
|
||||
- **Placed gradients follow the Warp effects:** under each of the fifteen Warp styles with a bend or a distortion, a gradient placed with the Gradient tool moves and scales from the object's bounds before the warp to its bounds after it, as under Free Distort; a stroke's gradient uses those bounds inflated by the stroke. This holds for an object's own Warp and for a fill's or stroke's own, on the canvas and in Object › Expand of the fill. In a group with a Warp effect, each path's and compound path's gradients follow its own bounds, a nested group's own fills and strokes follow the nested group's bounds, and the group's own fills and strokes follow the group's bounds. In the art Pathfinder and compound shapes take, a symbol instance's Warp moves the gradients of its symbol's art by the art's bounds, and a compound shape's Warp moves its gradients by the bounds of its area. A gradient fitted to the object still fits the warped outline, and a warp with no bend and no distortion leaves the gradient exactly in place. Existing documents with a Warp effect on art with a placed gradient draw differently: before, the gradient stayed where it was while the outline changed. How gradients follow the warp is VectorCraft's own rule; what the Warp effects do to the object is described in [Reshaping with effects](https://helpx.adobe.com/illustrator/using/reshaping-objects-effects.html) (effects `tests::warp_bends_when_the_warp_moves_the_outline`, `bake::tests::baked_warps_move_placed_gradients_from_the_bounds_to_the_warped_bounds`, `bake::tests::a_warp_with_no_bend_leaves_a_placed_gradient_exactly_in_place`, `bake::tests::a_fills_and_a_strokes_own_warp_move_its_placed_gradient_with_the_bounds`, `group::tests::a_groups_warp_moves_the_gradients_placed_on_its_paths_by_each_paths_bounds`, `group::tests::a_groups_warp_moves_its_own_a_nested_groups_and_a_compound_paths_gradients_by_their_bounds`, `group::tests::shape_art_moves_the_gradients_of_a_symbol_instances_art_and_a_compound_shape_with_a_warp`; render `objectfx::a_paths_gradient_moves_and_scales_with_its_warped_bounds`; engine `objexpand::a_placed_gradient_moved_by_a_warp_expands_as_the_canvas_draws_it`). Scores and hours unchanged.
|
||||
- **Warp as a Bézier patch per style:** each of the fifteen Warp styles bends the object's bounds as one Bézier patch whose control points the style sets from the bend and the size of the bounds; the horizontal distortion then scales each column of control points about its middle and the vertical distortion scales each row. The Warp effects and warp envelopes share the map, and content with no width or no height takes the middle of the patch along that side. New warps, the built-in graphic styles' Warps and warps opened from `.ai` files carry `version` 1 for it. Warps in `.vectorcraft` documents saved before it read as `version` 0 and keep their earlier look, and edits keep a warp's version; a `version` 0 warp saved to `.ai` opens again with `version` 1, since the file holds only its settings. The patches are VectorCraft's own rules; the styles and ranges are those of the Warp options in [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html) (doc `live::tests::warp_nets_bend_each_style_as_its_own_patch`, `warp_net_arc_edges_are_concentric_circles`, `warp_net_distortion_scales_the_columns_then_the_rows`, `warp_net_takes_any_numbers`, `warp_net_bends_a_huge_box_without_overflowing`, `warp_envelopes_bend_flat_content_as_the_middle_of_the_patch`, `warp_envelopes_write_their_map_version_only_when_it_has_one`, `style_libs::tests::builtin_warp_styles_take_the_nets_map`; effects `tests::a_warp_draws_with_the_map_its_version_names`, `tests::a_warp_bends_a_flat_box_as_the_middle_of_its_patch`, `tests::new_warps_take_the_nets_version`; engine `effectedit::warps_keep_the_map_their_version_names`, `envelope::new_warp_envelopes_take_the_net_and_saved_ones_keep_their_map`; eps `ai_editing::tests::every_effect_a_graphic_style_holds_is_read_back`, `import::ai::tests::a_warp_envelope_opens_live`, `import::ai::tests::warp_envelopes_read_every_style`, `import::ai::style::tests::a_stack_of_fills_with_effects_of_their_own_is_read_as_itself`; ui-egui `dialogs::tests_warp::warp_options_edit_keeps_each_targets_map_version`). Scores and hours unchanged.
|
||||
- **Warp effects bend images:** a Warp effect on an image, or on a group that holds one, bends the image's pixels with its frame, as the Warp effect is documented to distort bitmap images in [Reshaping with effects](https://helpx.adobe.com/illustrator/using/reshaping-objects-effects.html): after the last effect, the image is cut into 10 × 10 cells of two triangles, each a clipped piece under its own affine map. Effects after the Warp reshape only the frame, a second Warp bends the same pieces, and warps saved without a version bend the pixels with their earlier map, as they bend the frame. A placed gradient on a group that holds the image follows the group's bounds as the pieces draw them. SVG writes the image once for the pieces; PDF, EPS, `.ai` and PNG write the bent pieces. The images stay unbent when their pieces would make the art pass 2 million objects, and so does an image whose frame keeps its shape past the anchor budget. A grayscale image's fill color now colorizes the pieces of a warp or an envelope, and an image under any geometry effect keeps that color and its smoothing setting. Existing documents with a Warp effect on an image, or on a group that holds one, draw differently: before, the image showed unbent, cut by the bent frame. So do existing documents with a colorized image under an envelope or a geometry effect, or an image drawn without smoothing under a geometry effect: before, those lost the color or the setting. The cell count and the limits are VectorCraft's own (effects `tests::warped_images_stay_within_the_object_budget`, `tests::warped_images_keep_document_keys_and_hidden_images_apart`, `tests::an_image_whose_frame_passes_the_budget_stays_whole`, `tests::effects_after_a_warp_reshape_an_images_frame_and_not_its_pieces`, `tests::an_images_pixels_take_the_map_their_warps_version_names`, `tests::an_image_under_a_geometry_effect_keeps_its_color_and_smoothing`, `tests::a_nested_groups_gradient_follows_its_warped_image`; engine `warp_images::an_images_pixels_bend_with_effect_warp`, `a_warped_image_keeps_its_bend_in_svg`, `a_warped_image_keeps_its_bend_in_pdf`, `a_warped_image_keeps_its_bend_in_ai`, `a_warped_image_keeps_its_bend_in_eps`, `a_warped_image_keeps_its_bend_in_png`, `svg_writes_an_image_clipped_twice_in_a_users_group_twice`, `expand_appearance_of_a_warped_image_draws_as_the_canvas_did`, `a_colorized_image_keeps_its_color_when_warped`, `a_warp_on_a_group_bends_the_image_in_it`, `envelope_distort::a_colorized_image_keeps_its_color_in_an_envelope`). Scores and hours unchanged.
|
||||
- **`.ai` files keep an image's Warp live:** an embedded image whose appearance is one Warp effect, with no opacity mask, is saved to the `.ai` editing data with an art style of that Warp: its bent pieces are its drawn look, and the image itself follows on `%_` lines, as for other styled objects. Opening an `.ai` file reads such an image as the image with its Warp live, its pixels, placement, name and opacity kept, when the live warp draws as the file's drawn look and the import has a comparison left; otherwise the drawn look stays, with the drawn-look note. The Warp effect applies to bitmap images ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html)), and native files keep live effects ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/supported-file-formats-in-illustrator.html)). A linked image with a Warp is saved as its drawn art, as before, and its live Warp is not saved. Images with an opacity mask, and images with a Warp and another effect or with another geometry effect, are saved and opened as their drawn art. Existing `.ai` files with an embedded image on `%_` lines open differently: before, that image also came in beside its drawn look. Which images keep the Warp and the comparison are VectorCraft's own rules (eps `ai_editing::tests::an_image_with_a_warp_reads_back_with_its_warp_live`, `an_image_whose_warp_draws_otherwise_than_its_look_keeps_its_look`, `an_image_on_hidden_lines_does_not_come_in_beside_its_look`, `other_warped_images_are_written_as_their_drawn_art`, `import::ai::read::ops::tests::an_image_takes_a_style_of_one_warp_alone`). Scores and hours unchanged.
|
||||
- **`.ai` files keep an image's Warp live:** an embedded image whose appearance is one Warp effect, with no opacity mask, is saved to the `.ai` editing data with an art style of that Warp: its bent pieces are its drawn look, and the image itself follows on `%_` lines, as for other styled objects. Opening an `.ai` file reads such an image as the image with its Warp live, its pixels, placement, name and opacity kept, when the live warp draws as the file's drawn look and the import has a comparison left; otherwise the drawn look stays, with the drawn-look note. The Warp effect applies to bitmap images ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html)), and native files keep live effects ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/supported-file-formats-in-illustrator.html)). Images with an opacity mask, and images with a Warp and another effect or with another geometry effect, are saved and opened as their drawn art. Existing `.ai` files with an embedded image on `%_` lines open differently: before, that image also came in beside its drawn look. Which images keep the Warp and the comparison are VectorCraft's own rules (eps `ai_editing::tests::an_image_with_a_warp_reads_back_with_its_warp_live`, `an_image_whose_warp_draws_otherwise_than_its_look_keeps_its_look`, `an_image_on_hidden_lines_does_not_come_in_beside_its_look`, `other_warped_images_are_written_as_their_drawn_art`, `import::ai::read::ops::tests::an_image_takes_a_style_of_one_warp_alone`). Scores and hours unchanged.
|
||||
- **`.ai` files keep a linked image's Warp live and its link:** a linked image whose appearance is one Warp effect, with no opacity mask, is saved to the `.ai` editing data with an art style of that Warp, as an embedded one is: its bent pieces are its drawn look, and the image itself follows on `%_` lines as a placed file with its path, its folder relative to the document's and its preview. Before, it was saved as its drawn art and its live Warp was lost. Opening the file reads it as the linked image with its Warp live, its link, pixels, placement, name, opacity, hide and lock kept, under the same drawn-look comparison; a hidden image with a Warp, embedded or linked, is now compared with its look drawn shown, and keeps its Warp too. A linked image with a Warp in an opacity mask's art keeps its Warp and is embedded, with the existing warning for linked images in masks. Linked files stay connected to their source and track it by relative path ([the help page](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/manage-linked-and-embedded-files/links-panel-overview.html)); the placed-file form is the one the `.ai` reader reads (`crates/eps/src/import/ai/read/art.rs` `end_place`). eps `ai_editing::tests::a_linked_image_with_a_warp_reads_back_linked_with_its_warp_live`, `a_large_linked_image_with_a_warp_reads_back_from_its_preview`, `a_hidden_or_locked_image_with_a_warp_keeps_its_state`, `a_linked_image_with_a_warp_in_a_mask_is_embedded_with_its_warp_live`. Scores and hours unchanged.
|
||||
- **EPS writes a repeated image's samples once:** an image drawn more than once in an EPS file (the pieces of an envelope or a Warp effect, copies of one image, or one image in a symbol's instances) has its compressed samples written once in the setup, and each placement reads them from there: a reusable stream rewound for each placement at Language Level 3 ([PostScript Language Reference, 3rd ed., §3.13](https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf)), and an array of strings of at most 49,152 bytes fed to the image by a procedure at Level 2. An image drawn once is written where it is drawn, as before. The 256 MB limit on the page counts the samples once, so an envelope of a large photo no longer loses pieces to it. A colorized image is reused per ink (eps `tests::an_image_placed_twice_writes_its_samples_once_at_both_levels`, `tests::an_envelopes_image_pieces_draw_one_copy_of_its_samples`, `tests::each_ink_of_a_colorized_image_placed_twice_is_written_once`, `tests::samples_longer_than_a_string_are_reused_at_both_levels`, `tests::the_page_limit_counts_a_repeated_images_samples_once`). Scores and hours unchanged.
|
||||
- **PDF writes a warped image once:** with downsampling on, the pieces of an image bent by a Warp effect are resampled for the widest and the highest size any piece places the image at, so every piece paints one image XObject (a colorized image: one colorized image and its soft mask), instead of one image for each size a piece is placed at. Another placement of the same image is still resampled for its own size. Without downsampling, a colorized image and a CMYK image with a grayscale image mask on its grid are decoded and encoded once for all the sizes they are placed at (pdf `tests::compression::a_warped_image_is_written_once_for_all_its_pieces`, `tests::colorize::a_colorized_warped_image_is_written_once_for_all_its_pieces`, `export::images::tests::colorized_and_grid_masked_images_are_made_once_for_every_size_without_downsampling`). Scores and hours unchanged.
|
||||
- **Distort Appearance with Transform effect options:** with Envelope Options › Distort Appearance on, a path whose Transform effect has Scale Strokes & Effects or Transform Patterns on, or copies it with a gradient, is bent as the same per-copy art it draws and expands as outside the envelope: each copy's own stroke weight, dashes, pattern placement and gradient (effects `bake::tests`). Scores and hours unchanged.
|
||||
@@ -767,7 +768,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Actions panel menu: Save Actions… does not write a set with a step that sets a folder preference (the Fonts, Plug-ins, Templates and Recovery folders and the two scratch disks) to a folder, and names the action and the preference instead; an empty folder and the scratch disk choices Startup and None save. Export Preferences leaves out both scratch disks, and `ui.inspect` under automation read roots leaves such a folder outside them out of recorded steps. These are VectorCraft's own rules (`ui.actionSet.save`, `prefs.export`).
|
||||
- Actions panel menu: Load Actions… also reads action sets saved as `.aia` files ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `actionSet.read`, `ui.actionSet.load`). VectorCraft's own rules: the set comes in with all its actions. A step with a label and no recorded values comes in as the one menu command with the step's label, when that command is directly in a top menu, is what its menu bar item runs with no values and opens no dialog. A step that is off in the file stays excluded from playback. The other steps, recorded menu items included, are left out, and the status bar says how many.
|
||||
- Actions panel: the panel menu's Action Options… renames the selected action and keeps its steps ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html): the menu item opens a dialog that renames the action or gives it a function key and color; `ui.action.options`). It also sets the action's function key and color.
|
||||
- Actions panel: Create New Action (bottom bar) and the panel menu's New Action… ask for the new action's name, set, function key and color, and Record adds the action and starts recording into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html): the New Action dialog takes the action's name and the set it goes in; `ui.action.new`, `ui.action.play`). The function key (F1 to F15, with Ctrl or Command and Shift) plays the action. An action's function key plays the action even when a command has the same shortcut, and the command keeps it. VectorCraft's own rules, which the page does not cover: when several actions have the same key the first in panel order plays; a copied action has no key; New Action does not open while an action is recording. The color is kept with the action for Button Mode, which VectorCraft does not have yet.
|
||||
- Actions panel: Create New Action (bottom bar) and the panel menu's New Action… ask for the new action's name, set, function key and color, and Record adds the action and starts recording into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html): the New Action dialog takes the action's name and the set it goes in; `ui.action.new`, `ui.action.play`). The function key (F1 to F15, with Ctrl or Command and Shift) plays the action. An action's function key plays the action even when a command has the same shortcut, and the command keeps it. VectorCraft's own rules, which the page does not cover: when several actions have the same key the first in panel order plays; a copied action has no key; New Action does not open while an action is recording. The color is the color of the action's button in Button Mode.
|
||||
- Actions panel: the panel menu's Button Mode shows the actions as buttons with their names in their Action Options colors, without sets or individual commands; a click on a button plays its action without its excluded steps, and choosing Button Mode again returns to the list ([the help page](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=467) and [the next page](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=468); [the help page](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/exclude-commands-from-actions.html) says commands cannot be excluded in Button Mode; `ui.action.buttonMode`, `ui.action.playAt`). VectorCraft's own rules, which the pages do not cover: the menu item shows a check mark while Button Mode is on; every set's actions show, in set order and then action order; an action without a color is an ordinary button; function keys still play their actions; the bottom bar shows only Stop Playing/Recording while an action plays or records, and Play Current Selection while one waits at a stop; Button Mode cannot be turned on while an action records; and the mode is kept with the UI preferences.
|
||||
- Actions panel: with an action selected, Begin Recording adds the commands recorded to that action, after its selected step or at its end, and a step dragged within its action or to another action goes where a highlighted line shows when the button is released ([the help page](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/add-commands-to-actions.html); `ui.action.record`, `ui.action.moveStep`). VectorCraft's own rules, which the page does not cover: with no action selected Begin Recording adds a new action; a step can go into an empty action or an action in another set; a release off the actions' rows leaves the step where it was; a moved step keeps whether it is excluded; Undo does not move it back.
|
||||
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
|
||||
- Actions panel: the current set is kept with the UI preferences, so after a restart a recording goes into the set that was current, not into the first set. With no set current, Begin Recording records into the first set and makes it current.
|
||||
@@ -888,6 +890,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Hindi wording (2026-11-13): rows outside the plug-in messages keep their English's meaning, quantities and clauses, and use the catalog's save, swatch, crop, resolution, path and type terms; a test checks those terms and the point unit in every row; native review remains; scores and estimates unchanged.
|
||||
- Hindi terms (2026-11-13): every row that names a variable (data merge, variable fonts and widths, environment variables) says वेरिएबल, and joining paths, anchor points, point type and lines says जोड़ें instead of शामिल हों, which stays for joining the community; a test checks both terms in every row; scores and estimates unchanged.
|
||||
- Hindi English words (2026-11-13): 264 rows no longer keep English words in their sentences, repeat a parameter's name in Hindi or garble them, among them "unknown align", which now names the alignment; lists of accepted values such as `(left|right|top|bottom)` stay as their English writes them; two tests check every row; native review remains; scores and estimates unchanged.
|
||||
- Message field names and values (2026-11-13): 49 status and error messages in the Hindi, Bulgarian, Danish, Indonesian, Dutch, Brazilian Portuguese, Swedish and Turkish catalogs keep the field names in brackets, such as `[subpath, anchor]`, and the values an `unknown` message lists, such as `(points, lines)`, as their English writes them; a test checks every catalog; scores and estimates unchanged.
|
||||
- Italian messages (2026-11-13): the Italian catalog (`it`) has 1,802 more status and error message rows, and the untranslated report's missing Italian messages go from 1,842 to 40; the translations are VectorCraft's own and await native review; scores and estimates unchanged. See [localization-parity.md](localization-parity.md).
|
||||
- Window › Help Bar shows or hides the tool-help strip at the bottom of the window apart from the status bar (`window.helpBar`). Each setting is kept with the UI preferences and in saved workspaces, and Presentation Mode hides both bars. Layouts saved before the setting existed show the Help Bar where they show the status bar (`help_bar_toggle_is_independent_and_saved_in_workspaces`, `help_bar_and_status_bar_render_independently`, `legacy_help_bar_visibility_follows_each_layouts_status_bar`, `a_help_bar_setting_the_file_lacks_is_saved`).
|
||||
- Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md).
|
||||
@@ -1626,6 +1629,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Preferences › File Handling › Data Recovery › Folder has a Choose… button on the desktop that picks the folder Data Recovery keeps its copies in ([Illustrator reference, Set Data Recovery preferences](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf)); a picked folder that Data Recovery cannot create its folders in or list is refused with a status message, and the field keeps its value. OK applies the same check to a folder typed into the field (VectorCraft's own rule): when it fails, the dialog stays open, the status bar says why and no preference changes; an empty field (the default folder) and the folder already saved are not checked. Preferences › Plug-ins & Scratch Disks is where additional plug-ins folders are set ([Illustrator preferences help](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/set-app-preferences.html)); its Additional Plug-ins Folder has a Choose… button on the desktop as well, VectorCraft's own, matching Data Recovery's, and VectorCraft's own rule refuses a picked or typed folder whose contents can't be listed. Additional Fonts Folder and Templates Folder stay typed only.
|
||||
`prefs.set` applies the dialog's check to `recoveryFolder` and `pluginsFolder` for every caller (VectorCraft's own rule): a folder that fails returns an error and no preference changes; an empty value and the folder already saved are not checked.
|
||||
When a Data Recovery copy can't be written, the status bar and the log show the error (with the document's title when one document's copy fails), the folder copies go to, whether that is the default recovery folder or the folder set in Preferences, and the preference that picks another folder (`recoveryFolder`; VectorCraft's own message).
|
||||
After Restore in the dialog Data Recovery shows at launch, the status bar keeps the restored documents' notes (VectorCraft's own rule): it shows the count followed by the first note, and hovering it lists each document's lines about its linked files and its warnings, named with the document's title when more than one was restored. The questions about the documents' missing or modified linked files come one document at a time, with that document made the current one, and a copy that can't be restored shows its error while the hover keeps the count and the restored documents' notes (`tests/recovery.rs`).
|
||||
A name typed in the desktop save panel without the file type's extension gets it, and when a file of that name exists, which the panel did not ask about, Save, Save As, Save a Copy, Save as Template, the exports and every other write through a save panel ask before replacing it (dialog `replaceFile`; Cancel writes nothing).
|
||||
A name returned by the desktop save panel with the extension of a format the save does not write has it replaced with the saved format's: Save As of a new document saves `card.ait` and `card.png` as `card.vectorcraft`, and Save as Template saves `card.pdf` as `card.vctemplate`. An extension that names no format stays part of the name (`notes.v2` is saved as `notes.v2.vectorcraft`).
|
||||
Export As, Save as PDF, Save for Office Documents, Save for Web, Text Export Options, print to file, the presets' Export… and the library, swatch and Document Info saves replace a typed extension of another format than the one they write in the same way: Export As PNG writes `card.svg` as `card.png`, Save as PDF writes `card.png` as `card.pdf`, and `document.exportSelection` with a `format` suggests and adds that format's extension. An extension of the written format stays as typed in any letter case (`photo.JPEG` for JPEG), Export As SVG writes `art.svgz` compressed, and `document.exportSelection` without a `format` writes the format the typed extension names (`tests/saveext.rs`).
|
||||
|
||||
+3
-2
@@ -408,11 +408,12 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Toolbar without group labels by default, one or two columns; All Tools drawer by flyout group with shortcuts, its menu (Reset, Basic, Advanced) and Show section | done | #1751; a preference brings the labels back; `toolbar.show` sets the Fill/Stroke, Coloring, Draw Mode and Screen Mode controls |
|
||||
| Actions panel: ≡ › Save Actions… saves the current set with all its actions to a file and ≡ › Load Actions… adds a saved set ([Manage a set of actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/manage-a-set-of-actions.html)) | partial | `ui.actionSet.save`, `ui.actionSet.load`; `panels/actions.rs` tests. The file is VectorCraft's own `.vcactions` JSON; Reset, Clear and Replace Actions remain |
|
||||
| Actions panel: ≡ › Load Actions… also reads action sets saved as `.aia` files ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html)) | partial | `actionSet.read`, `ui.actionSet.load`; `tests/actionset.rs`, `panels/actions.rs` tests, `import_fuzz`. Only steps with a label and no recorded values come in, as menu commands that run with no values and open no dialog, matched by label; the other steps, recorded menu items included, are left out and counted |
|
||||
| Actions panel: ≡ › Action Options… renames the selected action and sets its function key and color ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html)) | partial | `ui.action.options`; `panels/actions.rs` tests (part of #1629). The color shows nowhere until Button Mode exists |
|
||||
| Actions panel: ≡ › Action Options… renames the selected action and sets its function key and color ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html)) | partial | `ui.action.options`; `panels/actions.rs` tests (part of #1629). Button Mode shows the color |
|
||||
| Actions panel: ≡ › Button Mode shows each action by name as a button in its Action Options color, without sets or steps; a click on a button plays the action, and choosing Button Mode again shows the list ([the help page](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html)) | done | `ui.action.buttonMode`, `ui.action.playAt`; `panels/actions.rs` tests. VectorCraft's own rules: every set's actions show in set order, an action without a color is an ordinary button, the mode is kept with the UI preferences, and it cannot be turned on while an action records |
|
||||
| Actions panel: ≡ › Playback Options… plays actions Accelerated, Step by Step or with a pause between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html)) | done | `ui.action.playbackOptions`; `panels/actions.rs` tests. The pause is a whole number of seconds from 1 to 60 (VectorCraft's own range); Batch… plays at full speed |
|
||||
| Actions panel: a clicked action stays selected for Play Current Selection and Delete Selection | partial | #1853 (part of #1629); editing actions and action shortcuts remain |
|
||||
| Actions panel: Create New Set (bottom bar, ≡ › New Set…) names a new set and Set Options… renames the current one; a click on a set's name makes it current and a recording goes into the current set ([helpx: Automation with actions](https://helpx.adobe.com/illustrator/using/automation-actions.html)); the current set is kept with the UI preferences, and with no set current a recording goes into the first set, which becomes current | partial | #3712; dragging actions between sets remains |
|
||||
| Actions panel: Create New Action (bottom bar, ≡ › New Action…) asks for the name, set, function key and color, and Record adds the action and records into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html)) | partial | `ui.action.new`, `ui.action.play`; `panels/actions.rs` tests. A function key plays its action, even when a command has the same shortcut, and the command keeps it. Button Mode, which shows the color, remains |
|
||||
| Actions panel: Create New Action (bottom bar, ≡ › New Action…) asks for the name, set, function key and color, and Record adds the action and records into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html)) | partial | `ui.action.new`, `ui.action.play`; `panels/actions.rs` tests. A function key plays its action, even when a command has the same shortcut, and the command keeps it. Button Mode shows the color |
|
||||
| Actions panel: Delete Selection (bottom bar) deletes the selected set with its actions and keeps the other sets ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html)) | partial | `ui.actionSet.delete`; `panels/actions.rs` tests. VectorCraft's own rules, which the overview does not cover: deleting a set asks first (a selected action is still deleted at once), Shift-click on a set's name selects a range of sets, ≡ › Delete does the same as the button, and Default Actions and the last set can be deleted. Undo does not restore a deleted set, and recorded commands cannot be selected or deleted |
|
||||
| Actions panel: ≡ › Duplicate copies the selected action or set ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html)) | partial | `ui.actionSet.duplicate`; `panels/actions.rs` tests. VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected, and a copied set holds copies of all its actions. Undo does not remove the copy, and recorded commands cannot be duplicated |
|
||||
| Properties panel per context (every selection type's Quick Actions) | partial | one clip group: Edit Clipping Path and Edit Contents, at the top of the panel rather than in Quick Actions (G1); the overview's additional controls ([Properties panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html)): Blend Options (one blend selected, with any tool, #4817), Repeat Options (one radial, grid or mirror repeat selected: Object › Repeat › Options, #5181; the overview does not list it) and Merge Live Paint (a Live Paint group with more) in Quick Actions, the Width Profile list (Width tool or a variable-width stroke) and an applied effect's Delete icon in Appearance, Corner and Corner Radius for corners selected with Direct Selection; Variable Font was already there; Edit Artboards: a Transform section (reference point, X, Y, W, H, preset, orientation) above the Artboards section (#5219) |
|
||||
|
||||
Reference in New Issue
Block a user