diff --git a/Cargo.lock b/Cargo.lock index 00fe4ca454..476af0dc89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6386,6 +6386,7 @@ dependencies = [ "pollster 1.0.1", "raw-window-handle", "rfd", + "rustix 1.1.6", "serde_json", "socket2", "tempfile", diff --git a/ROADMAP.md b/ROADMAP.md index fe038ec97a..346c532277 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -18,6 +18,8 @@ gate: all six core workflows (path drawing and editing, paint and style, type, l production, file exchange) work end to end on macOS with save and reopen, and only the last is partial, without blocking it ([Alpha gate](docs/roadmap.md#alpha-gate)). +> **Gujarati status (2026-11-14, #6613):** 4,673 catalog rows: the original 4,286 remain unchanged; all 387 additions are contributor-reviewed and approved, with 11 wording corrections. Status/error messages remain partial, and the pinned CI/release fonts and web startup list contain no Gujarati face; web selection stays unavailable until a Gujarati interface face is bundled. Scores and estimates unchanged. + ## Headline numbers | | Value | Kind | @@ -96,6 +98,7 @@ Weights, evidence and the missing items per area: [target-app-parity.md](docs/ta | Simplified Chinese | `zh-hans` | partial (messages in English) | 85% | | Spanish | `es` | full | 100% | | Hindi | `hi` | catalog complete; machine-assisted draft, native review pending | complete under the scanner | +| Gujarati | `gu` | 4,673 contributor-reviewed catalog rows; 11 corrections applied to the 387 additions | menus and dialogs; status/error messages partial | | Arabic | `ar` | none (no right-to-left interface) | 0% | | French | `fr` | full | 100% | | Portuguese (Brazil) | `pt-br` | full | 100% | @@ -218,6 +221,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. | | 2026-11-10 | minor | Tidy-up, no content change: the unheaded change bullets at the top of the file and a revision row inside the headline-numbers table move to this table under their commit dates. Scores and estimates unchanged. | | 2026-11-10 | minor | Raster effects: Effect Gallery thumbnails and an in-dialog preview (an original drawing through each filter). Features-table Raster effects 85% → 88%, hours 1–2 → none. Headline numbers unchanged, so the progress log is unchanged. | | 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): a menu for Make, Release and Expand Compound Shape and a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons create compound shapes on a click. Scores and estimates unchanged. | @@ -226,6 +230,8 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | 2026-11-03 | minor | UI: Window → Help Bar toggles the tool-help strip separately from the status bar and saves visibility with workspaces. Scores and estimates unchanged. | | 2026-11-03 | minor | Docked Tools stays between left-side panels and the canvas; redocking accepts canvas and window edges. Requested layout choice; scores and estimates unchanged. | | 2026-11-02 | minor | Selection QoL: saved marquee modes, screen drag direction with a 3 px dead zone, and translated mode/E feedback (#5313); VectorCraft addition, scores and estimates unchanged. | +| 2026-11-12 | minor | Gujarati (`gu`): reviewed by a native speaker; zero/one singular plurals (CLDR); Gujarati craft-fonts faces join the UI fonts; strings added on main since branching translated (4,286 rows) | +| 2026-11-11 | minor | Gujarati (`gu`): 4,235-row machine-assisted interface catalog, one/other plurals, desktop Gujarati font fallback; messages not yet translated; native review pending | | 2026-11-06 | minor | Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged | | 2026-11-05 | minor | Progress log row for the Type Tools add-on's Release Type on a Path (#5532); headline numbers unchanged. | | 2026-11-03 | minor | Progress log row for the Type Tools add-on's Text Tweaker (#4924); headline numbers unchanged. | diff --git a/apps/vectorcraft/Cargo.toml b/apps/vectorcraft/Cargo.toml index 2da5f0083d..9a1b11e350 100644 --- a/apps/vectorcraft/Cargo.toml +++ b/apps/vectorcraft/Cargo.toml @@ -110,6 +110,10 @@ wayland-protocols = { version = "0.32", features = ["client", "unstable"] } # wayland-protocols' `unstable` set) made on the window's own connection. MIT, already in the build # through winit and rfd. wayland-backend = { version = "0.3", features = ["client_system"] } +# The Wayland clipboard's pipes (src/clipboard.rs): `poll` and the nonblocking flag give up a read +# or send when the other app moves no data for a while. Apache-2.0 WITH LLVM-exception OR +# Apache-2.0 OR MIT, already in the build through the workspace. +rustix = { workspace = true, features = ["std", "event", "fs"] } # The save dialog's file type (src/portal_save.rs, #2679): the desktop portal over D-Bus, on the # systems where rfd shows the portal's dialog. MIT, already in the build through accesskit. Pinned diff --git a/apps/vectorcraft/src/clipboard.rs b/apps/vectorcraft/src/clipboard.rs index 77a96442c4..17b9327956 100644 --- a/apps/vectorcraft/src/clipboard.rs +++ b/apps/vectorcraft/src/clipboard.rs @@ -1790,15 +1790,17 @@ mod portable { /// starts a thread for each paste of a Copy's data by another app ([`MAX_SENDS`] at once). Reads /// run on another worker and take [`MAX_BYTES`] at most; text waits [`LOAD_WAIT`] at most, SVG and /// PNG as long as a paste's bitmap from arboard. A read that gets no answer in its wait is kept for -/// a paste to wait for off the UI thread, as arboard's are. +/// a paste to wait for off the UI thread, as arboard's are. A read or send whose pipe moves no data +/// for [`IDLE_WAIT`] fails and closes the pipe, which frees its worker or thread. #[cfg(target_os = "linux")] mod wayland { - use std::io::{Read, Write}; - use std::os::fd::AsFd as _; + use std::io::{ErrorKind, Read, Write}; + use std::os::fd::{AsFd, BorrowedFd}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError, mpsc}; use std::time::{Duration, Instant}; + use rustix::event::{PollFd, PollFlags, Timespec}; use vectorcraft_engine::cmd::clipboard::names_a_picture; use vectorcraft_ui_egui::ClipboardRead; use wayland_client::backend::ObjectData; @@ -1827,6 +1829,11 @@ mod wayland { const MAX_SENDS: usize = 8; /// The most types of an offer that are kept (untrusted data). const MAX_TYPES: usize = 256; + /// How long a read or send waits for the other app to move a byte through the pipe before it + /// fails, VectorCraft's own limit; the wait starts again after each byte moved. + const IDLE_WAIT: Duration = LOAD_WAIT; + /// The most bytes a read takes from the pipe at a time. + const CHUNK: usize = 64 << 10; /// What a read gives: the bytes, or why there are none. type Got = Result, String>; @@ -1898,37 +1905,117 @@ mod wayland { Some(first) } - /// `from` read to its end, `cap` bytes at most: more is an error, as is a failed read. - fn read_capped(from: impl Read, cap: usize) -> Got { - let mut data = vec![]; - from.take(cap as u64 + 1).read_to_end(&mut data).map_err(|e| format!("couldn't read the clipboard's data: {e}"))?; - if data.len() > cap { - return Err(format!("the clipboard's data is larger than {} MiB", cap >> 20)); + /// The time `idle` from now; None when that is past the clock's range (no limit). + fn idle_until(idle: Duration) -> Option { + Instant::now().checked_add(idle) + } + + /// Makes `fd` nonblocking, its other flags kept. + fn nonblocking(fd: BorrowedFd<'_>) -> std::io::Result<()> { + let flags = rustix::fs::fcntl_getfl(fd)?; + rustix::fs::fcntl_setfl(fd, flags | rustix::fs::OFlags::NONBLOCK)?; + Ok(()) + } + + /// Waits until `fd` is ready for `events`, closed or failed (true), or `until` passes (false); + /// None waits without a limit. A wait a signal cuts short goes on until `until`. + fn ready(fd: BorrowedFd<'_>, events: PollFlags, until: Option) -> std::io::Result { + loop { + let left = until.map(|t| t.saturating_duration_since(Instant::now())); + let timeout = left.and_then(|d| Timespec::try_from(d).ok()); + let mut fds = [PollFd::new(&fd, events)]; + match rustix::event::poll(&mut fds, timeout.as_ref()) { + Ok(n) => return Ok(n > 0), + Err(rustix::io::Errno::INTR) => {} + Err(e) => return Err(e.into()), + } } - Ok(data) + } + + /// `from` read to its end, `cap` bytes at most: more is an error, as is a failed read and a + /// pipe that is left open with no byte coming for `idle`. A read that fails gives none of the + /// bytes it took, and closes `from`. + fn read_capped(mut from: impl Read + AsFd, cap: usize, idle: Duration) -> Got { + let failed = |e: std::io::Error| format!("couldn't read the clipboard's data: {e}"); + nonblocking(from.as_fd()).map_err(failed)?; + let (mut data, mut chunk) = (vec![], vec![0u8; CHUNK]); + let mut until = idle_until(idle); + loop { + match from.read(&mut chunk) { + Ok(0) => return Ok(data), + Ok(n) => { + let got = chunk.get(..n).ok_or_else(|| failed(ErrorKind::InvalidData.into()))?; + if data.len() + got.len() > cap { + return Err(format!("the clipboard's data is larger than {} MiB", cap >> 20)); + } + data.extend_from_slice(got); + until = idle_until(idle); + } + Err(e) if e.kind() == ErrorKind::Interrupted => {} + Err(e) if e.kind() == ErrorKind::WouldBlock => { + if !ready(from.as_fd(), PollFlags::IN, until).map_err(failed)? { + return Err("the app that copied stopped sending the clipboard's data".to_string()); + } + } + Err(e) => return Err(failed(e)), + } + } + } + + /// `data` written to `to`: an error when the app that pastes closes its end (the process + /// ignores `SIGPIPE`) or takes no byte for `idle`. + fn send(mut to: impl Write + AsFd, data: &[u8], idle: Duration) -> std::io::Result<()> { + nonblocking(to.as_fd())?; + let (mut sent, mut until) = (0, idle_until(idle)); + while let Some(rest) = data.get(sent..).filter(|rest| !rest.is_empty()) { + match to.write(rest) { + Ok(0) => return Err(ErrorKind::WriteZero.into()), + Ok(n) => { + sent += n; + until = idle_until(idle); + } + Err(e) if e.kind() == ErrorKind::Interrupted => {} + Err(e) if e.kind() == ErrorKind::WouldBlock => { + if !ready(to.as_fd(), PollFlags::OUT, until)? { + return Err(ErrorKind::TimedOut.into()); + } + } + Err(e) => return Err(e), + } + } + Ok(()) } /// The threads that send a Copy's data to the apps that paste it, counted. - #[derive(Clone, Default)] - struct Sends(Arc); + #[derive(Clone)] + struct Sends { + running: Arc, + /// How long a send waits for the app that pastes to take a byte. + idle: Duration, + } + + impl Default for Sends { + fn default() -> Self { + Self { running: Arc::default(), idle: IDLE_WAIT } + } + } impl Sends { - /// `data` written to `to` on a thread of its own, which then closes `to`; an app that - /// closes its end early ends the write (the process ignores `SIGPIPE`). While - /// [`MAX_SENDS`] are being written `to` is closed at once with nothing written → false. - fn start(&self, mut to: impl Write + Send + 'static, data: Arc<[u8]>) -> bool { - if self.0.fetch_add(1, Ordering::SeqCst) >= MAX_SENDS { - self.0.fetch_sub(1, Ordering::SeqCst); + /// `data` written to `to` on a thread of its own ([`send`]), which then closes `to`; an app + /// that closes its end early or takes no byte for the send's idle wait ends the write. + /// While [`MAX_SENDS`] are being written `to` is closed at once with nothing written → false. + fn start(&self, to: impl Write + AsFd + Send + 'static, data: Arc<[u8]>) -> bool { + if self.running.fetch_add(1, Ordering::SeqCst) >= MAX_SENDS { + self.running.fetch_sub(1, Ordering::SeqCst); return false; } - let count = self.0.clone(); - let send = move || { - let _ = to.write_all(&data); - drop(to); + let (count, idle) = (self.running.clone(), self.idle); + let work = move || { + let _ = send(to, &data, idle); count.fetch_sub(1, Ordering::SeqCst); }; - if std::thread::Builder::new().name("wayland-clipboard-send".into()).spawn(send).is_err() { - self.0.fetch_sub(1, Ordering::SeqCst); + if std::thread::Builder::new().name("wayland-clipboard-send".into()).spawn(work).is_err() { + self.running.fetch_sub(1, Ordering::SeqCst); return false; } true @@ -1937,7 +2024,7 @@ mod wayland { /// How many are being written. #[cfg(test)] fn running(&self) -> usize { - self.0.load(Ordering::SeqCst) + self.running.load(Ordering::SeqCst) } } @@ -2298,7 +2385,7 @@ mod wayland { impl Receiver { /// The clipboard's data of type `wire`, read until the app that copied closes the pipe, - /// [`MAX_BYTES`] at most. + /// [`MAX_BYTES`] at most; an error when that app sends no byte for [`IDLE_WAIT`]. fn receive(&self, wire: String) -> Got { let offer = lock(&self.objects).selection.clone().ok_or("the clipboard is empty")?; let (from, to) = std::io::pipe().map_err(|e| format!("couldn't open a pipe for the clipboard's data: {e}"))?; @@ -2307,7 +2394,7 @@ mod wayland { // holds the only one, and the read ends when that app closes it. drop(to); self.conn.flush().map_err(|e| format!("couldn't reach the compositor: {e}"))?; - read_capped(from, MAX_BYTES) + read_capped(from, MAX_BYTES, IDLE_WAIT) } } @@ -2557,31 +2644,86 @@ mod wayland { assert_eq!(keyboard_serial(&modifiers(9), None), None); } + /// A wait no test reaches: the other end of each pipe below either moves data or closes. + const LONG: Duration = Duration::from_secs(600); + + /// A pipe holding `data`, its write end closed when `close`. + fn pipe_with(data: &[u8], close: bool) -> (std::io::PipeReader, Option) { + let (from, mut to) = std::io::pipe().unwrap(); + to.write_all(data).unwrap(); + (from, (!close).then_some(to)) + } + #[test] fn a_read_stops_at_its_limit() { assert_eq!(MAX_BYTES, 64 * 1024 * 1024); - assert_eq!(read_capped(&b"12345678"[..], 8), Ok(b"12345678".to_vec())); - assert_eq!(read_capped(&b""[..], 8), Ok(vec![])); - assert_eq!(read_capped(&[0u8; 9][..], 8), Err("the clipboard's data is larger than 0 MiB".to_string())); - assert_eq!(read_capped(&vec![0u8; (2 << 20) + 1][..], 2 << 20), Err("the clipboard's data is larger than 2 MiB".to_string())); - /// A pipe that breaks. - struct Broken; - impl Read for Broken { - fn read(&mut self, _: &mut [u8]) -> std::io::Result { - Err(std::io::ErrorKind::BrokenPipe.into()) - } + assert_eq!(IDLE_WAIT, LOAD_WAIT); + assert_eq!(read_capped(pipe_with(b"12345678", true).0, 8, Duration::ZERO), Ok(b"12345678".to_vec())); + assert_eq!(read_capped(pipe_with(b"", true).0, 8, Duration::ZERO), Ok(vec![])); + assert_eq!(read_capped(pipe_with(&[0u8; 9], true).0, 8, Duration::ZERO), Err("the clipboard's data is larger than 0 MiB".to_string())); + // More than a pipe holds, from an app that is still sending. + let (from, to) = std::io::pipe().unwrap(); + assert!(Sends::default().start(to, vec![0u8; (2 << 20) + 1].into())); + assert_eq!(read_capped(from, 2 << 20, LONG), Err("the clipboard's data is larger than 2 MiB".to_string())); + // A pipe that can't be read: the write end. + let (_, to) = std::io::pipe().unwrap(); + let unreadable = std::fs::File::from(std::os::fd::OwnedFd::from(to)); + assert!(read_capped(unreadable, 8, LONG).is_err_and(|e| e.starts_with("couldn't read the clipboard's data"))); + } + + #[test] + fn a_read_from_an_app_that_stops_sending_fails_and_closes_the_pipe() { + let stopped = Err("the app that copied stopped sending the clipboard's data".to_string()); + // Some bytes and then nothing, the pipe left open: none of them are pasted. + let (from, to) = pipe_with(b"part", false); + assert_eq!(read_capped(from, MAX_BYTES, Duration::ZERO), stopped); + // The read end is closed, so that app's next write fails. + let mut to = to.unwrap(); + assert_eq!(to.write(b"x").map_err(|e| e.kind()), Err(ErrorKind::BrokenPipe)); + // Nothing at all, and data that fills the limit exactly with more never coming. + let (from, _held) = pipe_with(b"", false); + assert_eq!(read_capped(from, MAX_BYTES, Duration::ZERO), stopped); + let (from, _held) = pipe_with(b"12345678", false); + assert_eq!(read_capped(from, 8, Duration::ZERO), stopped); + // The next read gets its data. + assert_eq!(read_capped(pipe_with(b"words", true).0, MAX_BYTES, Duration::ZERO), Ok(b"words".to_vec())); + } + + #[test] + fn a_send_to_an_app_that_stops_reading_fails_and_frees_its_thread() { + let data: Arc<[u8]> = vec![3u8; 300_000].into(); + // More than a pipe holds, to an app that never reads. + let (_held, to) = std::io::pipe().unwrap(); + assert_eq!(send(to, &data, Duration::ZERO).map_err(|e| e.kind()), Err(ErrorKind::TimedOut)); + // An app that closes its end before reading. + let (from, to) = std::io::pipe().unwrap(); + drop(from); + assert_eq!(send(to, &data, LONG).map_err(|e| e.kind()), Err(ErrorKind::BrokenPipe)); + // A send thread to an app that does not read until the send gives up, its end still open. + let sends = Sends { idle: Duration::ZERO, ..Sends::default() }; + let (from, to) = std::io::pipe().unwrap(); + assert!(sends.start(to, data.clone())); + while sends.running() > 0 { + std::thread::yield_now(); } - assert!(read_capped(Broken, 8).is_err_and(|e| e.starts_with("couldn't read the clipboard's data"))); + // What the pipe held when the send gave up, then the end of the data: the send closed it. + let got = read_capped(from, MAX_BYTES, Duration::ZERO).map(|d| d.len()); + assert!(got.as_ref().is_ok_and(|&n| n > 0 && n < data.len()), "{got:?}"); + // The thread is free for the next paste. + let (from, to) = std::io::pipe().unwrap(); + assert!(sends.start(to, b"words"[..].into())); + assert_eq!(read_capped(from, MAX_BYTES, LONG), Ok(b"words".to_vec())); } #[test] fn a_copys_data_is_sent_through_the_pipe_and_closed() { let sends = Sends::default(); + assert_eq!(sends.idle, IDLE_WAIT); let (from, to) = std::io::pipe().unwrap(); let data: Arc<[u8]> = vec![7u8; 300_000].into(); assert!(sends.start(to, data.clone())); // More than a pipe holds: the write ends once all of it is read, then the pipe closes. - assert_eq!(read_capped(from, MAX_BYTES).as_deref(), Ok(&data[..])); + assert_eq!(read_capped(from, MAX_BYTES, LONG).as_deref(), Ok(&data[..])); // An app that closes its end before reading ends the send. let (from, to) = std::io::pipe().unwrap(); drop(from); @@ -2593,9 +2735,9 @@ mod wayland { #[test] fn pastes_that_read_nothing_hold_a_few_sends_at_most() { - let sends = Sends::default(); + let sends = Sends { idle: LONG, ..Sends::default() }; let data: Arc<[u8]> = vec![1u8; 300_000].into(); - // Apps that ask for the data and never read it. + // Apps that ask for the data and never read it, within the sends' idle wait. let stalled: Vec<_> = (0..MAX_SENDS) .map(|_| { let (from, to) = std::io::pipe().unwrap(); @@ -2606,7 +2748,7 @@ mod wayland { // The next paste gets its pipe closed at once, with nothing written. let (from, to) = std::io::pipe().unwrap(); assert!(!sends.start(to, data.clone())); - assert_eq!(read_capped(from, MAX_BYTES), Ok(vec![])); + assert_eq!(read_capped(from, MAX_BYTES, Duration::ZERO), Ok(vec![])); // Once those apps close their pipes, the sends end and others are sent again. drop(stalled); while sends.running() > 0 { @@ -2614,7 +2756,7 @@ mod wayland { } let (from, to) = std::io::pipe().unwrap(); assert!(sends.start(to, data.clone())); - assert_eq!(read_capped(from, MAX_BYTES).map(|d| d.len()), Ok(data.len())); + assert_eq!(read_capped(from, MAX_BYTES, LONG).map(|d| d.len()), Ok(data.len())); } /// What a clipboard holds, by type. diff --git a/crates/doc/src/graph.rs b/crates/doc/src/graph.rs index 43074a4e80..d561663524 100644 --- a/crates/doc/src/graph.rs +++ b/crates/doc/src/graph.rs @@ -363,6 +363,14 @@ pub struct SeriesPaint { /// Object blend mode of each path directly in the series group, as [`Self::opacity`]. Absent: Normal. #[serde(skip_serializing_if = "Option::is_none")] pub blend: Option, + /// A gradient in [`Self::fill`] is placed relative to the unit box (0, 0)–(1, 1), and each mark takes it on its own + /// geometric bounds. False: the gradient is in document coordinates, and every mark takes it unchanged. + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub fill_unit_box: bool, + /// A gradient in [`Self::stroke`] is placed relative to the unit box, as [`Self::fill_unit_box`], and each mark takes + /// it on its stroke's box (the geometric bounds grown by half the weight). + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub stroke_unit_box: bool, } /// The object rows one kind of graph label keeps ([`LabelStyles::values_rows`]): the paint, weight, opacity and blend mode diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 90a0d85ec8..2cffad6581 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -29,7 +29,7 @@ use crate::group::{has_own_paint, paints}; use crate::reshape::{as_art, geometry_effects, glyph_outlines, outline_nested_text, reshape_leaves}; use crate::{ GeomContext, adjust_in_document, apply_geometry_carrying, crop_marks_art, evaluate_container, has_adjustment, has_crop_marks, has_geometry, - has_item_geometry, has_pathfinder, is_pathfinder, merged_params, needs_outline, outline_text, reshape, + has_instance_row_raster, has_item_geometry, has_pathfinder, is_pathfinder, merged_params, needs_outline, outline_text, reshape, }; fn item_effects(item: &AppearanceItem) -> &[vectorcraft_doc::Effect] { @@ -58,6 +58,7 @@ pub fn needs_bake(n: &Node) -> bool { || has_geometry(&n.appearance.effects) || n.projection().is_some() || n.appearance.items.iter().any(|i| has_geometry(item_effects(i))) + || has_instance_row_raster(n) || n.children().is_some_and(|ch| ch.iter().any(|c| needs_bake(c))) } @@ -142,9 +143,11 @@ fn bake_node(d: &mut Document, n: &Node) -> Option { } // Type, images, symbol instances and live objects: reshaped through their outlines, and so are // type whose own fills or strokes have geometry effects and images whose own fills or strokes - // paint a shape ([`has_item_geometry`]). Type that Transform effects only move and copy stays - // type, moved to each place. - if needs_outline(n) && (has_geometry(&n.appearance.effects) || n.projection().is_some() || has_item_geometry(n)) { + // paint a shape ([`has_item_geometry`]), and symbol instances whose active fill or stroke has a + // raster effect, which the fills and strokes of their art then carry + // ([`has_instance_row_raster`]). Type that Transform effects only move and copy stays type, + // moved to each place. + if needs_outline(n) && (has_geometry(&n.appearance.effects) || n.projection().is_some() || has_item_geometry(n) || has_instance_row_raster(n)) { if let Some(m) = moved_type_group(n) { return Some(bake_pieces(d, m)); } diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 6316a7a273..c18a31d842 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -80,8 +80,9 @@ pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks}; pub use pixel::{PIXEL_EFFECTS, PixelFx, PixelSpace}; pub use raster::{RASTERIZE, RasterFx, Rasterize, color_param, follow_items, item_effects_in, outset, raster_effects, without_geometry}; pub use reshape::{ - SYMBOL_ART_MAX_NODES, expand_outlined, follows_image_pixels, gives_image_shape, has_gradient_copies, has_item_geometry, has_painted_effects, - has_transform_art, has_transform_copies, has_transform_options, image_paint, needs_outline, outline_art, outline_text, reshape, + SYMBOL_ART_MAX_NODES, expand_outlined, follows_image_pixels, gives_image_shape, has_gradient_copies, has_instance_row_raster, has_item_geometry, + has_painted_effects, has_transform_art, has_transform_copies, has_transform_options, image_paint, needs_outline, outline_art, outline_text, + reshape, }; pub use revolve::{ CLASSIC_REVOLVE, REVOLVE, has_revolve, is_revolve, revolve_art, revolve_art_in, revolve_options, validate_revolve, validate_revolve_in, diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index 8c829d9e0b..83ff68431f 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -162,15 +162,26 @@ fn active_stroke(n: &Node) -> Option<&vectorcraft_doc::StrokeLayer> { n.appearance.stroke().filter(|s| s.visible && s.opacity > 0.0) } -/// The visible geometry effects in `fx`, in order. -fn geometry_of(fx: &[Effect]) -> Vec { - fx.iter().filter(|e| e.visible && is_geometry(&e.id)).cloned().collect() +/// Is `n` a symbol instance whose active fill or stroke has a visible raster effect (a shadow, glow, +/// blur, feather or other painted effect, [`crate::is_raster`])? Its art takes that effect on each +/// fill or stroke in it that paints ([`add_instance_row_effects`]), and draws it there. +pub fn has_instance_row_raster(n: &Node) -> bool { + let raster = |fx: &[Effect]| fx.iter().any(|e| e.visible && crate::is_raster(&e.id)); + matches!(n.kind, NodeKind::SymbolInstance { .. }) + && (active_fill(n).is_some_and(|f| raster(&f.effects)) || active_stroke(n).is_some_and(|s| raster(&s.effects))) } -/// `art`, the art symbol instance `n` places, with the visible geometry effects of `n`'s active -/// fill added after the effects of every fill in it that paints, and those of its active stroke -/// after the effects of every stroke in it that paints ([`active_fill`], [`active_stroke`]). Each -/// fill or stroke keeps its paint, and the others keep their shapes. Live objects in it that are +/// The visible geometry and raster effects in `fx`, in order: those a symbol instance's active fill +/// or stroke passes on to its art. +fn row_effects_of(fx: &[Effect]) -> Vec { + fx.iter().filter(|e| e.visible && (is_geometry(&e.id) || crate::is_raster(&e.id))).cloned().collect() +} + +/// `art`, the art symbol instance `n` places, with the visible geometry and raster effects of +/// `n`'s active fill added after the effects of every fill in it that paints, and those of its +/// active stroke after the effects of every stroke in it that paints ([`active_fill`], +/// [`active_stroke`]): a Drop Shadow on the instance's fill shadows each filled shape in the art. +/// Each fill or stroke keeps its paint, and the others keep their shapes. Live objects in it that are /// reached are evaluated first (with the symbols of `doc`), so the effects reshape what they draw. /// Clip groups' clipping paths, guides and the art of opacity masks keep their shapes. A symbol /// instance in the art takes the effects on its own active fill or stroke, or on a new fill or @@ -180,8 +191,8 @@ fn geometry_of(fx: &[Effect]) -> Vec { /// instance in the art takes them. fn add_instance_row_effects(doc: Option<&Document>, n: &Node, art: &mut Node) { let NodeKind::SymbolInstance { symbol, .. } = &n.kind else { return }; - let fill = active_fill(n).map_or(vec![], |f| geometry_of(&f.effects)); - let stroke = active_stroke(n).map_or(vec![], |s| geometry_of(&s.effects)); + let fill = active_fill(n).map_or(vec![], |f| row_effects_of(&f.effects)); + let stroke = active_stroke(n).map_or(vec![], |s| row_effects_of(&s.effects)); if !fill.is_empty() || !stroke.is_empty() { add_row_effects(doc, symbol, art, &fill, &stroke); } @@ -526,8 +537,8 @@ fn with_id(art: &Node, id: NodeId) -> Node { /// a symbol instance as `symbol_art` (the symbol's art, stained as the caller draws it) placed by /// the instance's transform (through the symbol's 9-slice guides in `doc`, when it has them), as /// the art its members draw when the instance has geometry effects of its own -/// ([`crate::group::members_as_drawn`]), its fills and strokes carrying the geometry effects of the -/// instance's active fill and stroke ([`add_instance_row_effects`]). +/// ([`crate::group::members_as_drawn`]), its fills and strokes carrying the geometry and raster +/// effects of the instance's active fill and stroke ([`add_instance_row_effects`]). /// `None` for other kinds (images, paths, containers), and for an instance whose art placed /// through the guides could hold more than [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects. A Repeat counts the Repeats in the art of /// `doc`'s symbols that its instances show toward its instance budget. @@ -709,12 +720,14 @@ pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping /// reshaped with it, and keep their effects from their first Convert to Shape or Outline Object /// on. The result is a group with `n`'s id, name, visibility, transparency, opacity mask /// and remaining (raster) effects; `None` when `n` has no visible geometry effect, no perspective -/// and is not type or an image with a fill or stroke of its own that [`has_item_geometry`] counts, -/// or has no art (a symbol instance without `symbol_art`). A path with [`has_transform_art`] is -/// always reshaped. The fills and strokes in the art whose own Transform effects reach their -/// appearance become objects of their own ([`transform_items`]). +/// and is not type or an image with a fill or stroke of its own that [`has_item_geometry`] counts +/// or a symbol instance whose active fill or stroke has a raster effect +/// ([`has_instance_row_raster`]), or has no art (a symbol instance without `symbol_art`). A path +/// with [`has_transform_art`] is always reshaped. The fills and strokes in the art whose own +/// Transform effects reach their appearance become objects of their own ([`transform_items`]). pub fn reshape(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { - if !has_transform_art(n) && ((geometry_effects(n).is_empty() && n.projection().is_none() && !has_item_geometry(n)) || !needs_outline(n)) { + let own = !geometry_effects(n).is_empty() || n.projection().is_some() || has_item_geometry(n) || has_instance_row_raster(n); + if !has_transform_art(n) && (!own || !needs_outline(n)) { return None; } as_art(doc, n, symbol_art) diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index 7500af0617..ac9de02cad 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -7,7 +7,7 @@ use vectorcraft_doc::color::{BlendMode, Paint}; use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, Node, NodeId, NodeKind, OpacityMask}; use vectorcraft_geom::{BezPath, FillRule, PathData, Point, Rect, Vec2}; use vectorcraft_pathops::BoolOp; -use vectorcraft_three_d::{Bevel, BevelShape, DepthPlane, Extrude, Facet, Inflate, MAX_OBJECT_FACES, Mesh, PlanarFace, View}; +use vectorcraft_three_d::{Bevel, BevelShape, DepthPlane, Extrude, Facet, Inflate, MAX_OBJECT_FACES, Mesh, MeshedSolid, PlanarFace, View}; #[cfg(test)] use vectorcraft_doc::color::Color; @@ -656,7 +656,7 @@ fn solid_parts(shapes: &[Profile], fallback: Rect) -> (Vec>, Rect) { #[cfg(test)] thread_local! { - /// How many solids [`built_solids`] has built on this thread, for the tests. + /// How many solids [`built_solids`] and [`meshes_for`] have built on this thread, for the tests. static SOLIDS_BUILT: std::cell::Cell = const { std::cell::Cell::new(0) }; } @@ -2039,31 +2039,35 @@ fn meshes_for(fx: &Effect, shapes: &[Profile], total: &mut usize, limit: usize) return Ok(out); } let (parts, bounds) = solid_parts(shapes, b); - // The solids the art draws, past the limits of the object's solids together as it is - // ([`built_solids`]), each built and its faces counted as the loop reaches it, then dropped. - let drawn = built_solids(&fx.id, &p, &parts, bounds, MAX_OBJECT_FACES).map(|r| r.map(|f| f.is_some())); - // Inflate lays out every puff at once (they share one eye); each Inflate and Extrude & Bevel - // solid and its mesh are built as the loop reaches them. + // Inflate lays out every puff at once (they share one eye). Each Inflate and Extrude & Bevel + // solid is built once, as the loop reaches it: its facets decide whether the art draws it, as + // [`built_solids`] does, and its faces before projection make its mesh. let mut inflated = (fx.id == INFLATE).then(|| { let shapes: Vec<(&PathData, FillRule)> = parts.iter().map(|(_, _, area, rule, _, _)| (area, *rule)).collect(); - vectorcraft_three_d::inflate_meshes(&shapes, bounds, inflate_options(&p), view) + vectorcraft_three_d::inflate_meshed(&shapes, bounds, inflate_options(&p), view) }); let extrude = if fx.id == CLASSIC_EXTRUDE { classic_extrude_options(&p) } else { extrude_options(&p) }; - for ((item, member, area, rule, _, _), drawn) in parts.iter().zip(drawn) { + let mut kept = 0usize; + for (item, member, area, rule, _, _) in &parts { let item: &AppearanceItem = item; - if !drawn? { - // A stroke the art leaves out is left out of the meshes too; Inflate's next puff - // still belongs to the next part. - if let Some(built) = inflated.as_mut() { - built.next(); - } - continue; - } - let mesh = match inflated.as_mut() { - Some(built) => built.next().unwrap_or_else(|| Ok(Mesh::default())), - None => vectorcraft_three_d::extrude_mesh(area, *rule, bounds, extrude, view), + #[cfg(test)] + SOLIDS_BUILT.with(|n| n.set(n.get() + 1)); + let built = match inflated.as_mut() { + Some(each) => each.next().unwrap_or_else(|| Ok(MeshedSolid::default())), + None => vectorcraft_three_d::extrude_meshed(area, *rule, bounds, extrude, view), }; - match mesh { + // A stroke the art leaves out is left out of the meshes too. The faces of the solids kept + // count together against the object's limit. + let solid = match built { + Ok(solid) => solid, + Err(_) if matches!(item, AppearanceItem::Stroke(_)) => continue, + Err(e) => return Err(e), + }; + kept = kept.saturating_add(solid.facets.len()); + if kept > MAX_OBJECT_FACES { + return Err(vectorcraft_three_d::TOO_COMPLEX_SOLID.to_string()); + } + match solid.mesh() { Ok(mesh) => { let (paint, opacity, _) = paint_of(item, *member); keep(&mut out, mesh, mesh_color(&paint, opacity))?; diff --git a/crates/effects/src/solids/tests_mesh.rs b/crates/effects/src/solids/tests_mesh.rs index aeefafbc77..a9de7cd7f9 100644 --- a/crates/effects/src/solids/tests_mesh.rs +++ b/crates/effects/src/solids/tests_mesh.rs @@ -208,3 +208,151 @@ fn a_gradient_takes_the_average_of_its_stops() { let pattern = Paint::Pattern { pattern: "x".into(), xf: vectorcraft_geom::Affine::IDENTITY }; assert!(close(mesh_color(&pattern, 0.5), [0.5, 0.5, 0.5, 0.5])); } + +/// The meshes of Extrude & Bevel or Inflate `fx` on `shapes`, each built apart from the facets that +/// decide which solids are drawn ([`built_solids`], then [`vectorcraft_three_d::extrude_mesh`] or +/// [`vectorcraft_three_d::inflate_meshes`]), for comparing with [`meshes_for`]. +fn meshes_built_apart(fx: &Effect, shapes: &[Profile], total: &mut usize, limit: usize) -> Result, String> { + let p = crate::merged_params(&fx.id, &fx.params); + let view = view_options(&p); + let (parts, bounds) = solid_parts(shapes, union_bounds(shapes).unwrap()); + let drawn = built_solids(&fx.id, &p, &parts, bounds, MAX_OBJECT_FACES).map(|r| r.map(|f| f.is_some())); + let mut inflated = (fx.id == INFLATE).then(|| { + let shapes: Vec<(&PathData, FillRule)> = parts.iter().map(|(_, _, area, rule, _, _)| (area, *rule)).collect(); + vectorcraft_three_d::inflate_meshes(&shapes, bounds, inflate_options(&p), view) + }); + let extrude = if fx.id == CLASSIC_EXTRUDE { classic_extrude_options(&p) } else { extrude_options(&p) }; + let mut out = vec![]; + for ((item, opacity, area, rule, _, _), drawn) in parts.iter().zip(drawn) { + let item: &AppearanceItem = item; + if !drawn? { + if let Some(built) = inflated.as_mut() { + built.next(); + } + continue; + } + let mesh = match inflated.as_mut() { + Some(built) => built.next().unwrap_or_else(|| Ok(Mesh::default())), + None => vectorcraft_three_d::extrude_mesh(area, *rule, bounds, extrude, view), + }; + match mesh { + Ok(mesh) => { + let (paint, opacity, _) = paint_of(item, *opacity); + keep_mesh(&mut out, total, SolidMesh { mesh, color: mesh_color(&paint, opacity) }, limit)?; + } + Err(_) if matches!(item, AppearanceItem::Stroke(_)) => {} + Err(e) => return Err(e), + } + } + Ok(out) +} + +/// The bits of every number in `meshes`, for comparing two exports exactly. +type MeshBits = Vec<([u32; 4], Vec<[u64; 3]>, Vec<[u64; 3]>, Vec<[u32; 3]>)>; + +fn mesh_bits(meshes: &[SolidMesh]) -> MeshBits { + meshes + .iter() + .map(|m| { + let (positions, normals) = (&m.mesh.positions, &m.mesh.normals); + ( + m.color.map(f32::to_bits), + positions.iter().map(|p| p.map(f64::to_bits)).collect(), + normals.iter().map(|n| n.map(f64::to_bits)).collect(), + m.mesh.triangles.clone(), + ) + }) + .collect() +} + +/// A five-pointed star about `(x, 150)` with curved edges, filled red and stroked black. +fn star_node(id: u64, x: f64) -> Node { + let mut b = BezPath::new(); + for k in 0..10 { + let (s, c) = (k as f64 * std::f64::consts::PI / 5.0).sin_cos(); + let r = if k % 2 == 0 { 60.0 } else { 25.0 }; + let p = (x + r * c, 150.0 + r * s); + if k == 0 { b.move_to(p) } else { b.curve_to((p.0 + 4.0, p.1), (p.0, p.1 + 4.0), p) } + } + b.close_path(); + Node::path(NodeId(id), PathData::from_bezpath(&b), Appearance::basic(Paint::solid(RED), Paint::solid(Color::BLACK), 6.0)) +} + +/// The shapes `meshes_for` builds from: one node's, or each of several. +fn shapes_of(nodes: &[&Node]) -> Vec { + nodes + .iter() + .map(|n| Profile { path: profile(n).unwrap().0, rule: FillRule::NonZero, items: solid_paints(n), opacity: 1.0, scopes: vec![] }) + .collect() +} + +/// Building each Extrude & Bevel and Inflate solid once exports the same meshes, to the bit, as +/// building its facets and its mesh apart, within and past the triangle budget. +#[test] +fn one_build_exports_the_meshes_built_apart() { + let mut second_stroke = square_node(3, 400.0); + if let Some(stroke) = second_stroke.appearance.items.get(1).cloned() { + second_stroke.appearance.items.push(stroke); + } + let (star, square, other) = (star_node(1, 100.0), square_node(2, 250.0), star_node(4, 600.0)); + let cases = [ + (EXTRUDE, json!({})), + (EXTRUDE, json!({ "bevel": "rounded", "bevelWidth": 6.0, "bevelHeight": 5.0, "twist": 60.0, "taper": 70.0 })), + (EXTRUDE, json!({ "bevel": "cove", "bevelInside": true, "bevelBothSides": true, "cap": false, "perspectiveAngle": 50.0 })), + (EXTRUDE, json!({ "depth": 0.0 })), + (CLASSIC_EXTRUDE, json!({ "bevel": "classic", "bevelHeight": 8.0 })), + (INFLATE, json!({})), + (INFLATE, json!({ "depth": 30.0, "bothSides": true, "volume": 80.0 })), + (INFLATE, json!({ "depth": 40.0, "twist": 360.0, "taper": 70.0, "cap": false })), + (INFLATE, json!({ "depth": 0.1, "twist": 0.1, "bothSides": true })), + ]; + let sets: [Vec<&Node>; 3] = [vec![&star], vec![&second_stroke], vec![&star, &square, &other]]; + for (id, params) in cases { + let effect = fx(id, params.clone()); + for nodes in &sets { + let shapes = shapes_of(nodes); + let (mut once, mut apart) = (0, 0); + let built = meshes_for(&effect, &shapes, &mut once, MAX_MESH_TRIANGLES).unwrap_or_else(|e| panic!("{id} {params}: {e}")); + let reference = meshes_built_apart(&effect, &shapes, &mut apart, MAX_MESH_TRIANGLES).unwrap(); + assert!(!reference.is_empty()); + assert_eq!(mesh_bits(&built), mesh_bits(&reference), "{id} {params}"); + assert_eq!(once, apart); + // Past the budget, at the same mesh, with the same count. + let limit = once - 1; + let (mut once, mut apart) = (0, 0); + let built = meshes_for(&effect, &shapes, &mut once, limit); + assert!(built.is_err(), "{id} {params}"); + assert_eq!(built.err(), meshes_built_apart(&effect, &shapes, &mut apart, limit).err(), "{id} {params}"); + assert_eq!(once, apart, "{id} {params}"); + } + } +} + +/// A stroke whose solid is past the limits is left out of the export and the fill's mesh exports, +/// as when the facets and the mesh are built apart. +#[test] +fn a_stroke_past_the_limits_is_left_out_of_the_export() { + // 40,000 corners: within the 65,536 points of the fill's solid, but not the stroke's outline. + let points: Vec = (0..40_000) + .map(|k| { + let (s, c) = (k as f64 * std::f64::consts::TAU / 40_000.0).sin_cos(); + let r = if k % 2 == 0 { 10_000.0 } else { 9_999.0 }; + Point::new(20_000.0 + r * c, 20_000.0 + r * s) + }) + .collect(); + let path = PathData::single(vectorcraft_geom::SubPath::polyline(&points, true)); + let n = Node::path(NodeId(1), path, Appearance::basic(Paint::solid(RED), Paint::solid(Color::BLACK), 2.0)); + for id in [EXTRUDE, INFLATE] { + // Without caps: a cap of 40,000 corners takes more work to cut into triangles than an + // export allows. + let effect = fx(id, json!({ "cap": false })); + let shapes = shapes_of(&[&n]); + // The fill's mesh alone has more triangles than an export takes. + let (mut once, mut apart) = (0, 0); + let built = meshes_for(&effect, &shapes, &mut once, usize::MAX).unwrap_or_else(|e| panic!("{id}: {e}")); + assert_eq!(built.len(), 1, "{id}: the fill's mesh alone"); + assert!(close(built[0].color, [0.8, 0.2, 0.1, 1.0]), "{id}"); + let reference = meshes_built_apart(&effect, &shapes, &mut apart, usize::MAX).unwrap(); + assert_eq!(mesh_bits(&built), mesh_bits(&reference), "{id}"); + } +} diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index d4fb2c39cd..19695fd4ff 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -559,6 +559,15 @@ pub(crate) fn given_object_ids(p: &Value, command: &str) -> Result usize { usize::try_from(v).unwrap_or(usize::MAX) } +/// The whole number of 0 or more that `p` gives for `key`, read with [`index`], or `None` when `p` +/// leaves it out or gives null. Any other value, such as `"3"`, `2.5`, `-1` or `true`, is an error +/// under `cmd`: "`{key}` must be a whole number". +pub(crate) fn index_opt(p: &Value, key: &str, cmd: &str) -> Result> { + match p.get(key) { + None | Some(Value::Null) => Ok(None), + Some(v) => v.as_u64().map(|n| Some(index(n))).ok_or_else(|| bad(cmd, format!("`{key}` must be a whole number"))), + } +} /// Validate all explicit targets before a command changes the document or selection. pub(crate) fn checked_ids_param(s: &Session, p: &Value, key: &str, command: &str) -> Result> { checked_nodes_param(s, p, key, command, false) diff --git a/crates/engine/src/cmd/addons/shapes.rs b/crates/engine/src/cmd/addons/shapes.rs index 87549e3694..7fc935546e 100644 --- a/crates/engine/src/cmd/addons/shapes.rs +++ b/crates/engine/src/cmd/addons/shapes.rs @@ -9,6 +9,8 @@ #[cfg(test)] mod tests; +use std::ops::RangeInclusive; + use serde_json::{Value, json}; use vectorcraft_doc::{LiveShape, NodeId, NodeKind}; use vectorcraft_geom::{Affine, FillRule, Point, Rect, shapes}; @@ -16,9 +18,25 @@ use vectorcraft_tools::addons::box3d::CustomShape; use super::super::create::insert_look; use super::super::{ - CommandSpec, Result, Session, bool_or, check_numbers, check_switches, cmd, f64_or, f64_req, has_doc, has_selection, str_param, targets, + CommandSpec, Result, Session, bad, bool_or, check_numbers, check_switches, cmd, f64_or, f64_req, has_doc, has_selection, number_or, str_param, + targets, }; +/// The sides of a polygon and the points of a star: whole numbers in this range. +const COUNTS: RangeInclusive = 3.0..=1000.0; +/// `rounded` and `randomized` of a polygon or star. +const ROUNDING: RangeInclusive = -10.0..=10.0; +/// A star's inner radius as a fraction of its outer radius. +const SPOKE_RATIO: RangeInclusive = 0.01..=1.0; +/// A spiral's `turns`. +const TURNS: RangeInclusive = 0.01..=1024.0; +/// A spiral's `divergence`. +const DIVERGENCE: RangeInclusive = 0.0..=1000.0; +/// A spiral's `innerRadius`, as a fraction of its radius. +const INNER_RADIUS: RangeInclusive = 0.0..=0.999; +/// The `arcMode` names; `slice` and `open` are also read as `pie` and `arc`. +const ARC_MODES: [&str; 4] = ["whole", "pie", "chord", "arc"]; + pub fn specs() -> Vec { vec![ cmd!( @@ -26,7 +44,7 @@ pub fn specs() -> Vec { "Rectangle", [], None, - "{x, y, width, height, radius?: pt, radii?: [tl,tr,br,bl], startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top-left corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", + "{x, y, width, height: pt (0 or more), radius?: pt (0 or more), radii?: [tl,tr,br,bl] (each 0 or more), startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top-left corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", has_doc, rectangle ), @@ -35,7 +53,7 @@ pub fn specs() -> Vec { "Ellipse", [], None, - "{x, y, width, height, startAngle?, endAngle?: degrees (counter-clockwise from 3 o'clock, as object.setLiveShape's pieStart and pieEnd; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", + "{x, y, width, height: pt (0 or more), startAngle?, endAngle?: degrees (counter-clockwise from 3 o'clock, as object.setLiveShape's pieStart and pieEnd; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", has_doc, ellipse ), @@ -44,7 +62,7 @@ pub fn specs() -> Vec { "Polygon", [], None, - "{cx, cy, radius, sides=6, rounded=0, randomized=0, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a corner straight up)} → {id}", + "{cx, cy, radius: pt (0 or more), sides=6: a whole number 3 to 1000, rounded=0, randomized=0: -10 to 10, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a corner straight up)} → {id}", has_doc, polygon ), @@ -53,7 +71,7 @@ pub fn specs() -> Vec { "Star", [], None, - "{cx, cy, radius1, radius2?, points=5, rounded=0, randomized=0, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top point; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a point straight up)} → {id}", + "{cx, cy, radius1: pt (0 or more), radius2?: pt (0.01 to 1 times radius1; default half of it), points=5: a whole number 3 to 1000, rounded=0, randomized=0: -10 to 10, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top point; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a point straight up)} → {id}", has_doc, star ), @@ -62,7 +80,7 @@ pub fn specs() -> Vec { "Spiral", [], None, - "{cx, cy, radius, turns=3, divergence=1, innerRadius=0, clockwise=true} → {id}", + "{cx, cy, radius: pt (0 or more), turns=3: 0.01 to 1024, divergence=1: 0 to 1000, innerRadius=0: 0 to 0.999 of radius, clockwise=true} → {id}", has_doc, spiral ), @@ -71,7 +89,7 @@ pub fn specs() -> Vec { "3D Box", [], None, - "{x, y, width, height, angleX=30 (degrees about the level axis: positive tips the top away from you), angleY=30 (degrees about the upright axis: positive turns the right side away from you), angleZ=0 (degrees clockwise in the page), parallelX=false, parallelY=true, parallelZ=false} → {id}", + "{x, y, width, height: pt (0 or more), angleX=30 (degrees about the level axis: positive tips the top away from you), angleY=30 (degrees about the upright axis: positive turns the right side away from you), angleZ=0 (degrees clockwise in the page), parallelX=false, parallelY=true, parallelZ=false} → {id}", has_doc, box3d ), @@ -80,33 +98,96 @@ pub fn specs() -> Vec { "Configure Shape", [], None, - "{ids?, radius?, radii?: [tl,tr,br,bl], startAngle?, endAngle?: degrees (as addon.shapes.rectangle, ellipse, polygon and star take them: an ellipse's counter-clockwise from 3 o'clock, the others' clockwise along the outline), arcMode?: whole|pie|chord|arc, sides?, isStar?, spokeRatio?, rounded?, randomized?, turns?, divergence?, innerRadius?, angleX?, angleY?: degrees (as addon.shapes.box3d: positive tips the top, or turns the right side, away from you), angleZ?: degrees clockwise in the page, parallelX?, parallelY?, parallelZ?, reset?: bool} → {updated}", + "{ids?, radius?: pt (0 or more; fitted to half each rectangle's shorter side), radii?: [tl,tr,br,bl] (each 0 or more), startAngle?, endAngle?: degrees (as addon.shapes.rectangle, ellipse, polygon and star take them: an ellipse's counter-clockwise from 3 o'clock, the others' clockwise along the outline), arcMode?: whole|pie|chord|arc, sides?: a whole number 3 to 1000, isStar?, spokeRatio?: 0.01 to 1, rounded?, randomized?: -10 to 10, turns?: 0.01 to 1024, divergence?: 0 to 1000, innerRadius?: 0 to 0.999, angleX?, angleY?: degrees (as addon.shapes.box3d: positive tips the top, or turns the right side, away from you), angleZ?: degrees clockwise in the page, parallelX?, parallelY?, parallelZ?, reset?: bool} an omitted or null value keeps each shape's setting → {updated}", has_selection, configure ), ] } -fn parse_arc_params(p: &Value) -> ((f64, f64), shapes::EllipseMode) { +/// The number `p` gives for `key`, or `default` when `p` leaves it out or gives null. A number +/// outside `range` is an error that names `key` and the range. +fn ranged(p: &Value, key: &str, default: f64, range: &RangeInclusive, cmd: &str) -> Result { + let x = number_or(p, key, default, cmd)?; + if !range.contains(&x) { + let (min, max) = (range.start(), range.end()); + return Err(bad(cmd, format!("`{key}` must be between {min} and {max} (got {x})"))); + } + Ok(x) +} + +/// The number of sides or points `p` gives for `key`, or `default` when `p` leaves it out or gives +/// null. A fraction or a number outside [`COUNTS`] is an error. +fn count(p: &Value, key: &str, default: u32, cmd: &str) -> Result { + let x = number_or(p, key, f64::from(default), cmd)?; + if x.fract() != 0.0 || !COUNTS.contains(&x) { + let (min, max) = (COUNTS.start(), COUNTS.end()); + return Err(bad(cmd, format!("`{key}` must be a whole number between {min} and {max} (got {x})"))); + } + Ok(x as u32) +} + +/// The size `p` gives for `key`: a number that is not negative. +fn size(p: &Value, key: &str, cmd: &str) -> Result { + not_negative(f64_req(p, key, cmd)?, key, cmd) +} + +/// The size `p` gives for `key`, or `default` when `p` leaves it out or gives null. +fn size_or(p: &Value, key: &str, default: f64, cmd: &str) -> Result { + not_negative(number_or(p, key, default, cmd)?, key, cmd) +} + +fn not_negative(x: f64, key: &str, cmd: &str) -> Result { + if x < 0.0 { + return Err(bad(cmd, format!("`{key}` must be a number not below 0"))); + } + Ok(x) +} + +/// The four corner radii `p` gives as `radii` ([tl, tr, br, bl]), or `None` when `p` leaves it out +/// or gives null. Anything but four numbers that are not negative is an error. +fn radii_param(p: &Value, cmd: &str) -> Result> { + let key = "radii"; + match p.get(key) { + None | Some(Value::Null) => Ok(None), + Some(v) => { + let four = v.as_array().filter(|a| a.len() == 4).and_then(|a| { + let mut out = [0.0; 4]; + for (slot, item) in out.iter_mut().zip(a) { + *slot = item.as_f64().filter(|r| *r >= 0.0)?; + } + Some(out) + }); + four.map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be four numbers not below 0: [tl, tr, br, bl]"))) + } + } +} + +/// The mode `p` gives as `arcMode`, or `None` when `p` leaves it out or gives null. Any other +/// value than one of [`ARC_MODES`] (or `slice` or `open`) is an error. +fn arc_mode(p: &Value, cmd: &str) -> Result> { + let key = "arcMode"; + match p.get(key) { + None | Some(Value::Null) => Ok(None), + Some(v) => v.as_str().and_then(parse_mode).map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be one of {}", ARC_MODES.join(", ")))), + } +} + +fn parse_arc_params(p: &Value, cmd: &str) -> Result<((f64, f64), shapes::EllipseMode)> { let start = f64_or(p, "startAngle", 0.0); let end = f64_or(p, "endAngle", 360.0); - let mode = match str_param(p, "arcMode") { - Some("pie" | "slice") => shapes::EllipseMode::Slice, - Some("chord") => shapes::EllipseMode::Chord, - Some("arc" | "open") => shapes::EllipseMode::Arc, - _ => shapes::EllipseMode::Whole, - }; + let mode = arc_mode(p, cmd)?.unwrap_or(shapes::EllipseMode::Whole); let span = (end - start).rem_euclid(360.0); let pie = if mode == shapes::EllipseMode::Whole && (span < 1e-6 || (span - 360.0).abs() < 1e-6) { (0.0, 360.0) } else { (start, end) }; - (pie, mode) + Ok((pie, mode)) } fn rect_of(p: &Value, c: &str) -> Result { let x = f64_req(p, "x", c)?; let y = f64_req(p, "y", c)?; - let w = f64_req(p, "width", c)?; - let h = f64_req(p, "height", c)?; - Ok(Rect::new(x, y, x + w.abs(), y + h.abs()).abs()) + let w = size(p, "width", c)?; + let h = size(p, "height", c)?; + Ok(Rect::new(x, y, x + w, y + h)) } fn rect_xf(r: Rect, rot: f64) -> Affine { @@ -141,18 +222,9 @@ fn rectangle(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.rectangle"; check_numbers(p, &["radius", "startAngle", "endAngle", "rotation"], c)?; let r = rect_of(p, c)?; - let radius = f64_or(p, "radius", 0.0).max(0.0); - let mut radii = [radius; 4]; - if let Some(arr) = p.get("radii").and_then(Value::as_array) - && arr.len() == 4 - { - for (i, item) in arr.iter().enumerate() { - if let Some(val) = item.as_f64().filter(|n| n.is_finite()) { - radii[i] = val.max(0.0); - } - } - } - let (pie, arc_mode) = parse_arc_params(p); + let radius = size_or(p, "radius", 0.0, c)?; + let radii = radii_param(p, c)?.unwrap_or([radius; 4]); + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = rect_xf(r, f64_or(p, "rotation", 0.0)); let live = LiveShape::Rectangle { w: r.width(), h: r.height(), radii, kinds: Default::default(), radii_relative: false, xf }; let cs = CustomShape::Rectangle { w: r.width(), h: r.height(), radii, kinds: Default::default(), radii_relative: false, xf, pie, arc_mode }; @@ -164,7 +236,7 @@ fn ellipse(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.ellipse"; check_numbers(p, &["startAngle", "endAngle", "rotation"], c)?; let r = rect_of(p, c)?; - let (pie, arc_mode) = parse_arc_params(p); + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = rect_xf(r, f64_or(p, "rotation", 0.0)); let live = LiveShape::Ellipse { w: r.width(), h: r.height(), pie, xf }; let cs = CustomShape::Ellipse { w: r.width(), h: r.height(), pie, arc_mode, xf }; @@ -174,13 +246,13 @@ fn ellipse(s: &mut Session, p: &Value) -> Result { fn polygon(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.polygon"; let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?); - let r = f64_req(p, "radius", c)?.abs(); + let r = size(p, "radius", c)?; check_numbers(p, &["sides", "rotation", "rounded", "randomized", "startAngle", "endAngle"], c)?; - let sides = (f64_or(p, "sides", 6.0).round() as i64).clamp(3, 1000) as u32; + let sides = count(p, "sides", 6, c)?; let rot = f64_or(p, "rotation", 0.0); - let rounded = f64_or(p, "rounded", 0.0).clamp(-10.0, 10.0); - let randomized = f64_or(p, "randomized", 0.0).clamp(-10.0, 10.0); - let (pie, arc_mode) = parse_arc_params(p); + let rounded = ranged(p, "rounded", 0.0, &ROUNDING, c)?; + let randomized = ranged(p, "randomized", 0.0, &ROUNDING, c)?; + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = Affine::translate(center.to_vec2()) * vectorcraft_geom::rotate_deg(rot); let live = LiveShape::Polygon { radius: r, sides, xf, radii: vec![], kinds: vec![], radii_relative: false }; let cs = CustomShape::Polygon { @@ -202,16 +274,17 @@ fn polygon(s: &mut Session, p: &Value) -> Result { fn star(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.star"; let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?); - let r1 = f64_req(p, "radius1", c)?.abs(); + let r1 = size(p, "radius1", c)?; check_numbers(p, &["radius2", "points", "rotation", "rounded", "randomized", "startAngle", "endAngle"], c)?; - let r2 = f64_or(p, "radius2", r1 / 2.0).abs(); - let points = (f64_or(p, "points", 5.0).round() as i64).clamp(3, 1000) as u32; + // The inner radius is a fraction of the outer one, in `SPOKE_RATIO`. + let r2 = ranged(p, "radius2", r1 / 2.0, &(r1 * SPOKE_RATIO.start()..=r1 * SPOKE_RATIO.end()), c)?; + let points = count(p, "points", 5, c)?; let rot = f64_or(p, "rotation", 0.0); - let rounded = f64_or(p, "rounded", 0.0).clamp(-10.0, 10.0); - let randomized = f64_or(p, "randomized", 0.0).clamp(-10.0, 10.0); - let (pie, arc_mode) = parse_arc_params(p); + let rounded = ranged(p, "rounded", 0.0, &ROUNDING, c)?; + let randomized = ranged(p, "randomized", 0.0, &ROUNDING, c)?; + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = Affine::translate(center.to_vec2()) * vectorcraft_geom::rotate_deg(rot); - let ratio = if r1 > 1e-9 { (r2 / r1).clamp(0.01, 1.0) } else { 0.5 }; + let ratio = if r1 > 1e-9 { (r2 / r1).clamp(*SPOKE_RATIO.start(), *SPOKE_RATIO.end()) } else { 0.5 }; let cs = CustomShape::Polygon { radius: r1, sides: points, @@ -231,12 +304,12 @@ fn star(s: &mut Session, p: &Value) -> Result { fn spiral(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.spiral"; let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?); - let r = f64_req(p, "radius", c)?.abs(); + let r = size(p, "radius", c)?; check_numbers(p, &["turns", "divergence", "innerRadius"], c)?; check_switches(p, &["clockwise"], c)?; - let turns = f64_or(p, "turns", 3.0).clamp(0.01, 1024.0); - let divergence = f64_or(p, "divergence", 1.0).clamp(0.0, 1000.0); - let inner_radius = f64_or(p, "innerRadius", 0.0).clamp(0.0, 0.999); + let turns = ranged(p, "turns", 3.0, &TURNS, c)?; + let divergence = ranged(p, "divergence", 1.0, &DIVERGENCE, c)?; + let inner_radius = ranged(p, "innerRadius", 0.0, &INNER_RADIUS, c)?; let clockwise = bool_or(p, "clockwise", true); let cs = CustomShape::Spiral { center, radius: r, turns, divergence, inner_radius, clockwise }; insert_custom(s, "Spiral", None, cs) @@ -299,24 +372,54 @@ fn configure(s: &mut Session, p: &Value) -> Result { c, )?; check_switches(p, &["isStar", "parallelX", "parallelY", "parallelZ", "reset"], c)?; + check_configure(p, c)?; let ids = targets(s, p)?; let reset = bool_or(p, "reset", false); let spiral_hint = p.get("turns").is_some() || p.get("divergence").is_some() || p.get("innerRadius").is_some(); + // The shapes the params change, worked out first so that a call that changes nothing records + // no undo step. + let changes: Vec<(NodeId, CustomShape)> = { + let doc = &s.doc()?.doc; + ids.into_iter() + .filter_map(|id| { + let n = doc.node(id)?; + let before = CustomShape::read_for_update(n, spiral_hint)?; + let mut cs = before.clone(); + let stored = CustomShape::read(n).is_some(); + (apply_shape_params(&mut cs, p, reset) && (cs != before || !stored)).then_some((id, cs)) + }) + .collect() + }; + if changes.is_empty() { + return Ok(json!({ "updated": 0 })); + } let updated = s.edit("Live Shape", |d, _| { let mut count = 0usize; - for id in ids { - let Some(n) = d.node_mut(id) else { continue }; - let Some(mut cs) = CustomShape::read_for_update(n, spiral_hint) else { continue }; - if apply_shape_params(&mut cs, p, reset) { - cs.write_to_node(n); - count += 1; - } + for (id, cs) in &changes { + let Some(n) = d.node_mut(*id) else { continue }; + cs.write_to_node(n); + count += 1; } Ok(count) })?; Ok(json!({ "updated": updated })) } +/// Error unless every bounded value `p` gives is in its range, before any shape changes. +fn check_configure(p: &Value, c: &str) -> Result<()> { + size_or(p, "radius", 0.0, c)?; + radii_param(p, c)?; + arc_mode(p, c)?; + count(p, "sides", 6, c)?; + ranged(p, "spokeRatio", 0.5, &SPOKE_RATIO, c)?; + ranged(p, "rounded", 0.0, &ROUNDING, c)?; + ranged(p, "randomized", 0.0, &ROUNDING, c)?; + ranged(p, "turns", 3.0, &TURNS, c)?; + ranged(p, "divergence", 1.0, &DIVERGENCE, c)?; + ranged(p, "innerRadius", 0.0, &INNER_RADIUS, c)?; + Ok(()) +} + fn num_param(p: &Value, k: &str) -> Option { p.get(k).and_then(Value::as_f64).filter(|v| v.is_finite()) } @@ -331,8 +434,8 @@ fn apply_arc_params(p: &Value, pie: &mut (f64, f64), arc_mode: &mut shapes::Elli pie.1 = a1.rem_euclid(360.0); changed = true; } - if let Some(m) = str_param(p, "arcMode") { - *arc_mode = parse_mode(m); + if let Some(m) = str_param(p, "arcMode").and_then(parse_mode) { + *arc_mode = m; if *arc_mode == shapes::EllipseMode::Whole { *pie = (0.0, 360.0); } else if (pie.1 - pie.0).abs() < 1e-6 || ((pie.1 - pie.0).abs() - 360.0).abs() < 1e-6 { @@ -351,19 +454,14 @@ fn apply_rect_params(p: &Value, reset: bool, max_r: f64, radii: &mut [f64; 4], p *arc_mode = shapes::EllipseMode::Whole; changed = true; } + // A radius larger than half the rectangle's shorter side is fitted to that size. if let Some(r) = num_param(p, "radius") { *radii = [r.clamp(0.0, max_r); 4]; changed = true; } - if let Some(arr) = p.get("radii").and_then(Value::as_array) - && arr.len() == 4 - { - for (i, v) in arr.iter().enumerate() { - if let Some(r) = v.as_f64().filter(|x| x.is_finite()) { - radii[i] = r.clamp(0.0, max_r); - changed = true; - } - } + if let Ok(Some(four)) = radii_param(p, "addon.shapes.configure") { + *radii = four.map(|r| r.clamp(0.0, max_r)); + changed = true; } changed | apply_arc_params(p, pie, arc_mode) } @@ -378,8 +476,9 @@ fn apply_poly_params(p: &Value, reset: bool, sides: &mut u32, spoke_ratio: &mut } changed = true; } - if let Some(n_sides) = p.get("sides").and_then(Value::as_u64) { - *sides = (n_sides as u32).clamp(3, 1000); + // `check_configure` has refused values outside their ranges. + if let Some(n_sides) = num_param(p, "sides") { + *sides = n_sides as u32; changed = true; } if let Some(star) = p.get("isStar").and_then(Value::as_bool) { @@ -387,15 +486,15 @@ fn apply_poly_params(p: &Value, reset: bool, sides: &mut u32, spoke_ratio: &mut changed = true; } if let Some(sr) = num_param(p, "spokeRatio") { - *spoke_ratio = Some(sr.clamp(0.01, 1.0)); + *spoke_ratio = Some(sr); changed = true; } if let Some(rnd) = num_param(p, "rounded") { - *rounded = rnd.clamp(-10.0, 10.0); + *rounded = rnd; changed = true; } if let Some(rand) = num_param(p, "randomized") { - *randomized = rand.clamp(-10.0, 10.0); + *randomized = rand; changed = true; } changed @@ -429,15 +528,15 @@ fn apply_shape_params(cs: &mut CustomShape, p: &Value, reset: bool) -> bool { changed = true; } if let Some(t) = num_param(p, "turns") { - *turns = t.clamp(0.01, 1024.0); + *turns = t; changed = true; } if let Some(div) = num_param(p, "divergence") { - *divergence = div.clamp(0.0, 1000.0); + *divergence = div; changed = true; } if let Some(ir) = num_param(p, "innerRadius") { - *inner_radius = ir.clamp(0.0, 0.999); + *inner_radius = ir; changed = true; } changed @@ -470,11 +569,12 @@ fn apply_shape_params(cs: &mut CustomShape, p: &Value, reset: bool) -> bool { } } -fn parse_mode(m: &str) -> shapes::EllipseMode { +fn parse_mode(m: &str) -> Option { match m { - "pie" | "slice" => shapes::EllipseMode::Slice, - "chord" => shapes::EllipseMode::Chord, - "arc" | "open" => shapes::EllipseMode::Arc, - _ => shapes::EllipseMode::Whole, + "whole" => Some(shapes::EllipseMode::Whole), + "pie" | "slice" => Some(shapes::EllipseMode::Slice), + "chord" => Some(shapes::EllipseMode::Chord), + "arc" | "open" => Some(shapes::EllipseMode::Arc), + _ => None, } } diff --git a/crates/engine/src/cmd/addons/shapes/tests.rs b/crates/engine/src/cmd/addons/shapes/tests.rs index 793ae6074c..bc5f1359e2 100644 --- a/crates/engine/src/cmd/addons/shapes/tests.rs +++ b/crates/engine/src/cmd/addons/shapes/tests.rs @@ -135,3 +135,169 @@ fn addon_shapes_commands_and_format_roundtrip_preserve_paths() { assert_eq!(CustomShape::read(a), CustomShape::read(b)); } } + +fn id_of(s: &mut Session, cmd: &str, p: serde_json::Value) -> NodeId { + NodeId(s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))["id"].as_u64().unwrap()) +} + +fn shape_of(s: &Session, id: NodeId) -> CustomShape { + CustomShape::read(s.doc().unwrap().doc.node(id).unwrap()).unwrap() +} + +/// Runs `cmd` with `p`, which must fail with `msg` and leave the document and its history as they +/// were. +fn refused(s: &mut Session, cmd: &str, p: serde_json::Value, msg: &str) { + let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false); + let undo = s.doc().unwrap().history.undo.len(); + let e = s.execute(cmd, &p).map(|v| v.to_string()).unwrap_err().to_string(); + assert!(e.contains(msg), "{cmd} {p}: {e}"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{cmd} {p}"); + assert_eq!(vectorcraft_format::save(&s.doc().unwrap().doc, false), saved, "{cmd} {p}"); +} + +#[test] +fn polygon_and_star_refuse_counts_that_are_not_whole_numbers_from_3_to_1000() { + let mut s = new_session(); + for n in [json!(2.5), json!(2), json!(0), json!(-6), json!(1000.5), json!(5000), json!(4_294_967_299u64)] { + let x = n.as_f64().unwrap(); + let p = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": n}); + refused(&mut s, "addon.shapes.polygon", p, &format!("`sides` must be a whole number between 3 and 1000 (got {x})")); + let p = json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": n}); + refused(&mut s, "addon.shapes.star", p, &format!("`points` must be a whole number between 3 and 1000 (got {x})")); + } + refused(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": "6"}), "`sides` must be a number"); + refused(&mut s, "addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": [5]}), "`points` must be a number"); + + // The ends of the range are taken as given, and an omitted or null count is the default. + for (n, want) in [(json!(3), 3), (json!(1000), 1000), (json!(7.0), 7), (json!(null), 6)] { + let id = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": n})); + assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides, spoke_ratio: None, .. } if sides == want), "{n}"); + } + let id = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0})); + assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides: 6, .. })); + for (n, want) in [(json!(3), 3), (json!(1000), 1000), (json!(null), 5)] { + let id = id_of(&mut s, "addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": n})); + assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides, spoke_ratio: Some(_), .. } if sides == want), "{n}"); + } +} + +#[test] +fn shapes_refuse_numbers_outside_their_ranges() { + let mut s = new_session(); + let poly = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0}); + let star = json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0}); + let spiral = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0}); + let with = |base: &serde_json::Value, key: &str, v: f64| { + let mut p = base.clone(); + p[key] = json!(v); + p + }; + for (cmd, base, key, bad, good) in [ + ("addon.shapes.polygon", &poly, "rounded", [-10.5, 10.5], [-10.0, 10.0]), + ("addon.shapes.polygon", &poly, "randomized", [-11.0, 10.01], [-10.0, 10.0]), + ("addon.shapes.star", &star, "rounded", [-10.5, 10.5], [-10.0, 10.0]), + ("addon.shapes.star", &star, "randomized", [-11.0, 10.01], [-10.0, 10.0]), + ("addon.shapes.star", &star, "radius2", [0.05, 11.0], [0.1, 10.0]), + ("addon.shapes.spiral", &spiral, "turns", [0.0, 1024.5], [0.01, 1024.0]), + ("addon.shapes.spiral", &spiral, "divergence", [-0.1, 1000.5], [0.0, 1000.0]), + ("addon.shapes.spiral", &spiral, "innerRadius", [-0.1, 1.0], [0.0, 0.999]), + ] { + let (min, max) = (good[0], good[1]); + for x in bad { + refused(&mut s, cmd, with(base, key, x), &format!("`{key}` must be between {min} and {max} (got {x})")); + } + for x in good { + s.execute(cmd, &with(base, key, x)).unwrap_or_else(|e| panic!("{cmd} {key} {x}: {e}")); + } + } + + // Sizes are not negative. + for (cmd, p, key) in [ + ("addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": -10.0}), "radius"), + ("addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": -10.0}), "radius1"), + ("addon.shapes.spiral", json!({"cx": 0.0, "cy": 0.0, "radius": -10.0}), "radius"), + ("addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": -10.0, "height": 10.0}), "width"), + ("addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": 10.0, "height": 10.0, "radius": -1.0}), "radius"), + ("addon.shapes.ellipse", json!({"x": 0.0, "y": 0.0, "width": 10.0, "height": -10.0}), "height"), + ("addon.shapes.box3d", json!({"x": 0.0, "y": 0.0, "width": -10.0, "height": 10.0}), "width"), + ] { + refused(&mut s, cmd, p, &format!("`{key}` must be a number not below 0")); + } + + // Corner radii are four numbers that are not negative. + for radii in [json!([1.0, 2.0, 3.0]), json!([1.0, 2.0, 3.0, -4.0]), json!([1.0, 2.0, 3.0, "4"]), json!(4.0)] { + let p = json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "radii": radii}); + refused(&mut s, "addon.shapes.rectangle", p, "`radii` must be four numbers not below 0: [tl, tr, br, bl]"); + } + + // An arc mode is one of the names the commands take. + for mode in [json!("slices"), json!(""), json!(3)] { + let p = json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "arcMode": mode}); + refused(&mut s, "addon.shapes.rectangle", p.clone(), "`arcMode` must be one of whole, pie, chord, arc"); + refused(&mut s, "addon.shapes.ellipse", p, "`arcMode` must be one of whole, pie, chord, arc"); + } + for mode in ["whole", "pie", "slice", "chord", "arc", "open"] { + s.execute("addon.shapes.ellipse", &json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "arcMode": mode})).unwrap(); + } +} + +#[test] +fn configure_refuses_values_outside_their_ranges_before_changing_any_shape() { + let mut s = new_session(); + let rect = id_of(&mut s, "addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "radius": 4.0})); + let poly = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 50.0, "cy": 50.0, "radius": 40.0, "sides": 5})); + let spiral = id_of(&mut s, "addon.shapes.spiral", json!({"cx": 60.0, "cy": 60.0, "radius": 45.0})); + let ids = json!([rect.0, poly.0, spiral.0]); + let cfg = |key: &str, v: serde_json::Value| json!({"ids": ids.clone(), key: v}); + let c = "addon.shapes.configure"; + for n in [json!(2.5), json!(2), json!(-1), json!(5000), json!(4_294_967_299u64)] { + let x = n.as_f64().unwrap(); + refused(&mut s, c, cfg("sides", n), &format!("`sides` must be a whole number between 3 and 1000 (got {x})")); + } + for (key, bad, min, max) in [ + ("spokeRatio", [0.0, 1.5], 0.01, 1.0), + ("rounded", [-10.5, 11.0], -10.0, 10.0), + ("randomized", [-11.0, 10.5], -10.0, 10.0), + ("turns", [0.0, 2000.0], 0.01, 1024.0), + ("divergence", [-1.0, 1000.5], 0.0, 1000.0), + ("innerRadius", [-0.5, 1.0], 0.0, 0.999), + ] { + for x in bad { + refused(&mut s, c, cfg(key, json!(x)), &format!("`{key}` must be between {min} and {max} (got {x})")); + } + } + refused(&mut s, c, cfg("radius", json!(-1.0)), "`radius` must be a number not below 0"); + for radii in [json!([1.0, 2.0]), json!([1.0, 2.0, 3.0, -1.0]), json!("4")] { + refused(&mut s, c, cfg("radii", radii), "`radii` must be four numbers not below 0: [tl, tr, br, bl]"); + } + refused(&mut s, c, cfg("arcMode", json!("bogus")), "`arcMode` must be one of whole, pie, chord, arc"); + // A valid value next to a refused one changes no shape either. + refused(&mut s, c, json!({"ids": ids.clone(), "sides": 8, "turns": 0.0}), "`turns` must be between 0.01 and 1024 (got 0)"); + + // In range, the values are taken as given; a radius larger than the rectangle takes is fitted to it. + s.execute(c, &json!({"ids": [poly.0], "sides": 1000, "rounded": -10.0, "randomized": 10.0})).unwrap(); + assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 1000, rounded, randomized, .. } if rounded == -10.0 && randomized == 10.0)); + s.execute(c, &json!({"ids": [poly.0], "sides": 3})).unwrap(); + assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 3, .. })); + s.execute(c, &json!({"ids": [spiral.0], "turns": 1024.0, "divergence": 0.0, "innerRadius": 0.999})).unwrap(); + assert!( + matches!(shape_of(&s, spiral), CustomShape::Spiral { turns, divergence, inner_radius, .. } if turns == 1024.0 && divergence == 0.0 && inner_radius == 0.999) + ); + s.execute(c, &json!({"ids": [rect.0], "radius": 1000.0})).unwrap(); + assert!(matches!(shape_of(&s, rect), CustomShape::Rectangle { radii, .. } if radii == [40.0; 4])); +} + +#[test] +fn configure_records_no_undo_step_when_nothing_changes() { + let mut s = new_session(); + let poly = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 50.0, "cy": 50.0, "radius": 40.0, "sides": 5, "rounded": 0.5})); + let undo = s.doc().unwrap().history.undo.len(); + let r = s.execute("addon.shapes.configure", &json!({"ids": [poly.0], "sides": 5, "rounded": 0.5})).unwrap(); + assert_eq!(r["updated"], 0); + assert_eq!(s.doc().unwrap().history.undo.len(), undo); + + let r = s.execute("addon.shapes.configure", &json!({"ids": [poly.0], "sides": 6})).unwrap(); + assert_eq!(r["updated"], 1); + assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1); + assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 6, .. })); +} diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index bdb238ce83..f457712415 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -127,6 +127,17 @@ fn node_refs(n: &Node, f: Visit) { if let Some(cm) = Document::color_mask_of(m) { f(Ref::Name(Res::Image, &cm.key)); } + // A graph keeps paints and style names for its series and labels, which a redraw gives them. + if let Some(g) = m.extras().graph.as_deref() { + vectorcraft_doc::swatches::graph_paints(m).for_each(|p| paint_refs(p, f)); + let l = &g.label_styles; + for s in [&l.values, &l.categories, &l.legend].into_iter().flatten().filter_map(|st| st.style_name.as_ref()) { + f(Ref::Name(Res::CharStyle, s)); + } + for s in [&l.values_para, &l.categories_para, &l.legend_para].into_iter().flatten().filter_map(|pa| pa.style_name.as_ref()) { + f(Ref::Name(Res::ParaStyle, s)); + } + } match &m.kind { NodeKind::Text(t) => { for r in &t.runs { @@ -671,6 +682,27 @@ impl Relink { self.name(Res::Image, &mut cm.key); self.name(Res::Image, &mut cm.from); } + if let Some(g) = n.extras_if_any().and_then(|e| e.graph.as_deref_mut()) { + for s in &mut g.series_paints { + s.fill.iter_mut().chain(s.stroke.iter_mut()).for_each(|p| self.paint(p)); + } + let l = &mut g.label_styles; + for st in [&mut l.values, &mut l.categories, &mut l.legend].into_iter().flatten() { + self.paint(&mut st.fill); + self.paint(&mut st.stroke); + if let Some(s) = &mut st.style_name { + self.name(Res::CharStyle, s); + } + } + for pa in [&mut l.values_para, &mut l.categories_para, &mut l.legend_para].into_iter().flatten() { + if let Some(s) = &mut pa.style_name { + self.name(Res::ParaStyle, s); + } + } + for r in [&mut l.values_rows, &mut l.categories_rows, &mut l.legend_rows].into_iter().flatten() { + r.paint.fill.iter_mut().chain(r.paint.stroke.iter_mut()).for_each(|p| self.paint(p)); + } + } match &mut n.kind { NodeKind::Text(t) => { for r in &mut t.runs { diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 576b05ffd6..9ecb5c3984 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -568,12 +568,11 @@ fn switch_units(s: &mut Session, _: &Value) -> Result { Ok(json!({ "units": next.label() })) } -/// The `artboard` param of the ruler origin commands: an artboard index, 0 when not given. +/// The `artboard` param of the ruler origin commands: an artboard index, 0 when left out or null. A +/// value that is not a whole number is "`artboard` must be a whole number"; one past the artboards +/// is "index out of range". fn artboard_param(s: &Session, p: &Value, c: &str) -> Result { - let index = match p.get("artboard") { - None => 0, - Some(v) => v.as_u64().and_then(|n| usize::try_from(n).ok()).ok_or_else(|| bad(c, "index out of range"))?, - }; + let index = super::index_opt(p, "artboard", c)?.unwrap_or(0); if index >= s.doc()?.doc.artboards.len() { return Err(bad(c, "index out of range")); } @@ -583,14 +582,15 @@ fn artboard_param(s: &Session, p: &Value, c: &str) -> Result { fn ruler_origin(s: &mut Session, p: &Value) -> Result { let d = &s.doc()?.doc; // A document without artboards measures from its global zero point. - let index = if d.artboards.is_empty() && p.get("artboard").is_none() { 0 } else { artboard_param(s, p, "document.rulerOrigin")? }; + let index = + if d.artboards.is_empty() && p.get("artboard").is_none_or(Value::is_null) { 0 } else { artboard_param(s, p, "document.rulerOrigin")? }; Ok(json!(s.doc()?.doc.ruler_origin_for(index))) } fn set_ruler_origin(s: &mut Session, p: &Value) -> Result { const C: &str = "document.setRulerOrigin"; let origin = Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?); - let index = if s.doc()?.doc.rulers == RulerMode::Global && p.get("artboard").is_none() { 0 } else { artboard_param(s, p, C)? }; + let index = if s.doc()?.doc.rulers == RulerMode::Global && p.get("artboard").is_none_or(Value::is_null) { 0 } else { artboard_param(s, p, C)? }; change_ruler_origin(s, index, Some(origin), "Ruler Origin") } diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 6ee3905d95..65672f8b6b 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -865,8 +865,8 @@ fn load_texture(params: &mut Value, cmd: &str) -> Result<()> { /// Has `n` an appearance Expand Appearance turns into objects: a path that isn't basic (several /// fills or strokes, effects, transparency of a fill or stroke, a brush), a group or layer with /// fills, strokes or effects of its own or such members, other objects with effects, type with -/// fills or strokes of its own, or a symbol instance whose active fill or stroke has a geometry -/// effect ([`effects::has_item_geometry`])? +/// fills or strokes of its own, or a symbol instance whose active fill or stroke has a geometry or +/// raster effect ([`effects::has_item_geometry`], [`effects::has_instance_row_raster`])? pub(crate) fn expandable(n: &Node) -> bool { expandable_except(n, &[]) } @@ -883,7 +883,9 @@ pub(crate) fn expandable_except(n: &Node, skip: &[NodeId]) -> bool { !n.appearance.items.is_empty() || !n.appearance.effects.is_empty() || children.iter().any(|c| expandable_except(c, skip)) } NodeKind::Text(_) => !n.appearance.items.is_empty() || !n.appearance.effects.is_empty(), - NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => effects::has_item_geometry(n) || !n.appearance.effects.is_empty(), + NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => { + effects::has_item_geometry(n) || effects::has_instance_row_raster(n) || !n.appearance.effects.is_empty() + } _ => !n.appearance.effects.is_empty(), } } diff --git a/crates/engine/src/cmd/fileio/tests_pdf.rs b/crates/engine/src/cmd/fileio/tests_pdf.rs index bd52fe71c7..f57e02fc2d 100644 --- a/crates/engine/src/cmd/fileio/tests_pdf.rs +++ b/crates/engine/src/cmd/fileio/tests_pdf.rs @@ -304,11 +304,22 @@ fn options_not_applied_yet_and_document_features_warn() { let frame = s.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap(); s.execute("paint.setStroke", &json!({"ids": [frame], "swatch": "Dots"})).unwrap(); assert_eq!(warnings(&export(&mut s, json!({}))).len(), 0); - // A knockout group is approximated: its warning comes back from every PDF path. + // A knockout group is written as a PDF knockout group: nothing to report. s.execute("select.set", &json!({"ids": [frame]})).unwrap(); s.execute("object.group", &json!({})).unwrap(); s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); let knockout = |w: Vec| w.iter().any(|w| w.contains("knockout groups")); + assert!(!knockout(warnings(&export(&mut s, json!({}))))); + // One whose soft masks would draw a brushed stroke is approximated: its warning comes back + // from every PDF path. + let rect = + |s: &mut Session, x: i32| s.execute("shape.rectangle", &json!({"x": x, "y": 45, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap(); + let (lower, upper) = (rect(&mut s, 20), rect(&mut s, 40)); + s.execute("brush.apply", &json!({"ids": [upper], "name": "3 pt. Round"})).unwrap(); + s.execute("object.setProps", &json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap(); + s.execute("select.set", &json!({"ids": [lower, upper]})).unwrap(); + s.execute("object.group", &json!({})).unwrap(); + s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); assert!(knockout(warnings(&export(&mut s, json!({}))))); assert!(knockout(warnings(&s.execute("document.export", &as_export(&json!({}))).unwrap()))); // Other formats have none. @@ -432,6 +443,17 @@ fn pdf_settings_summarise_changes_and_warnings() { s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "ids": [g]})).unwrap(); assert_eq!(s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap()["warnings"], json!([])); s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); + // A knockout group is written as a PDF knockout group: nothing to report. + assert_eq!(s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap()["warnings"], json!([])); + // One whose soft masks would draw a brushed stroke is approximated. + let rect = + |s: &mut Session, x: i32| s.execute("shape.rectangle", &json!({"x": x, "y": 45, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap(); + let (lower, upper) = (rect(&mut s, 20), rect(&mut s, 40)); + s.execute("brush.apply", &json!({"ids": [upper], "name": "3 pt. Round"})).unwrap(); + s.execute("object.setProps", &json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap(); + s.execute("select.set", &json!({"ids": [lower, upper]})).unwrap(); + s.execute("object.group", &json!({})).unwrap(); + s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); assert_eq!(s.execute("document.pdfSettings", &json!({})).unwrap()["warnings"], json!([])); let full = s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap(); assert!(warnings(&full).iter().any(|w| w.contains("knockout groups")), "{}", full["warnings"]); diff --git a/crates/engine/src/cmd/gradient.rs b/crates/engine/src/cmd/gradient.rs index 54ca07da7e..b47fd0e5e3 100644 --- a/crates/engine/src/cmd/gradient.rs +++ b/crates/engine/src/cmd/gradient.rs @@ -33,7 +33,7 @@ pub fn specs() -> Vec { "Gradient Vector", [], None, - "{start?: [x,y], end?: [x,y] (document coordinates; both or neither: omitted, the vector stays), aspect?: % (radial, conic and diamond: the extent's height / width across the vector, a negative one mirroring a conic's sweep and an error for the other kinds; default: kept), focal?: [x,y] (document coordinates) | null (radial: the focal point, where the first stop sits, pulled inside the extent ellipse; null centres it; default: it keeps its place in the ellipse; an error on a conic or diamond gradient), ids?|id?, stroke?: bool (default: the targeted item's kind, else the active proxy), item?: fill/stroke item index|null (alias: index; omitted: the Appearance panel's active item when it is of the edited kind), target?: \"object\"|\"contents\", index?: appearance item index (as item)} set the gradient vector, aspect ratio and focal point (solid paints become the default gradient; type objects set it on their runs, in text space). On a path directly in a graph series, the graph also stores the gradient with its placement when it is on the topmost visible fill or stroke (docs/mcp.md); on a graph label's row, the graph stores the label's topmost visible fill and stroke rows for every label of its kind, with the gradient fitted to each label", + "{start?: [x,y], end?: [x,y] (document coordinates; both or neither: omitted, the vector stays), aspect?: % (radial, conic and diamond: the extent's height / width across the vector, a negative one mirroring a conic's sweep and an error for the other kinds; default: kept), focal?: [x,y] (document coordinates) | null (radial: the focal point, where the first stop sits, pulled inside the extent ellipse; null centres it; default: it keeps its place in the ellipse; an error on a conic or diamond gradient), ids?|id?, stroke?: bool (default: the targeted item's kind, else the active proxy), item?: fill/stroke item index|null (alias: index; omitted: the Appearance panel's active item when it is of the edited kind), target?: \"object\"|\"contents\", index?: appearance item index (as item)} set the gradient vector, aspect ratio and focal point (solid paints become the default gradient; type objects set it on their runs, in text space). On a path directly in a graph series, the graph also stores the gradient with its placement relative to the path's box when it is on the topmost visible fill or stroke, and every path of the series takes it at the same place on its own box (docs/mcp.md); on a graph label's row, the graph stores the label's topmost visible fill and stroke rows for every label of its kind, with the gradient fitted to each label", has_doc, set_gradient_geom ), diff --git a/crates/engine/src/cmd/graph/generate.rs b/crates/engine/src/cmd/graph/generate.rs index a789304b24..69510652b9 100644 --- a/crates/engine/src/cmd/graph/generate.rs +++ b/crates/engine/src/cmd/graph/generate.rs @@ -6,7 +6,7 @@ use super::labels::{ recolour_text, runs, shadow_of, }; use super::markers::{MARKER_BUDGET, marker_cost, marker_designs}; -use super::series::{series_marks, with_series_opacity}; +use super::series::{series_marks, with_series_gradients, with_series_opacity}; use super::{ Arc, BezPath, Color, Document, GraphKind, GraphSpec, Justify, Node, NodeKind, Paint, PathData, Point, Rect, TickLength, ValueAxisSide, shapes, }; @@ -1006,7 +1006,7 @@ impl<'a> Plot<'a> { } } let name = g.series.get(s).cloned().filter(|n| !n.is_empty()).unwrap_or_else(|| format!("Series {}", s + 1)); - let grp = self.b.series(&name, s, with_series_opacity(g, s, items)); + let grp = self.b.series(&name, s, with_series_opacity(g, s, with_series_gradients(g, s, items))); self.b.out.push(grp); } self.b.out.append(&mut self.on_top); diff --git a/crates/engine/src/cmd/graph/series.rs b/crates/engine/src/cmd/graph/series.rs index 51a7ead550..aeaceb9e91 100644 --- a/crates/engine/src/cmd/graph/series.rs +++ b/crates/engine/src/cmd/graph/series.rs @@ -2,7 +2,7 @@ use vectorcraft_color::BlendMode; use super::axis::radar_axis; use super::labels::{Role, filled_lines_on, label_para, label_style}; -use super::{Arc, CharStyle, Color, Document, GraphKind, GraphSpec, HashMap, Node, NodeId, NodeKind, Paint, Result, Session, Value, bad}; +use super::{Arc, CharStyle, Color, Document, GraphKind, GraphSpec, HashMap, Node, NodeId, NodeKind, Paint, Rect, Result, Session, Value, bad}; // ---------- generation ---------- @@ -229,7 +229,8 @@ pub(crate) fn capture_series_top_paints(doc: &mut Document, edited: &[(NodeId, b continue; } let (paint, width) = shown_paint(node, *fill); - captures.push((r.graph, r.index, *fill, paint, (!*fill).then_some(width))); + let width = (!*fill).then_some(width); + captures.push((r.graph, r.index, *fill, paint, width, paint_box(node, width))); } store_series_paints(doc, captures) } @@ -281,11 +282,129 @@ fn settle_series_paints(doc: &mut Document, before: SeriesPaintsBefore) -> bool changed } -/// Store each capture (graph, series index, fill or stroke, paint, stroke weight) in its series. Returns whether a +/// The box a series keeps a gradient relative to: (0, 0)–(1, 1), as graphic styles keep theirs. +const UNIT_BOX: Rect = vectorcraft_doc::GraphicStyle::UNIT_BOX; + +/// The gradient `slot` keeps for its fill (`fill`) or stroke as the graph draws it on box `on`: one kept relative to the +/// unit box placed on `on`, also when `on` has no width or no height, and one kept in document coordinates as it is kept. +/// None when the slot keeps no gradient, or keeps one relative to the unit box and there is no box. +fn kept_on(slot: &vectorcraft_doc::SeriesPaint, fill: bool, on: Option) -> Option { + let (kept, unit) = if fill { (&slot.fill, slot.fill_unit_box) } else { (&slot.stroke, slot.stroke_unit_box) }; + let Some(Paint::Gradient(g)) = kept else { return None }; + if !unit { + return Some(Paint::Gradient(g.clone())); + } + let on = on?; + let mut g = g.clone(); + g.rebase(UNIT_BOX, on); + Some(Paint::Gradient(g)) +} + +/// Whether `a` and `b` are the same paint, a gradient's vector, focal point and freeform points each within 1e-6 pt (the +/// rounding a move or a placement on another box leaves). +fn same_paint_placed(a: &Paint, b: &Paint) -> bool { + let (Paint::Gradient(a), Paint::Gradient(b)) = (a, b) else { return a == b }; + let near = |p: vectorcraft_geom::Point, q: vectorcraft_geom::Point| (p - q).hypot() <= 1e-6; + let mut a = (**a).clone(); + match (&mut a.geom, &b.geom) { + (Some(ag), Some(bg)) => { + let focal = match (ag.focal, bg.focal) { + (Some(p), Some(q)) => near(p, q), + (p, q) => p.is_none() && q.is_none(), + }; + if !(near(ag.start, bg.start) && near(ag.end, bg.end) && focal) { + return false; + } + (ag.start, ag.end, ag.focal) = (bg.start, bg.end, bg.focal); + } + (None, None) => {} + _ => return false, + } + match (&mut a.freeform, &b.freeform) { + (Some(af), Some(bf)) => { + if af.points.len() != bf.points.len() || !af.points.iter().zip(&bf.points).all(|(p, q)| near(p.at, q.at)) { + return false; + } + for (p, q) in af.points.iter_mut().zip(&bf.points) { + p.at = q.at; + } + } + (None, None) => {} + _ => return false, + } + a == **b +} + +/// The box a gradient on `node`'s fill (`width` None) or on its stroke of `width` is placed on: the geometric bounds, grown +/// by half the weight for a stroke. The box can have no width or no height. +fn paint_box(node: &Node, width: Option) -> Option { + let b = node.geometric_bounds()?; + Some(width.map_or(b, |w| vectorcraft_doc::appearance::stroke_paint_bounds(b, w))) +} + +/// `paint` as a series keeps it: a gradient placed on `from` (the edited mark's [`paint_box`]) is moved to the same place +/// relative to [`UNIT_BOX`], so each mark takes it on its own box. A box with no width or no height, which a gradient's +/// place cannot be kept relative to, leaves the paint in document coordinates. Returns the paint and whether it was moved. +fn unit_box_paint(paint: Paint, from: Option) -> (Paint, bool) { + match (paint, from.filter(|b| b.width() > 1e-9 && b.height() > 1e-9)) { + (Paint::Gradient(mut g), Some(from)) => { + g.rebase(from, UNIT_BOX); + (Paint::Gradient(g), true) + } + (paint, _) => (paint, false), + } +} + +/// The paths directly inside series `i`'s group (`items`) with the series' kept gradients, which are kept relative to the +/// unit box, placed on each path's own box: a fill on its geometric bounds, a stroke on its stroke's box. Paints kept in +/// document coordinates (from files saved before the unit box) are left as they are. +pub(super) fn with_series_gradients(g: &GraphSpec, i: usize, mut items: Vec>) -> Vec> { + let Some(slot) = g.series_paints.get(i) else { return items }; + fn kept(p: &Option, unit: bool) -> Option<&Paint> { + p.as_ref().filter(|p| unit && matches!(p, Paint::Gradient(_))) + } + let (fill, stroke) = (kept(&slot.fill, slot.fill_unit_box), kept(&slot.stroke, slot.stroke_unit_box)); + if fill.is_none() && stroke.is_none() { + return items; + } + // A Filled Lines band takes the kept stroke as its fill when no fill is kept. + let band_fill = if slot.fill.is_none() { stroke } else { None }; + let fill_from_store = |p: &Paint| fill == Some(p) || band_fill == Some(p); + let stroke_from_store = |p: &Paint| stroke == Some(p); + for item in &mut items { + if !matches!(item.kind, NodeKind::Path { .. }) { + continue; + } + let Some(b) = item.geometric_bounds() else { continue }; + let ap = &item.appearance; + if !ap.fill().is_some_and(|f| fill_from_store(&f.paint)) && !ap.stroke().is_some_and(|st| stroke_from_store(&st.paint)) { + continue; + } + let ap = &mut Arc::make_mut(item).appearance; + if let Some(f) = ap.fill_mut().filter(|f| fill_from_store(&f.paint)) + && let Paint::Gradient(gp) = &mut f.paint + { + gp.rebase(UNIT_BOX, b); + } + if let Some(st) = ap.stroke_mut().filter(|st| stroke_from_store(&st.paint)) { + let to = st.paint_bounds(b); + if let Paint::Gradient(gp) = &mut st.paint { + gp.rebase(UNIT_BOX, to); + } + } + } + items +} + +/// A paint taken from a series mark: graph, series index, fill or stroke, paint, stroke weight and the mark's +/// [`paint_box`]. +type Capture = (NodeId, usize, bool, Paint, Option, Option); + +/// Store each capture in its series; a gradient is kept relative to the unit box ([`unit_box_paint`]). Returns whether a /// series' captured fill or stroke differs from what the graph stored before the first capture. -fn store_series_paints(doc: &mut Document, captures: Vec<(NodeId, usize, bool, Paint, Option)>) -> bool { +fn store_series_paints(doc: &mut Document, captures: Vec) -> bool { let mut before = SeriesPaintsBefore::new(); - for (graph, index, fill, paint, width) in captures { + for (graph, index, fill, paint, width, from) in captures { let Some(spec) = doc.node_mut(graph).and_then(|n| n.extras_if_any().and_then(|e| e.graph.as_deref_mut())) else { continue }; before.entry(graph).or_insert_with(|| spec.series_paints.clone()); // `index` is below the graph's series count (`series_members`). @@ -293,10 +412,17 @@ fn store_series_paints(doc: &mut Document, captures: Vec<(NodeId, usize, bool, P spec.series_paints.resize(index + 1, vectorcraft_doc::SeriesPaint::default()); } let Some(slot) = spec.series_paints.get_mut(index) else { continue }; + // A mark that shows the kept gradient as it was drawn leaves the kept gradient as it is. + let drawn = kept_on(slot, fill, from).is_some_and(|kept| same_paint_placed(&kept, &paint)); + let (paint, unit) = unit_box_paint(paint, from); if fill { - slot.fill = Some(paint); + if !drawn { + (slot.fill, slot.fill_unit_box) = (Some(paint), unit); + } } else { - slot.stroke = Some(paint); + if !drawn { + (slot.stroke, slot.stroke_unit_box) = (Some(paint), unit); + } slot.stroke_width = width; } } diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index c997e803d5..45ce3e22e9 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -2754,10 +2754,39 @@ fn a_gradient_edit_on_a_series_mark_is_kept() { series_0_is(&mut s, id, (&fill, 0.4, &red, 2.0), &was); } -/// A gradient placed on a series mark's fill with `paint.setGradientGeom` is stored on the graph as it is placed, and -/// every mark of the series has that fill, placement included, after the graph is drawn again. +/// Where gradient `paint` sits on box `b`: its vector's start and end, then each freeform point, as fractions of the box. +fn gradient_place(paint: &Paint, b: vectorcraft_geom::Rect) -> Vec { + let Paint::Gradient(g) = paint else { panic!("not a gradient: {paint:?}") }; + let points = g.geom.iter().flat_map(|geom| [geom.start, geom.end]).chain(g.freeform.iter().flat_map(|f| f.points.iter().map(|p| p.at))); + points.flat_map(|p| [(p.x - b.x0) / b.width(), (p.y - b.y0) / b.height()]).collect() +} + +/// The box a gradient on `mark`'s fill (`stroke` false) or stroke is placed on. +fn paint_box_of(mark: &vectorcraft_doc::Node, stroke: bool) -> vectorcraft_geom::Rect { + let b = mark.geometric_bounds().unwrap(); + if stroke { vectorcraft_doc::appearance::stroke_paint_bounds(b, mark.appearance.stroke_width()) } else { b } +} + +/// The fill (`stroke` false) or stroke of every path of series 0, the legend swatch included, has a gradient at `place` on +/// its own box, and the gradients are not all at one place in the document. Returns how many paths were checked. +fn series_0_gradients_at(s: &Session, id: NodeId, stroke: bool, place: &[f64]) -> usize { + let n = s.doc().unwrap().doc.node(id).unwrap().clone(); + let paths: Vec<_> = series(&n, 0).children().unwrap().iter().filter(|m| matches!(m.kind, NodeKind::Path { .. })).cloned().collect(); + let paints: Vec = paths.iter().map(|m| if stroke { m.appearance.stroke_paint() } else { m.appearance.fill_paint() }).collect(); + for (m, paint) in paths.iter().zip(&paints) { + let at = gradient_place(paint, paint_box_of(m, stroke)); + assert_eq!(at.len(), place.len(), "{at:?}"); + assert!(at.iter().zip(place).all(|(a, b)| (a - b).abs() < 1e-9), "{at:?} is not {place:?}"); + } + assert!(paints.windows(2).any(|w| w[0] != w[1]), "every mark has the gradient at one place: {paints:?}"); + paths.len() +} + +/// A gradient placed on a series mark's fill with `paint.setGradientGeom` is stored relative to the mark's box, and every +/// mark of the series has it at the same place on its own box after the graph is drawn again, also after a data change +/// moves and resizes the marks and after the graph is moved. #[test] -fn a_gradient_placed_on_a_series_mark_is_kept_with_its_placement() { +fn a_gradient_placed_on_a_series_mark_takes_the_same_place_on_every_mark() { let (mut s, id, _, marks) = column_marks(); // `paint.setFill` stores the gradient before it is placed. s.execute("paint.setFill", &json!({"ids": [marks[0].0], "gradient": {"kind": "linear"}})).unwrap(); @@ -2766,26 +2795,258 @@ fn a_gradient_placed_on_a_series_mark_is_kept_with_its_placement() { s.execute("paint.setGradientGeom", &json!({"ids": [marks[0].0], "start": [0, 0], "end": [10, 40]})).unwrap(); let placed = fill_of(&s, marks[0]); assert_ne!(placed, fill, "the command places the gradient"); - assert_eq!(spec(&s, id).series_paints[0].fill, Some(placed.clone())); - s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap(); - assert!(marks_of(&s, id, 0).iter().all(|m| m.0 == placed), "{:?}", marks_of(&s, id, 0)); + let mark = s.doc().unwrap().doc.node(marks[0]).unwrap().clone(); + let place = gradient_place(&placed, paint_box_of(&mark, false)); + let kept = spec(&s, id).series_paints[0].clone(); + assert!(kept.fill_unit_box); + assert_eq!(gradient_place(kept.fill.as_ref().unwrap(), vectorcraft_doc::GraphicStyle::UNIT_BOX), place); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4], [6, 1]]})).unwrap(); + assert!(series_0_gradients_at(&s, id, false, &place) >= 4); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + s.execute("object.move", &json!({"dx": 50, "dy": 30})).unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[2, 2], [5, 4], [1, 1]]})).unwrap(); + series_0_gradients_at(&s, id, false, &place); } -/// A point recolored with `paint.freeform.setPoint` in a freeform gradient on a series mark's fill is stored on the graph. +/// A gradient placed on a series mark's stroke is kept relative to the mark's stroke box, and every mark of the series has +/// it at the same place on its own stroke box after the graph is drawn again. +#[test] +fn a_gradient_placed_on_a_series_marks_stroke_takes_the_same_place_on_every_mark() { + let (mut s, id, _, marks) = column_marks(); + s.execute("paint.setStroke", &json!({"ids": [marks[1].0], "gradient": {"kind": "radial"}})).unwrap(); + s.execute("stroke.set", &json!({"ids": [marks[1].0], "weight": 4})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [marks[1].0], "stroke": true, "start": [150, 200], "end": [170, 260]})).unwrap(); + let mark = s.doc().unwrap().doc.node(marks[1]).unwrap().clone(); + let place = gradient_place(&mark.appearance.stroke_paint(), paint_box_of(&mark, true)); + assert!(spec(&s, id).series_paints[0].stroke_unit_box); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4], [6, 1]]})).unwrap(); + series_0_gradients_at(&s, id, true, &place); +} + +/// A placed gradient picked up with the Eyedropper from art outside the graph is kept relative to the series mark it is +/// dropped on, and every mark of the series has it at the same place on its own box. +#[test] +fn a_placed_gradient_dropped_on_a_series_mark_takes_the_same_place_on_every_mark() { + let (mut s, id, _, marks) = column_marks(); + let r = NodeId(s.execute("shape.rectangle", &json!({"x": 600, "y": 600, "width": 40, "height": 20})).unwrap()["id"].as_u64().unwrap()); + s.execute("paint.setFill", &json!({"ids": [r.0], "gradient": {"kind": "linear"}})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [r.0], "start": [610, 600], "end": [630, 620]})).unwrap(); + s.execute("appearance.copyFrom", &json!({"source": r.0, "ids": [marks[2].0]})).unwrap(); + let mark = s.doc().unwrap().doc.node(marks[2]).unwrap().clone(); + let place = gradient_place(&mark.appearance.fill_paint(), paint_box_of(&mark, false)); + assert!(spec(&s, id).series_paints[0].fill_unit_box); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4], [6, 1]]})).unwrap(); + series_0_gradients_at(&s, id, false, &place); +} + +/// A graphic style with a placed gradient applied to a series mark is kept relative to that mark's box, and every mark of +/// the series has the gradient at the same place on its own box. +#[test] +fn a_graphic_style_gradient_applied_to_a_series_mark_takes_the_same_place_on_every_mark() { + let (mut s, id, _, marks) = column_marks(); + let r = NodeId(s.execute("shape.rectangle", &json!({"x": 600, "y": 600, "width": 40, "height": 20})).unwrap()["id"].as_u64().unwrap()); + s.execute("paint.setFill", &json!({"ids": [r.0], "gradient": {"kind": "linear"}})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [r.0], "start": [610, 600], "end": [630, 620]})).unwrap(); + s.execute("graphicStyle.new", &json!({"id": r.0, "name": "Placed"})).unwrap(); + s.execute("graphicStyle.apply", &json!({"name": "Placed", "ids": [marks[2].0]})).unwrap(); + let mark = s.doc().unwrap().doc.node(marks[2]).unwrap().clone(); + let place = gradient_place(&mark.appearance.fill_paint(), paint_box_of(&mark, false)); + assert!(spec(&s, id).series_paints[0].fill_unit_box); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4], [6, 1]]})).unwrap(); + series_0_gradients_at(&s, id, false, &place); +} + +/// A point recolored with `paint.freeform.setPoint` in a freeform gradient on a series mark's fill is stored on the graph, +/// and every mark of the series has the gradient's points at the same place on its own box after the graph is drawn again. #[test] fn a_freeform_gradient_edit_on_a_series_mark_is_kept() { let (mut s, id, _, marks) = column_marks(); let points = json!([{"at": [20, 20], "color": "#53aadf"}, {"at": [50, 50], "color": "#ff8000"}]); // `paint.setFill` stores the gradient before its point is recolored. s.execute("paint.setFill", &json!({"ids": [marks[0].0], "gradient": {"kind": "freeform", "freeform": {"points": points}}})).unwrap(); - let fill = fill_of(&s, marks[0]); - assert_eq!(spec(&s, id).series_paints[0].fill, Some(fill.clone())); s.execute("paint.freeform.setPoint", &json!({"ids": [marks[0].0], "index": 1, "color": "#00ff00"})).unwrap(); let edited = fill_of(&s, marks[0]); - assert_ne!(edited, fill, "the command recolors the point"); - assert_eq!(spec(&s, id).series_paints[0].fill, Some(edited.clone())); + let Paint::Gradient(g) = &edited else { panic!("{edited:?}") }; + assert_eq!(g.freeform.as_ref().unwrap().points[1].color.to_hex(), "#00ff00"); + let mark = s.doc().unwrap().doc.node(marks[0]).unwrap().clone(); + let place = gradient_place(&edited, paint_box_of(&mark, false)); + let kept = spec(&s, id).series_paints[0].clone(); + assert!(kept.fill_unit_box); + assert_eq!(gradient_place(kept.fill.as_ref().unwrap(), vectorcraft_doc::GraphicStyle::UNIT_BOX), place); s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap(); - assert!(marks_of(&s, id, 0).iter().all(|m| m.0 == edited), "{:?}", marks_of(&s, id, 0)); + series_0_gradients_at(&s, id, false, &place); +} + +/// A gradient a graph kept in document coordinates (a file saved before gradients were kept relative to the unit box) is +/// drawn on every mark as it was kept. +#[test] +fn a_gradient_kept_in_document_coordinates_is_drawn_as_kept() { + let (mut s, id, _, marks) = column_marks(); + s.execute("paint.setFill", &json!({"ids": [marks[0].0], "gradient": {"kind": "linear"}})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [marks[0].0], "start": [0, 0], "end": [10, 40]})).unwrap(); + let placed = fill_of(&s, marks[0]); + s.edit("Kept in document coordinates", |d, _| { + let mut extras = d.node_mut(id).unwrap().extras_mut(); + let slot = &mut extras.graph.as_mut().unwrap().series_paints[0]; + (slot.fill, slot.fill_unit_box) = (Some(placed.clone()), false); + Ok(()) + }) + .unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap(); + assert!(marks_of(&s, id, 0).iter().all(|m| m.0 == placed), "{:?}", marks_of(&s, id, 0)); + // The same gradient kept for the fill relative to the unit box leaves the stroke kept in document coordinates as it is. + s.edit("Kept fill and stroke", |d, _| { + let mut extras = d.node_mut(id).unwrap().extras_mut(); + let slot = &mut extras.graph.as_mut().unwrap().series_paints[0]; + (slot.fill, slot.fill_unit_box) = (Some(placed.clone()), true); + (slot.stroke, slot.stroke_unit_box, slot.stroke_width) = (Some(placed.clone()), false, Some(2.0)); + Ok(()) + }) + .unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[2, 2], [3, 4]]})).unwrap(); + let drawn = marks_of(&s, id, 0); + assert!(drawn.iter().all(|m| m.1 == placed), "{drawn:?}"); + series_0_gradients_at(&s, id, false, &gradient_place(&placed, vectorcraft_doc::GraphicStyle::UNIT_BOX)); +} + +/// A gradient placed on a series path with no height (a flat line's fill) is kept in document coordinates, and every +/// path of the series that takes the kept fill has it at that place in the document. +#[test] +fn a_gradient_placed_on_a_series_path_with_no_height_is_kept_in_document_coordinates() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 800, "height": 800})).unwrap(); + let id = graph(&mut s, "line"); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[3, 2], [3, 4], [3, 6]]})).unwrap(); + let flat = |s: &Session| { + let parts = series_parts(s, id, 0); + parts.into_iter().find(|m| m.appearance.fill_paint().is_none() && m.geometric_bounds().is_some_and(|b| b.height() < 1e-9)).unwrap().id + }; + let line = flat(&s); + s.execute("paint.setFill", &json!({"ids": [line.0], "gradient": {"kind": "linear"}})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [line.0], "start": [150, 120], "end": [300, 280]})).unwrap(); + let placed = fill_of(&s, line); + let kept = spec(&s, id).series_paints[0].clone(); + assert!(!kept.fill_unit_box); + assert_eq!(kept.fill, Some(placed.clone())); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[3, 1], [3, 5], [3, 2]]})).unwrap(); + let fills: Vec = series_parts(&s, id, 0).iter().map(|m| m.appearance.fill_paint()).filter(|p| matches!(p, Paint::Gradient(_))).collect(); + assert!(fills.len() >= 2 && fills.iter().all(|p| *p == placed), "{fills:?}"); +} + +/// Writing a series mark's stroke weight as it is, after the graph drew the mark's stroke from a gradient the series +/// keeps relative to the unit box, leaves the kept gradient, the document and its undo history as they are, on every mark. +#[test] +fn rewriting_a_series_marks_drawn_gradient_stroke_records_no_undo_step() { + let (mut s, id, _, marks) = column_marks(); + s.execute("paint.setStroke", &json!({"ids": [marks[1].0], "gradient": {"kind": "radial"}})).unwrap(); + s.execute("stroke.set", &json!({"ids": [marks[1].0], "weight": 4})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [marks[1].0], "stroke": true, "start": [150, 200], "end": [170, 260]})).unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[1.3, 2], [3.7, 4], [6.1, 1]]})).unwrap(); + let was = s.doc().unwrap().doc.clone(); + let undo = s.doc().unwrap().history.undo.len(); + let drawn: Vec<_> = series_parts(&s, id, 0).into_iter().filter(|m| matches!(m.appearance.stroke_paint(), Paint::Gradient(_))).collect(); + assert!(drawn.len() >= 4, "{}", drawn.len()); + for mark in drawn { + let row = top_row(&s, mark.id, false); + s.execute("appearance.setItem", &json!({"ids": [mark.id.0], "index": row, "weight": mark.appearance.stroke_width()})).unwrap(); + assert!(s.doc().unwrap().doc == was, "writing mark {:?} changed the document", mark.id); + assert_eq!(s.doc().unwrap().history.undo.len(), undo); + } + // A mark moved with its gradient shows the kept gradient on its moved box, up to rounding. + let kept = spec(&s, id).series_paints[0].clone(); + for (i, mark) in series_parts(&s, id, 0).into_iter().filter(|m| matches!(m.appearance.stroke_paint(), Paint::Gradient(_))).enumerate() { + s.execute("select.set", &json!({"ids": [mark.id.0]})).unwrap(); + s.execute("object.move", &json!({"dx": 13.37 + i as f64 * 0.731, "dy": -7.19})).unwrap(); + let moved = s.doc().unwrap().doc.node(mark.id).unwrap().geometric_bounds().unwrap(); + assert!((moved.x0 - mark.geometric_bounds().unwrap().x0 - 13.37 - i as f64 * 0.731).abs() < 1e-9, "mark {:?} did not move", mark.id); + let undo = s.doc().unwrap().history.undo.len(); + let row = top_row(&s, mark.id, false); + s.execute("appearance.setItem", &json!({"ids": [mark.id.0], "index": row, "weight": mark.appearance.stroke_width()})).unwrap(); + assert_eq!(spec(&s, id).series_paints[0], kept, "writing moved mark {:?} changed the kept stroke", mark.id); + assert_eq!(s.doc().unwrap().history.undo.len(), undo); + } +} + +/// Writing the fill of a series mark with no height (a column for a value of 0) as it is, after the graph drew the mark's +/// fill from a gradient the series keeps relative to the unit box, leaves the kept gradient and its unit-box flag as they +/// are. +#[test] +fn rewriting_a_drawn_gradient_fill_on_a_series_mark_with_no_height_keeps_the_kept_gradient() { + let (mut s, id, _, marks) = column_marks(); + s.execute("paint.setFill", &json!({"ids": [marks[0].0], "gradient": {"kind": "linear"}})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [marks[0].0], "start": [0, 0], "end": [10, 40]})).unwrap(); + let kept = spec(&s, id).series_paints[0].clone(); + assert!(kept.fill_unit_box); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[0, 2], [3, 4], [6, 1]]})).unwrap(); + let flat = series_parts(&s, id, 0) + .into_iter() + .find(|m| matches!(m.appearance.fill_paint(), Paint::Gradient(_)) && m.geometric_bounds().is_some_and(|b| b.height() < 1e-9)) + .unwrap() + .id; + let changed = s.edit("Write the fill as it is", |d, _| Ok(super::capture_series_top_paints(d, &[(flat, true)]))).unwrap(); + assert!(!changed); + assert_eq!(spec(&s, id).series_paints[0], kept); +} + +/// Writing a series mark's stroke weight as it is, after the graph drew the mark's stroke from a gradient the series keeps +/// in document coordinates (a file saved before the unit box), leaves the kept gradient and its unit-box flag as they are +/// and records no undo step. +#[test] +fn rewriting_a_series_marks_stroke_drawn_from_a_gradient_kept_in_document_coordinates_keeps_it() { + let (mut s, id, _, marks) = column_marks(); + s.execute("paint.setStroke", &json!({"ids": [marks[1].0], "gradient": {"kind": "radial"}})).unwrap(); + s.execute("stroke.set", &json!({"ids": [marks[1].0], "weight": 2})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [marks[1].0], "stroke": true, "start": [150, 200], "end": [170, 260]})).unwrap(); + let placed = s.doc().unwrap().doc.node(marks[1]).unwrap().appearance.stroke_paint(); + s.edit("Kept in document coordinates", |d, _| { + let mut extras = d.node_mut(id).unwrap().extras_mut(); + let slot = &mut extras.graph.as_mut().unwrap().series_paints[0]; + (slot.stroke, slot.stroke_unit_box, slot.stroke_width) = (Some(placed.clone()), false, Some(2.0)); + Ok(()) + }) + .unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4], [6, 1]]})).unwrap(); + let kept = spec(&s, id).series_paints[0].clone(); + let undo = s.doc().unwrap().history.undo.len(); + let drawn: Vec<_> = series_parts(&s, id, 0).into_iter().filter(|m| m.appearance.stroke_paint() == placed).collect(); + assert!(drawn.len() >= 3, "{}", drawn.len()); + for mark in drawn { + let row = top_row(&s, mark.id, false); + s.execute("appearance.setItem", &json!({"ids": [mark.id.0], "index": row, "weight": mark.appearance.stroke_width()})).unwrap(); + assert_eq!(spec(&s, id).series_paints[0], kept, "writing mark {:?} changed the kept stroke", mark.id); + assert_eq!(s.doc().unwrap().history.undo.len(), undo); + } +} + +/// A gradient placed on a line series' stroke is kept relative to the line's stroke box, and with Draw Filled Lines the +/// band, which takes the kept stroke as its fill, has it at the same place on its own box. +#[test] +fn a_gradient_kept_for_a_lines_stroke_takes_the_same_place_on_its_filled_line() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 800, "height": 800})).unwrap(); + let id = graph(&mut s, "line"); + let line = series_parts(&s, id, 0).into_iter().find(|m| m.appearance.fill_paint().is_none()).unwrap().id; + s.execute("paint.setStroke", &json!({"ids": [line.0], "gradient": {"kind": "linear"}})).unwrap(); + s.execute("paint.setGradientGeom", &json!({"ids": [line.0], "stroke": true, "start": [150, 120], "end": [300, 280]})).unwrap(); + let kept = spec(&s, id).series_paints[0].clone(); + assert!(kept.stroke_unit_box && kept.fill.is_none()); + let place = gradient_place(kept.stroke.as_ref().unwrap(), vectorcraft_doc::GraphicStyle::UNIT_BOX); + s.execute("graph.setType", &json!({"id": id.0, "filledLines": true})).unwrap(); + let band = series_parts(&s, id, 0).into_iter().find(|m| matches!(m.appearance.fill_paint(), Paint::Gradient(_))).unwrap(); + let at = gradient_place(&band.appearance.fill_paint(), band.geometric_bounds().unwrap()); + assert!(at.iter().zip(&place).all(|(a, b)| (a - b).abs() < 1e-9), "{at:?} is not {place:?}"); +} + +/// A series' kept unit-box flags are read from and written to a file, and a file without them reads as document +/// coordinates. +#[test] +fn a_series_paints_unit_box_flags_round_trip() { + let kept = vectorcraft_doc::SeriesPaint { fill_unit_box: true, stroke_unit_box: true, ..Default::default() }; + let text = serde_json::to_string(&kept).unwrap(); + assert_eq!(text, r#"{"fillUnitBox":true,"strokeUnitBox":true}"#); + assert_eq!(serde_json::from_str::(&text).unwrap(), kept); + let old: vectorcraft_doc::SeriesPaint = serde_json::from_str("{}").unwrap(); + assert!(!old.fill_unit_box && !old.stroke_unit_box); } /// A series mark's fill recolored with Invert or Complement (the proxy, or one appearance row), an Edit Colors command, diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index ae8a7ef6b2..ced9347bc1 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -21,7 +21,7 @@ use vectorcraft_color::{Color, GradientPaint, Paint}; use vectorcraft_doc::{Appearance, Node, NodeId, TextStyleDef}; use super::clipboard::SwatchChoices; -use super::{CommandSpec, EngineError, Result, Session, always, bad, cmd, color_value, has_doc, ids_param, index, ok, point_param, str_param}; +use super::{CommandSpec, EngineError, Result, Session, always, bad, cmd, color_value, has_doc, ids_param, index_opt, ok, point_param, str_param}; use crate::Clipboard; /// The extension of a library file. @@ -1649,11 +1649,11 @@ fn move_group(s: &mut Session, p: &Value) -> Result { const C: &str = "library.moveGroup"; let id = library_id(s, p, C)?; let group = group_param(p, C)?; - let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(C, "missing `to` (an index)"))?; + let to = index_opt(p, "to", C)?.ok_or_else(|| bad(C, "missing `to` (an index)"))?; let (name, at) = s.libraries.change(&id, |lib| { let i = group_at(lib, group, C)?; let g = lib.groups.remove(i); - let at = index(to).min(lib.groups.len()); + let at = to.min(lib.groups.len()); let name = g.name.clone(); lib.groups.insert(at, g); Ok((name, at)) diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index d879a546ab..2383d05a3a 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -88,7 +88,7 @@ pub fn specs() -> Vec { fn artboard_reorder(s: &mut Session, p: &Value) -> Result { const C: &str = "artboard.reorder"; let i = artboard_index(&s.doc()?.doc, p, C)?; - let to = p.get("to").and_then(Value::as_u64).map(index).ok_or_else(|| bad(C, "missing to"))?; + let to = super::index_opt(p, "to", C)?.ok_or_else(|| bad(C, "missing to"))?; s.edit("Reorder Artboards", |d, _| { if i >= d.artboards.len() { return Err(EngineError::Other("no such artboard".into())); diff --git a/crates/engine/src/cmd/path.rs b/crates/engine/src/cmd/path.rs index 33bde936bf..9d7b42cca2 100644 --- a/crates/engine/src/cmd/path.rs +++ b/crates/engine/src/cmd/path.rs @@ -12,7 +12,7 @@ use super::draw2::usize_opt; use super::pathops::Beside; use super::{ CommandSpec, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, f64_req, finite_numbers, has_anchors, has_doc, has_selected_path, - has_selection, ids_param, index, number_or, object_param, ok, str_param, switch_or, targets, + has_selection, ids_param, index_opt, number_or, object_param, ok, str_param, switch_or, targets, }; use crate::EngineError; @@ -170,7 +170,7 @@ pub(super) fn path_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Result< fn append_anchor(s: &mut Session, p: &Value) -> Result { let id = object_param(p, "id", "path.appendAnchor")?.ok_or_else(|| bad("path.appendAnchor", "missing id"))?; let a = anchor_from_json(p, "path.appendAnchor")?; - let subpath = p.get("subpath").and_then(Value::as_u64).map(|u| u as usize); + let subpath = index_opt(p, "subpath", "path.appendAnchor")?; s.edit("Pen", |d, sel| { let path = path_mut(d, id)?; let si = match subpath { @@ -208,7 +208,7 @@ fn append_anchor(s: &mut Session, p: &Value) -> Result { /// `at` first moves the anchor, its handles with it (the Pen's closing drag with Space held). fn close(s: &mut Session, p: &Value) -> Result { let id = object_param(p, "id", "path.close")?.ok_or_else(|| bad("path.close", "missing id"))?; - let subpath = p.get("subpath").and_then(Value::as_u64).map(|u| u as usize); + let subpath = index_opt(p, "subpath", "path.close")?; let handle = |key: &str| match p.get(key).filter(|h| !h.is_null()) { None => Ok(None), Some(h) => finite_numbers(h).map(|[x, y]| Some(Point::new(x, y))).ok_or_else(|| bad("path.close", format!("`{key}` must be [x, y]"))), @@ -310,7 +310,7 @@ pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_ge fn set_handle(s: &mut Session, p: &Value) -> Result { let id = object_param(p, "id", "path.setHandle")?.ok_or_else(|| bad("path.setHandle", "missing id"))?; let si = usize_opt(p, "subpath", "path.setHandle")?.unwrap_or(0); - let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.setHandle", "missing anchor"))?; + let ai = index_opt(p, "anchor", "path.setHandle")?.ok_or_else(|| bad("path.setHandle", "missing anchor"))?; let out = str_param(p, "which") != Some("in"); let pos = Point::new(f64_req(p, "x", "path.setHandle")?, f64_req(p, "y", "path.setHandle")?); let independent = switch_or(p, "independent", false, "path.setHandle")?; @@ -337,7 +337,7 @@ fn set_handle(s: &mut Session, p: &Value) -> Result { fn move_anchor(s: &mut Session, p: &Value) -> Result { let id = object_param(p, "id", "path.moveAnchor")?.ok_or_else(|| bad("path.moveAnchor", "missing id"))?; let si = usize_opt(p, "subpath", "path.moveAnchor")?.unwrap_or(0); - let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.moveAnchor", "missing anchor"))?; + let ai = index_opt(p, "anchor", "path.moveAnchor")?.ok_or_else(|| bad("path.moveAnchor", "missing anchor"))?; let pos = Point::new(f64_req(p, "x", "path.moveAnchor")?, f64_req(p, "y", "path.moveAnchor")?); // An anchor already at (x, y) changes nothing and records no undo step. if let Some(a) = anchor_of(s, id, si, ai)? @@ -453,7 +453,7 @@ pub(super) fn paths_in(d: &vectorcraft_doc::Document, ids: &[NodeId]) -> Vec Result { const C: &str = "path.reverse"; let want = p.get("reversed").map(|v| v.as_bool().ok_or_else(|| bad(C, "`reversed` must be true or false"))).transpose()?; - let subpath = p.get("subpath").and_then(Value::as_u64).map(|u| u as usize); + let subpath = index_opt(p, "subpath", C)?; let ids = paths_in(&s.doc()?.doc, &targets(s, p)?); if ids.is_empty() { return Err(bad(C, "select paths")); @@ -1784,7 +1784,7 @@ fn reshape(s: &mut Session, p: &Value) -> Result { fn insert_anchor(s: &mut Session, p: &Value) -> Result { let id = object_param(p, "id", "path.insertAnchor")?.ok_or_else(|| bad("path.insertAnchor", "missing id"))?; let si = usize_opt(p, "subpath", "path.insertAnchor")?.unwrap_or(0); - let seg = p.get("segment").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.insertAnchor", "missing segment"))?; + let seg = index_opt(p, "segment", "path.insertAnchor")?.ok_or_else(|| bad("path.insertAnchor", "missing segment"))?; let t = number_or(p, "t", 0.5, "path.insertAnchor")?.clamp(0.0, 1.0); let idx = s.edit("Add Anchor Point", |d, sel| { let path = path_mut(d, id)?; diff --git a/crates/engine/src/cmd/scripts.rs b/crates/engine/src/cmd/scripts.rs index f5edf1f91d..385fd071c4 100644 --- a/crates/engine/src/cmd/scripts.rs +++ b/crates/engine/src/cmd/scripts.rs @@ -32,7 +32,7 @@ pub fn specs() -> Vec { "Run Script", [], None, - "{path?: a .js or .jsx file | source?: the script's text (exactly one of the two), name?: the name errors and the report use (default: the script's #script line, else the path's file name, or \"script\"), args?: {…} (at most 1 MiB as JSON; vc.args in the script), answers?: [{kind: alert|confirm|prompt, value: \"ok\" (alert) | true or false (confirm) | text or null (prompt)}] (for a run with nobody to ask: the answers to its dialogs, in order; at most 10000 and 1 MiB as JSON; refused in the app, where the person answers; an empty object is an empty list), maxFuel?: whole number from 1 to 100000000000 (instructions in all; default 1000000000)} run a JavaScript file against the open documents, through the vc object: vc.run(id, params) runs a command and returns its result, a failed command throws an error the script can catch and puts back the documents and the session state it changed, while any file it wrote stays written and is named; vc.document is a snapshot of the active document or null; vc.commands lists the commands a script may run; vc.log(value) adds to the report and the Script Console (script.console); $.write(...values) writes the values to the Script Console as String(value) gives them, and $.writeln(...values) does the same and ends the line; $.evalFile(path, timeout?) runs a file with the same globals and returns its last value (timeout, in milliseconds from 0, is taken and not waited on), $.fileName is the file being run (empty for text), $.includePath the #includepath list and $.engineName the engine's name (VectorCraft without #targetengine); alert(message[, title, errorIcon]), confirm(message[, noAsDflt, title]) and prompt(message, preset[, title]) open a dialog with a Stop button (default title \"Script Alert\"; message and preset as String(value) gives them; errorIcon and noAsDflt as Boolean(value) gives them; No is the default with noAsDflt): alert returns undefined, confirm true for Yes and false for No, prompt the field's text for OK and null for Cancel; in the app the run waits at the call for the person's answer and Stop ends it as script.stop does, a dialog cannot open inside a callback of a built-in, a getter, a conversion or a file $.evalFile runs and throws there (an #include file's code is the script's own and can), and a run with nobody to ask takes the next of answers, and ends when none is left or the next is for another kind of dialog, or when the script ends with answers unused; vc.args is args. Before anything runs, the script's preprocessor lines (# or //@ at the start of a line, outside strings and comments) are read: #include \"file\" and //@include put that file's text in place of the line, looked for beside the file that includes it, then in each #includepath folder in turn (separated by ;, relative to that file's folder; a later #includepath replaces the list), then in each JSINCLUDE folder; #script names the run; #target takes any value and changes nothing; #strict on makes an assignment to a read-only or getter-only property, or a new property on an object that is not extensible (preventExtensions, seal, freeze, vc, $), throw a TypeError, without the rest of ES5 strict mode, and #strict off turns that off (the last #strict line of the script and its includes sets the run; off without one; a $.evalFile file's own line holds for its code); #targetengine name runs the script in the named engine, whose globals and functions stay for the next run that names it until script.engine.drop drops it or the session ends (script.engines lists them; a name of letters, digits and _ not starting with a digit, or a quoted one of 1 to 64 bytes without control characters, matched as written; only in the script itself, once, before its code and #include lines; main, and a script without the line, get a new engine each run; a session keeps 8 named engines, a ninth name is refused, each has 64 MiB of heap, and an uncaught error, a limit or script.stop keeps the globals the run set while the documents go back; host functions kept in a global act for the run that calls them; an error in code an earlier run compiled is at that run's file, line and column, within 8 MiB of source maps kept for each engine, the oldest going first; not on the web); #engine is refused as unsupported, any other #name as unknown, and another //@name is a comment. A script run from a file reads its includes and $.evalFile's files only in that file's folder and the folders below it, refusing a path with a .. name or a link that leads out (a JSINCLUDE folder outside is passed over); a script given as text reads none. File(path) and Folder(path) name files and folders and read them in the same folders, and inside the automation read roots when they are set: exists, length, created, modified, getFiles(mask or function; at most 2048 entries), resolve, and open('r') with read, readln, readch, seek, tell and close; a path outside, a network or device path and a link that leads out give false or null with error \"Script file access denied\", a file is read whole at most 8 MiB (checked from its size first) and a run reads at most 32 MiB, and writing, the file dialogs and execute return false or null with error set. An error in another file is at that file's line and column; vc.api.version and vc.api.has(feature) describe the API. The run is one transaction: on success each changed document gets one undo step, Run Script, and each preference it changed keeps its value, and on an uncaught error, a limit or script.stop every open document and the session go back as they were; files a command wrote before that stay written and are named. While it runs, other commands that change anything end with a busy error. Commands that change anything outside the documents (script.*, except script.check, plugin.*, prefs.*, actionSet.*, shortcutSet.*, help.*, print.*, library.*, importMatches.*, edit.undoGroup.*, file.recovery.*, stroke.widthProfile.*, saving, importing or deleting a preset, text.addFontFiles, text.findFontFiles, text.rescanFonts, links.findFiles, file.place.queue, color.loadProfile, color.removeProfile, file.print, view.proofSetup, view.proofColors, view.overprintPreview, view.separationsPreview) throw. VectorCraft's own limits: 8 MiB of source a file and 32 MiB in all, 64 included or evaluated files 16 deep, 1024 paths looked up for them, 256 MiB of heap (128 MiB on the web), 2000000 host calls, 200000000 instructions without a pause, 64 levels, 16 MiB and 1048576 entries for a value crossing to or from a command, 10000 log entries and 4 MiB of log, 16 KiB of a dialog's message, 256 bytes of its title and 16 KiB of a prompt's preset (past these a RangeError), and 100000 objects and 512 MiB of new document data, past which a person is asked to continue or stop and a run with nobody to ask ends → {ok: true, name, changed: [document names], written: [paths], log: [values], fuelUsed, hostCalls, runTimeMs (the time the script ran, its commands included, waits between frames and for a person's answer left out; 0 without a clock), timingAvailable (false on the web), hostCallCounts: {vcRun, documentReads, commandListReads, vcLog, consoleWrites, fileCalls, fileExecutes}, slowestCommands: [{id, calls, totalMs, maxMs}] (at most 5, the longest in all first, then by id), console: {lines: [text], droppedLines, truncatedLines} (the run's own output, vc.log lines and error), dialogs: [{kind, message, title, errorIcon (alert), noAsDefault (confirm), preset (prompt), answer: \"ok\" | true | false | text | null | \"stop\" | \"missing\"}] (in the order they opened, at most 10000 and 1 MiB), dialogCount, dialogsTruncated}, or, in the app, {running: true, name} and the report later in ui.inspect's scriptReport; a run that fails ends with its error, and its report there also has kind, message, line, column, file (when the error is in another file than the script, or in code an earlier run of its engine compiled), fuelLeft, fuelUsed, hostCalls and the fields above", + "{path?: a .js or .jsx file | source?: the script's text (exactly one of the two), name?: the name errors and the report use (default: the script's #script line, else the path's file name, or \"script\"), args?: {…} (at most 1 MiB as JSON; vc.args in the script), answers?: [{kind: alert|confirm|prompt, value: \"ok\" (alert) | true or false (confirm) | text or null (prompt)}] (for a run with nobody to ask: the answers to its dialogs, in order; at most 10000 and 1 MiB as JSON; refused in the app, where the person answers; an empty object is an empty list), maxFuel?: whole number from 1 to 100000000000 (instructions in all; default 1000000000)} run a JavaScript file against the open documents, through the vc object: vc.run(id, params) runs a command and returns its result, a failed command throws an error the script can catch and puts back the documents and the session state it changed, while any file it wrote stays written and is named; vc.document is a snapshot of the active document or null; vc.commands lists the commands a script may run; vc.log(value) adds to the report and the Script Console (script.console); $.write(...values) writes the values to the Script Console as String(value) gives them, and $.writeln(...values) does the same and ends the line; $.evalFile(path, timeout?) runs a file with the same globals and returns its last value (timeout, in milliseconds from 0, is taken and not waited on), $.fileName is the file of the code that reads it (the file being run for code that eval or Function compiled; empty for code compiled from a script given as text), $.includePath the #includepath list and $.engineName the engine's name (VectorCraft without #targetengine); alert(message[, title, errorIcon]), confirm(message[, noAsDflt, title]) and prompt(message, preset[, title]) open a dialog with a Stop button (default title \"Script Alert\"; message and preset as String(value) gives them; errorIcon and noAsDflt as Boolean(value) gives them; No is the default with noAsDflt): alert returns undefined, confirm true for Yes and false for No, prompt the field's text for OK and null for Cancel; in the app the run waits at the call for the person's answer and Stop ends it as script.stop does, a dialog cannot open inside a callback of a built-in, a getter, a conversion or a file $.evalFile runs and throws there (an #include file's code is the script's own and can), and a run with nobody to ask takes the next of answers, and ends when none is left or the next is for another kind of dialog, or when the script ends with answers unused; vc.args is args. Before anything runs, the script's preprocessor lines (# or //@ at the start of a line, outside strings and comments) are read: #include \"file\" and //@include put that file's text in place of the line, looked for beside the file that includes it, then in each #includepath folder in turn (separated by ;, relative to that file's folder; a later #includepath replaces the list), then in each JSINCLUDE folder; #script names the run; #target takes any value and changes nothing; #strict on makes an assignment to a read-only or getter-only property, or a new property on an object that is not extensible (preventExtensions, seal, freeze, vc, $), throw a TypeError, without the rest of ES5 strict mode, and #strict off turns that off (the last #strict line of the script and its includes sets the run; off without one; a $.evalFile file's own line holds for its code); #targetengine name runs the script in the named engine, whose globals and functions stay for the next run that names it until script.engine.drop drops it or the session ends (script.engines lists them; a name of letters, digits and _ not starting with a digit, or a quoted one of 1 to 64 bytes without control characters, matched as written; only in the script itself, once, before its code and #include lines; main, and a script without the line, get a new engine each run; a session keeps 8 named engines, a ninth name is refused, each has 64 MiB of heap, and an uncaught error, a limit or script.stop keeps the globals the run set while the documents go back; host functions kept in a global act for the run that calls them; an error in code an earlier run compiled is at that run's file, line and column, within 8 MiB of source maps kept for each engine, the oldest going first; not on the web); #engine is refused as unsupported, any other #name as unknown, and another //@name is a comment. A script run from a file reads its includes and $.evalFile's files only in that file's folder and the folders below it, refusing a path with a .. name or a link that leads out (a JSINCLUDE folder outside is passed over); a script given as text reads none. File(path) and Folder(path) name files and folders and read them in the same folders, and inside the automation read roots when they are set: exists, length, created, modified, getFiles(mask or function; at most 2048 entries), resolve, and open('r') with read, readln, readch, seek, tell and close; a path outside, a network or device path and a link that leads out give false or null with error \"Script file access denied\", a file is read whole at most 8 MiB (checked from its size first) and a run reads at most 32 MiB, and writing, the file dialogs and execute return false or null with error set. An error in another file is at that file's line and column; vc.api.version and vc.api.has(feature) describe the API. The run is one transaction: on success each changed document gets one undo step, Run Script, and each preference it changed keeps its value, and on an uncaught error, a limit or script.stop every open document and the session go back as they were; files a command wrote before that stay written and are named. While it runs, other commands that change anything end with a busy error. Commands that change anything outside the documents (script.*, except script.check, plugin.*, prefs.*, actionSet.*, shortcutSet.*, help.*, print.*, library.*, importMatches.*, edit.undoGroup.*, file.recovery.*, stroke.widthProfile.*, saving, importing or deleting a preset, text.addFontFiles, text.findFontFiles, text.rescanFonts, links.findFiles, file.place.queue, color.loadProfile, color.removeProfile, file.print, view.proofSetup, view.proofColors, view.overprintPreview, view.separationsPreview) throw. VectorCraft's own limits: 8 MiB of source a file and 32 MiB in all, 64 included or evaluated files 16 deep, 1024 paths looked up for them, 256 MiB of heap (128 MiB on the web), 2000000 host calls, 200000000 instructions without a pause, 64 levels, 16 MiB and 1048576 entries for a value crossing to or from a command, 10000 log entries and 4 MiB of log, 16 KiB of a dialog's message, 256 bytes of its title and 16 KiB of a prompt's preset (past these a RangeError), and 100000 objects and 512 MiB of new document data, past which a person is asked to continue or stop and a run with nobody to ask ends → {ok: true, name, changed: [document names], written: [paths], log: [values], fuelUsed, hostCalls, runTimeMs (the time the script ran, its commands included, waits between frames and for a person's answer left out; 0 without a clock), timingAvailable (false on the web), hostCallCounts: {vcRun, documentReads, commandListReads, vcLog, consoleWrites, fileCalls, fileExecutes}, slowestCommands: [{id, calls, totalMs, maxMs}] (at most 5, the longest in all first, then by id), console: {lines: [text], droppedLines, truncatedLines} (the run's own output, vc.log lines and error), dialogs: [{kind, message, title, errorIcon (alert), noAsDefault (confirm), preset (prompt), answer: \"ok\" | true | false | text | null | \"stop\" | \"missing\"}] (in the order they opened, at most 10000 and 1 MiB), dialogCount, dialogsTruncated}, or, in the app, {running: true, name} and the report later in ui.inspect's scriptReport; a run that fails ends with its error, and its report there also has kind, message, line, column, file (when the error is in another file than the script, or in code an earlier run of its engine compiled), fuelLeft, fuelUsed, hostCalls and the fields above", always, run ), diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 370664f795..bb45bf0fca 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -21,7 +21,7 @@ use vectorcraft_color::Paint; use vectorcraft_doc::{Appearance, DEFAULT_GRAPHIC_STYLE, Document, GraphicStyle, Node, NodeId, NodeKind, Shared}; use vectorcraft_geom::Rect; -use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, index, object_param, ok, str_param, unique_name}; +use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, index_opt, object_param, ok, str_param, unique_name}; use crate::EngineError; pub fn specs() -> Vec { @@ -585,10 +585,10 @@ fn style_merge(s: &mut Session, p: &Value) -> Result { fn style_move(s: &mut Session, p: &Value) -> Result { let cmd = "graphicStyle.move"; let name = str_param(p, "name").ok_or_else(|| bad(cmd, "missing name"))?; - let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing `to` (an index)"))?; + let to = index_opt(p, "to", cmd)?.ok_or_else(|| bad(cmd, "missing `to` (an index)"))?; let d = &s.doc()?.doc; let from = style_index(d, name)?; - let to = index(to).min(d.graphic_styles.len() - 1); + let to = to.min(d.graphic_styles.len() - 1); if from == to { return ok(); } diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index 6b14c98916..b3fab4a2bc 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -12,8 +12,8 @@ use vectorcraft_geom::{Affine, PathData, Vec2}; use super::edit::selected_roots; use super::pathops::{length_given, number_given, shape_node}; use super::{ - CommandSpec, EngineError, Result, Session, bad, check_numbers, cmd, color_value, f64_req, has_doc, has_selection, index, number_or, object_param, - ok, str_param, switch_or, targets, text_param, + CommandSpec, EngineError, Result, Session, bad, check_numbers, cmd, color_value, f64_req, has_doc, has_selection, index, index_opt, number_or, + object_param, ok, str_param, switch_or, targets, text_param, }; pub fn specs() -> Vec { @@ -1376,7 +1376,7 @@ fn set_path_handle(s: &mut Session, p: &Value) -> Result { const C: &str = "text.setPathHandle"; let id = object_param(p, "id", C)?.ok_or_else(|| bad(C, "missing id"))?; let si = super::draw2::usize_opt(p, "subpath", C)?.unwrap_or(0); - let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad(C, "missing anchor"))?; + let ai = index_opt(p, "anchor", C)?.ok_or_else(|| bad(C, "missing anchor"))?; let out = super::object::choice(p, C, "which", &["in", "out"])? != Some("in"); let pos = vectorcraft_geom::Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?); let independent = switch_or(p, "independent", false, C)?; diff --git a/crates/engine/src/cmd/typemenu.rs b/crates/engine/src/cmd/typemenu.rs index 87c16bb9b9..c140ba6572 100644 --- a/crates/engine/src/cmd/typemenu.rs +++ b/crates/engine/src/cmd/typemenu.rs @@ -138,7 +138,7 @@ pub fn specs() -> Vec { "Paste without Formatting", ["Edit"], Some("Cmd+Alt+V"), - "{center?, dx?, dy?, swatchConflict?} paste as edit.paste; pasted text takes the default character style (one run) and leaves its character and paragraph styles behind → {ids, added, merged, renamed}", + "{center?, dx?, dy?, swatchConflict?} paste as edit.paste; pasted text takes the default character style (one run) and leaves its character and paragraph styles behind, as do the labels of pasted graphs → {ids, added, merged, renamed}", has_clipboard, paste_plain ), @@ -1460,6 +1460,16 @@ fn strip_formatting(n: &mut vectorcraft_doc::Node) { t.set_all_paras(Default::default()); refresh_bounds(t); } + // A graph keeps the names of its labels' character and paragraph styles for each redraw. + if let Some(g) = n.extras_if_any().and_then(|e| e.graph.as_deref_mut()) { + let l = &mut g.label_styles; + for st in [&mut l.values, &mut l.categories, &mut l.legend].into_iter().flatten() { + st.style_name = None; + } + for pa in [&mut l.values_para, &mut l.categories_para, &mut l.legend_para].into_iter().flatten() { + pa.style_name = None; + } + } if let Some(ch) = n.children_mut() { for c in ch.iter_mut() { strip_formatting(std::sync::Arc::make_mut(c)); diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index 1edeb84e93..1b95b5fe2a 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -26,7 +26,8 @@ //! Before a run starts, its preprocessor lines are read ([`include`]): included files take the //! place of their lines, and an error in one is reported at its own file, line and column. //! `$.evalFile(path)` runs another file in the same global environment and gives its last value, -//! `$.fileName` is the file being run and `$.includePath` the `#includepath` list. +//! `$.fileName` is the file of the code that reads it (the file being run for code that `eval` or +//! `Function` compiled) and `$.includePath` the `#includepath` list. //! //! A run writes to the Script Console with `$.write` and `$.writeln`, and `vc.log` adds a line //! there too. The session's console gets a start line, the run's output, its uncaught error, an end diff --git a/crates/engine/src/tests/clipboard.rs b/crates/engine/src/tests/clipboard.rs index d0e003ec49..6cbfd909a9 100644 --- a/crates/engine/src/tests/clipboard.rs +++ b/crates/engine/src/tests/clipboard.rs @@ -468,6 +468,72 @@ fn styles_symbols_and_images_of_the_same_name_are_renamed() { assert_eq!(key(&s, img2), src_key); } +/// A graph whose series fill, value labels and legend rows paint with global swatch "Brand" and +/// whose value labels are set in character style "Emphasis" and paragraph style "Body", as a +/// later redraw would give them to its marks and labels. +fn styled_graph(s: &mut Session) -> NodeId { + s.execute("swatch.new", &json!({"name": "Brand", "color": "#ff0000", "global": true})).unwrap(); + s.execute("charStyle.new", &json!({"name": "Emphasis", "attrs": {"size": 24.0}})).unwrap(); + s.execute("paraStyle.new", &json!({"name": "Body", "attrs": {"space_after": 8.0}})).unwrap(); + let v = s.execute("graph.create", &json!({"type": "column", "x": 200, "y": 200, "width": 60, "height": 40, "csv": ",2024\nQ1,3\nQ2,5"})); + let id = id_of(&v.unwrap()); + let brand = linked(hex("#ff0000"), "Brand", 1.0); + let mut extras = doc_mut(s).node_mut(id).unwrap().extras_mut(); + let g = extras.graph.as_mut().unwrap(); + g.series_paints = vec![vectorcraft_doc::SeriesPaint { fill: Some(brand.clone()), ..Default::default() }]; + let l = &mut g.label_styles; + l.values = Some(vectorcraft_doc::CharStyle { fill: brand.clone(), style_name: Some("Emphasis".into()), ..Default::default() }); + l.values_para = Some(vectorcraft_doc::ParaStyle { style_name: Some("Body".into()), ..Default::default() }); + let rows = vectorcraft_doc::SeriesPaint { stroke: Some(brand), ..Default::default() }; + l.legend_rows = Some(vectorcraft_doc::graph::LabelRows { paint: rows, ..Default::default() }); + id +} + +/// The swatch names a graph's stored paints link to, and its value labels' character and +/// paragraph style names. +fn graph_links(s: &Session, id: NodeId) -> (Vec, Option, Option) { + let n = node(s, id); + let swatches = vectorcraft_doc::swatches::graph_paints(&n).filter_map(|p| match p { + Paint::Solid { swatch: Some(w), .. } => Some(w.clone()), + _ => None, + }); + let l = &n.extras().graph.as_deref().unwrap().label_styles; + let values = l.values.as_ref().and_then(|st| st.style_name.clone()); + (swatches.collect(), values, l.values_para.as_ref().and_then(|pa| pa.style_name.clone())) +} + +/// A pasted graph brings the swatches and text styles its series and labels keep, and follows +/// them when they come in under new names. +#[test] +fn a_pasted_graph_brings_and_follows_its_label_styles_and_paints() { + let mut s = session(); + let g = styled_graph(&mut s); + copy(&mut s, &[g]); + + new_doc(&mut s); + let v = s.execute("edit.pasteInPlace", &json!({})).unwrap(); + let d = doc(&s); + assert!(d.swatch("Brand").is_some(), "{v}"); + assert!(d.char_styles.iter().any(|x| x.name == "Emphasis") && d.para_styles.iter().any(|x| x.name == "Body"), "{v}"); + let brand = vec!["Brand".to_string(); 3]; + assert_eq!(graph_links(&s, ids_of(&v)[0]), (brand.clone(), Some("Emphasis".into()), Some("Body".into()))); + + // Into a document that gives each name something else: they come in renamed. + new_doc(&mut s); + s.execute("swatch.new", &json!({"name": "Brand", "color": "#0000ff", "global": true})).unwrap(); + s.execute("charStyle.new", &json!({"name": "Emphasis", "attrs": {"size": 8.0}})).unwrap(); + s.execute("paraStyle.new", &json!({"name": "Body", "attrs": {"space_after": 2.0}})).unwrap(); + let v = s.execute("edit.pasteInPlace", &json!({"swatchConflict": "add"})).unwrap(); + let brand2 = vec!["Brand 2".to_string(); 3]; + assert_eq!(graph_links(&s, ids_of(&v)[0]), (brand2, Some("Emphasis 2".into()), Some("Body 2".into())), "{v}"); + + // Without formatting: the labels' text styles stay behind. + new_doc(&mut s); + let v = s.execute("edit.pasteWithoutFormatting", &json!({})).unwrap(); + assert!(doc(&s).char_styles.is_empty() && doc(&s).para_styles.is_empty(), "{v}"); + assert_eq!(graph_links(&s, ids_of(&v)[0]), (brand, None, None)); +} + #[test] fn pasting_again_reuses_what_an_earlier_paste_added() { let mut s = session(); diff --git a/crates/engine/src/tests/expand.rs b/crates/engine/src/tests/expand.rs index d853b91110..e4bfa26b2f 100644 --- a/crates/engine/src/tests/expand.rs +++ b/crates/engine/src/tests/expand.rs @@ -422,6 +422,122 @@ fn a_compound_clipping_path_in_the_art_keeps_its_shape() { assert_eq!(seen, (true, true)); } +/// A Drop Shadow 20 pt to the right on row `row` of instance `id`, solid black with no blur. +fn shadow_row(s: &mut Session, id: NodeId, row: usize) { + let params = json!({"x": 20, "y": 0, "blur": 0, "opacity": 100, "mode": "normal"}); + run(s, "effect.apply", json!({"ids": [id.0], "item": row, "effect": "stylize.dropShadow", "params": params})); +} + +/// Is `p` dark: a shadow, not the white page or the red fill? +fn dark(p: [u8; 4]) -> bool { + p[..3].iter().all(|c| *c < 100) +} + +/// A raster effect on a symbol instance's fill draws on the filled shapes of its art: the shadow +/// falls right of the square, the bounds take it in, a hidden fill draws none, and Expand +/// Appearance keeps the look in one undo step. +#[test] +fn a_symbol_instance_fill_shadow_shadows_its_filled_art() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", true); + assert!(near(render(&s).pixel(160, 100), WHITE)); + shadow_row(&mut s, id, row); + let before = render(&s); + assert!(dark(before.pixel(165, 100)), "the fill's shadow falls right of the square: {:?}", before.pixel(165, 100)); + assert!(near(before.pixel(100, 100), RED), "the art draws over its shadow: {:?}", before.pixel(100, 100)); + let doc = s.doc().unwrap().doc.clone(); + let b = vectorcraft_render::painted_bounds(&doc, doc.node(id).unwrap()).expect("the instance has bounds"); + assert!(b.x1 >= 169.0, "the bounds take in the shadow: {b:?}"); + // A view that shows only the shadow, right of the square: the instance is still drawn. + let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; + let right = vectorcraft_render::Renderer::new().render(&doc, 200, 200, vectorcraft_geom::Affine::translate((-155.0, 0.0)), &opts); + assert!(dark(right.pixel(10, 100)), "the shadow past the square's box is drawn: {:?}", right.pixel(10, 100)); + + assert!(expandable(&s), "the fill's shadow can be expanded"); + run(&mut s, "effect.expandAppearance", json!({})); + let ch = node(&s, id).children().cloned().expect("the instance becomes a group"); + assert!(matches!(ch[0].kind, NodeKind::Image(_)), "the shadow is an image under the art: {}", ch[0].kind_label()); + assert!(matches!(ch[1].kind, NodeKind::SymbolInstance { .. }), "the art stays an instance: {}", ch[1].kind_label()); + no_effects_left(&ch[1]); + let after = render(&s); + assert!(differing(&before, &after, 24) < 400, "expanding changed {} pixels", differing(&before, &after, 24)); + run(&mut s, "edit.undo", json!({})); + let n = node(&s, id); + assert!(matches!(n.kind, NodeKind::SymbolInstance { .. }), "one undo gives the instance back"); + assert!(n.appearance.items[row].effects().iter().any(|e| e.id == "stylize.dropShadow")); + + run(&mut s, "appearance.setItem", json!({"ids": [id.0], "index": row, "visible": false})); + assert!(near(render(&s).pixel(165, 100), WHITE), "a hidden fill draws no shadow"); +} + +/// A raster effect on a symbol instance's stroke draws on the stroked shapes of its art only: the +/// stroke's shadow falls right of the stroke, and none falls where only the fill would cast one. +#[test] +fn a_symbol_instance_stroke_shadow_shadows_only_its_stroked_art() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", false); + shadow_row(&mut s, id, row); + let r = render(&s); + assert!(dark(r.pixel(170, 100)), "the stroke's shadow falls right of the stroke: {:?}", r.pixel(170, 100)); + assert!(near(r.pixel(160, 100), WHITE), "the fill casts no shadow: {:?}", r.pixel(160, 100)); +} + +/// Geometry and raster effects on one fill of an instance: the shadow falls from the grown fill. +#[test] +fn a_symbol_instance_fill_shadow_follows_the_fill_offset_before_it() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", true); + offset_row(&mut s, id, row, 10.0); + shadow_row(&mut s, id, row); + let r = render(&s); + assert!(near(r.pixel(44, 100), RED), "the fill grows by the offset: {:?}", r.pixel(44, 100)); + assert!(dark(r.pixel(175, 100)), "the shadow falls from the grown fill: {:?}", r.pixel(175, 100)); +} + +/// An instance inside a symbol's art passes the outer instance's fill shadow on to its own art. +#[test] +fn a_nested_symbol_instance_takes_the_outer_fill_shadow() { + let mut s = session(); + square(&mut s); + run(&mut s, "symbol.new", json!({"name": "Inner"})); + let id = NodeId(run(&mut s, "symbol.new", json!({"name": "Outer"}))["id"].as_u64().unwrap()); + run(&mut s, "appearance.addFill", json!({"ids": [id.0]})); + let row = node(&s, id).appearance.items.len() - 1; + run(&mut s, "appearance.setItem", json!({"ids": [id.0], "index": row, "none": true})); + shadow_row(&mut s, id, row); + assert!(dark(render(&s).pixel(165, 100)), "the inner art casts the shadow: {:?}", render(&s).pixel(165, 100)); +} + +/// Export draws a symbol instance's fill shadow on the filled shapes of its art: baking leaves the +/// shadow on the art's fill, not its stroke, and PDF export makes an image of it. +#[test] +fn export_draws_a_symbol_instance_fill_shadow() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", true); + shadow_row(&mut s, id, row); + let doc = s.doc().unwrap().doc.clone(); + let baked = vectorcraft_render::effects::bake_document(&doc).expect("the instance is baked"); + let n = baked.node(id).expect("the baked art keeps the instance's id").clone(); + assert!(!matches!(n.kind, NodeKind::SymbolInstance { .. })); + let shadowed = |i: &vectorcraft_doc::AppearanceItem| i.effects().iter().any(|e| e.id == "stylize.dropShadow"); + let (mut fills, mut strokes) = (0, 0); + n.walk(&mut |part| { + fills += part.appearance.items.iter().filter(|i| i.is_fill() && shadowed(i)).count(); + strokes += part.appearance.items.iter().filter(|i| !i.is_fill() && shadowed(i)).count(); + }); + assert_eq!((fills, strokes), (1, 0), "the art's fill carries the shadow"); + + let flat = crate::flatten_raster_effects(&doc).expect("the shadow becomes an image"); + let mut image = None; + flat.node(id).expect("the flattened art keeps the instance's id").walk(&mut |part| { + if matches!(part.kind, NodeKind::Image(_)) { + image = part.geometric_bounds(); + } + }); + let image = image.expect("the shadow is an image"); + assert!(image.x1 >= 165.0, "the image holds the shadow right of the square: {image:?}"); +} + #[test] fn two_fills_and_an_offset_stroke_become_three_paths_that_look_the_same() { let mut s = session(); diff --git a/crates/engine/src/tests/knockout.rs b/crates/engine/src/tests/knockout.rs index 723ef66dd2..9f2139fd47 100644 --- a/crates/engine/src/tests/knockout.rs +++ b/crates/engine/src/tests/knockout.rs @@ -138,7 +138,7 @@ fn knockout_settings_round_trip_and_old_bools_load() { } #[test] -fn pdf_and_svg_write_knockout_groups_as_masks() { +fn pdf_writes_knockout_groups_and_svg_writes_them_as_masks() { let mut s = session(); let g = knockout_group(&mut s); let plain = s.doc().unwrap().doc.clone(); @@ -149,10 +149,11 @@ fn pdf_and_svg_write_knockout_groups_as_masks() { let r = pdf(&plain); assert!(!String::from_utf8_lossy(&r.bytes).contains("/SMask") && r.warnings.is_empty()); let r = pdf(&ko); - let text = String::from_utf8_lossy(&r.bytes); - // The lower rectangle is drawn through a luminosity mask made of the upper one's alpha. - assert!(text.contains("/Luminosity") && text.contains("/Alpha"), "soft masks"); - assert!(r.warnings.iter().any(|w| w.contains("knockout")), "{:?}", r.warnings); + let text = String::from_utf8_lossy(&r.bytes).replace(' ', ""); + // The rectangles knock out the area they paint: a PDF knockout group, without soft masks. + assert_eq!(text.matches("/Ktrue").count(), 1, "a knockout group"); + assert!(!text.contains("/Luminosity"), "no soft masks"); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); let svg = |d: &Document| vectorcraft_svg::export(d, &Default::default()); assert!(!svg(&plain).contains("knockout")); @@ -173,5 +174,5 @@ fn pdf_and_svg_write_knockout_groups_as_masks() { assert!(svg(&page).contains("isolation")); page.page_knockout = true; assert_eq!(svg(&page).matches("mask=\"url(#knockout-").count(), 1, "the layer is neutral: its objects knock each other out"); - assert!(String::from_utf8_lossy(&pdf(&page).bytes).contains("/Luminosity")); + assert!(String::from_utf8_lossy(&pdf(&page).bytes).replace(' ', "").contains("/Ktrue")); } diff --git a/crates/engine/src/tests/params.rs b/crates/engine/src/tests/params.rs index 8a33376075..52cb314de0 100644 --- a/crates/engine/src/tests/params.rs +++ b/crates/engine/src/tests/params.rs @@ -849,3 +849,75 @@ fn journal_entries_pass_the_check() { assert!(check_params(id, params).is_ok(), "{id} {params}"); } } + +/// An index of a path, type, panel, library, style or ruler command given as something other than a +/// whole number of 0 or more (`"3"`, `2.5`, `-1`, `true`, `[]`, `{}`) is refused as the wrong type, +/// naming its key. A required one is not reported as missing, and an optional one (`subpath` of +/// `path.appendAnchor`, `path.close` and `path.reverse`, `artboard` of the ruler origin commands) is +/// not read as its default. A required one left out or null is still missing, and an optional one left +/// out or null keeps its default. Neither changes the document or records an undo step. +#[test] +fn a_malformed_index_is_not_missing_or_its_default() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let id = s.execute("path.create", &json!({"d": "M0 0 L10 0 M20 0 L30 0"})).unwrap()["id"].clone(); + s.execute("library.create", &json!({"name": "Brand"})).unwrap(); + s.execute("library.createGroup", &json!({"name": "Warm"})).unwrap(); + let doc = s.doc().unwrap().doc.clone(); + let undo = s.doc().unwrap().history.undo.len(); + let with = |base: &serde_json::Value, key: &str, v: Option| { + let mut p = base.clone(); + let o = p.as_object_mut().unwrap(); + match v { + Some(v) => o.insert(key.into(), v), + None => o.remove(key), + }; + p + }; + let refused = |s: &mut Session, cmd: &str, p: &serde_json::Value, want: &str| match s.execute(cmd, p) { + Err(EngineError::BadParams { cmd: c, msg }) => assert_eq!((c.as_str(), msg.as_str()), (cmd, want), "{p}"), + other => panic!("{cmd} {p}: {other:?}"), + }; + let malformed = [json!("3"), json!(2.5), json!(-1), json!(true), json!([]), json!({})]; + let handle = json!({"id": id, "x": 5, "y": 5}); + let required = [ + ("path.setHandle", handle.clone(), "anchor", "missing anchor"), + ("path.moveAnchor", handle.clone(), "anchor", "missing anchor"), + ("path.insertAnchor", json!({"id": id}), "segment", "missing segment"), + ("text.setPathHandle", handle.clone(), "anchor", "missing anchor"), + ("artboard.reorder", json!({"index": 0}), "to", "missing to"), + ("library.moveGroup", json!({"group": "Warm"}), "to", "missing `to` (an index)"), + ("graphicStyle.move", json!({"name": "Default"}), "to", "missing `to` (an index)"), + ]; + for (cmd, base, key, missing) in &required { + for v in &malformed { + refused(&mut s, cmd, &with(base, key, Some(v.clone())), &format!("`{key}` must be a whole number")); + } + for p in [with(base, key, None), with(base, key, Some(json!(null)))] { + refused(&mut s, cmd, &p, missing); + } + } + let optional = [ + ("path.appendAnchor", handle.clone(), "subpath"), + ("path.close", json!({"id": id}), "subpath"), + ("path.reverse", json!({"ids": [id]}), "subpath"), + ("document.rulerOrigin", json!({}), "artboard"), + ("document.setRulerOrigin", json!({"x": 5, "y": 5}), "artboard"), + ("document.resetRulerOrigin", json!({}), "artboard"), + ]; + for (cmd, base, key) in &optional { + for v in &malformed { + refused(&mut s, cmd, &with(base, key, Some(v.clone())), &format!("`{key}` must be a whole number")); + } + } + // A whole number that names no artboard is still out of range. + refused(&mut s, "document.resetRulerOrigin", &json!({"artboard": 1}), "index out of range"); + assert_eq!(s.doc().unwrap().doc, doc); + assert_eq!(s.doc().unwrap().history.undo.len(), undo); + // Left out or null, an optional one keeps its default. + for (cmd, base, key) in &optional { + for p in [with(base, key, None), with(base, key, Some(json!(null)))] { + s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}")); + } + } +} diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 0f58e00181..a3b2800fd3 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -8321,7 +8321,7 @@ impl ArtBrushOptions { /// A path with the Calligraphic brush, and one with an Art brush in Tints whose art bends along the /// path, come back as those paths with the brush on their stroke at its weight, not as their drawn -/// look; a path with the Art brush that keeps its art's colors comes back as its drawn look. +/// look; so does a path with the Art brush that keeps its art's colors. #[test] fn brushed_paths_are_read_back_brushed() { let mut doc = brushes_document(); @@ -8341,8 +8341,63 @@ fn brushed_paths_are_read_back_brushed() { assert_eq!(art[1].path_data(), ours[1].path_data()); brushed(&mut doc, "Plain ribbon", (20.0, 180.0), (280.0, 180.0), 1.0); let out = export_ai_editing(&doc, &opts()).unwrap(); - assert!(out.warnings.iter().any(|w| w == writer::STROKES), "{:?}", out.warnings); - assert!(matches!(read_back(&doc).layers[0].children().unwrap()[2].kind, NodeKind::Group { .. })); + assert!(!out.warnings.iter().any(|w| w == writer::STROKES), "{:?}", out.warnings); + let back = read_back(&doc); + let n = &back.layers[0].children().unwrap()[2]; + assert!(matches!(n.kind, NodeKind::Path { .. }), "{n:#?}"); + assert_eq!(n.appearance.stroke().and_then(|s| s.brush.as_deref()), Some("Plain ribbon")); +} + +/// Art brushes at another width, flipped along or across, scaled proportionately or stretched +/// between guides (flipped along too, or at guides the written numbers round) are written with those +/// options, and a path with each comes back as the path with the brush on its stroke, except Scale +/// Proportionately's, which is written as its brush's art. +#[test] +fn art_brushes_with_options_are_read_back_with_them() { + use vectorcraft_brush::{ArtBrush, ArtScale, Brush, BrushKind}; + let thirds = ArtScale::BetweenGuides { start: 1.0 / 3.0, end: 2.0 / 3.0 }; + let options: Vec<(&str, ArtBrush)> = vec![ + ("Wide", ArtBrush { width: 250.0, ..ArtBrush::default() }), + ("Along", ArtBrush { flip_along: true, ..ArtBrush::default() }), + ("Across", ArtBrush { flip_across: true, ..ArtBrush::default() }), + ("Proportional", ArtBrush { scale: ArtScale::Proportional, width: 50.0, ..ArtBrush::default() }), + ("Guides", ArtBrush { scale: ArtScale::BetweenGuides { start: 0.25, end: 0.5 }, ..ArtBrush::default() }), + ("Guides along", ArtBrush { scale: ArtScale::BetweenGuides { start: 0.25, end: 0.5 }, flip_along: true, ..ArtBrush::default() }), + ("Thirds", ArtBrush { scale: thirds, flip_along: true, ..ArtBrush::default() }), + ]; + let lib: Vec = options + .iter() + .map(|(name, a)| Brush { name: (*name).into(), kind: BrushKind::Art(ArtBrush { art: brush_art_node(), ..a.clone() }) }) + .collect(); + let mut doc = Document::new(300.0, 200.0); + vectorcraft_brush::store(&mut doc, &lib); + for (i, b) in lib.iter().enumerate() { + let y = 20.0 + 25.0 * i as f64; + brushed(&mut doc, &b.name, (20.0, y), (280.0, y), 1.0); + } + let out = export_ai_editing(&doc, &opts()).unwrap(); + assert!(!out.warnings.iter().any(|w| w == writer::BRUSHES), "{:?}", out.warnings); + let back = read_back(&doc); + let got = vectorcraft_brush::library(&back); + assert_eq!(got.len(), lib.len()); + for (g, w) in got.iter().zip(&lib) { + let (BrushKind::Art(g), BrushKind::Art(w)) = (&g.kind, &w.kind) else { panic!("art") }; + if w.scale == thirds { + let ArtScale::BetweenGuides { start, end } = g.scale else { panic!("guides: {g:?}") }; + assert!((start - 1.0 / 3.0).abs() < 1e-6 && (end - 2.0 / 3.0).abs() < 1e-6, "{g:?}"); + assert_eq!(ArtBrushOptions::of(&ArtBrush { scale: thirds, ..g.clone() }), ArtBrushOptions::of(w)); + } else { + assert_eq!(ArtBrushOptions::of(g), ArtBrushOptions::of(w)); + } + } + for (n, b) in back.layers[0].children().unwrap().iter().zip(&lib) { + if b.name == "Proportional" { + assert!(matches!(n.kind, NodeKind::Group { .. }), "{}: the brush's art: {n:#?}", b.name); + continue; + } + assert!(matches!(n.kind, NodeKind::Path { .. }), "{}: {n:#?}", b.name); + assert_eq!(n.appearance.stroke().and_then(|s| s.brush.as_deref()), Some(b.name.as_str())); + } } /// A compound path with the Calligraphic brush comes back as that compound path with the brush on its @@ -8646,8 +8701,8 @@ fn brushes_write_the_same_bytes_again() { /// beside a brush that comes back, and each changed from a brush that comes back by the one setting /// that leaves it out: an Art brush whose art is a group with a fill of its own, a Scatter brush whose /// art is a path without points, an Art brush whose art is a group without members, Pattern and Bristle -/// brushes, Art brushes flipped along or across, overlapping, at another width, with a width that -/// varies, in another direction or scale, Art and Scatter brushes in Tints and Shades or Hue Shift, a +/// brushes, Art brushes overlapping, with a width that varies, in another direction or with guides +/// that leave no art between them, Art and Scatter brushes in Tints and Shades or Hue Shift, a /// brush whose name holds a slash and a second brush of a name. A brush left out doesn't take its /// name from a later one. #[test] @@ -8671,13 +8726,10 @@ fn brushes_the_reader_cant_read_are_left_out() { ("empty group", art_with(&|a| a.art = Node::group(vectorcraft_doc::NodeId(1), vec![]))), ("pattern", BrushKind::Pattern(PatternBrush::default())), ("bristle", BrushKind::Bristle(Bristle::default())), - ("flipped along", art_with(&|a| a.flip_along = true)), - ("flipped across", art_with(&|a| a.flip_across = true)), ("overlap", art_with(&|a| a.overlap = true)), - ("width", art_with(&|a| a.width = 50.0)), ("width varies", art_with(&|a| a.width_mode = Some(Variation::Pressure))), ("direction", art_with(&|a| a.direction = Direction::RightToLeft)), - ("proportional", art_with(&|a| a.scale = ArtScale::Proportional)), + ("guides with no art between them", art_with(&|a| a.scale = ArtScale::BetweenGuides { start: 0.5, end: 0.5 })), ("art tints and shades", art_with(&|a| a.colorization = Colorization::TintsAndShades)), ("art hue shift", art_with(&|a| a.colorization = hue)), ( @@ -8887,8 +8939,8 @@ fn an_empty_brushes_panel_comes_back_empty() { } /// A path with one of a new document's brushes writes the brushes the reader reads, and keeps its -/// brush. The new document's Art brush with other options, its Pattern brushes and its Bristle -/// brushes are left out with a warning, so the file opens without them. +/// brush. The new document's Pattern brushes and its Bristle brushes are left out with a warning, +/// so the file opens without them. #[test] fn a_new_documents_brush_on_a_path_writes_the_brushes() { let mut doc = Document::new(300.0, 200.0); @@ -8898,7 +8950,7 @@ fn a_new_documents_brush_on_a_path_writes_the_brushes() { let back = read_back(&doc); let n = &back.layers[0].children().unwrap()[0]; assert_eq!(n.appearance.stroke().and_then(|s| s.brush.as_deref()), Some("3 pt. Round")); - let left_out = ["Arrow", "Chain", "Stitches", "Diamonds", "Bristle Round", "Bristle Flat Fan"]; + let left_out = ["Chain", "Stitches", "Diamonds", "Bristle Round", "Bristle Flat Fan"]; let want: Vec<&str> = vectorcraft_brush::defaults().iter().map(|b| b.name.as_str()).filter(|n| !left_out.contains(n)).collect(); let lib = vectorcraft_brush::library(&back); assert_eq!(lib.iter().map(|b| b.name.as_str()).collect::>(), want); diff --git a/crates/eps/src/ai_editing/writer/brushes.rs b/crates/eps/src/ai_editing/writer/brushes.rs index 2c281e702d..e8a7d10b15 100644 --- a/crates/eps/src/ai_editing/writer/brushes.rs +++ b/crates/eps/src/ai_editing/writer/brushes.rs @@ -23,7 +23,7 @@ use super::{Writer, Xy}; use crate::ai_ops; use crate::ps; -pub(in crate::ai_editing) const BRUSHES: &str = "Pattern and Bristle brushes, Art brushes that don't stretch their art along the path from left to right at 100% unflipped without overlap, Scatter and Art brushes in Tints and Shades or Hue Shift, brushes whose art holds an empty path or group or more than paths, compound paths and groups with one solid fill and one plain stroke each, brushes whose names hold a slash or repeat an earlier brush's name, brushes that would open with other settings, brushes past the number or the amount of art one file's brushes can hold, and every brush on a large canvas are left out of the Brushes panel in the editing data for other apps"; +pub(in crate::ai_editing) const BRUSHES: &str = "Pattern and Bristle brushes, Art brushes that don't run their art from left to right, that overlap or whose width follows the pen, Scatter and Art brushes in Tints and Shades or Hue Shift, brushes whose art holds an empty path or group or more than paths, compound paths and groups with one solid fill and one plain stroke each, brushes whose names hold a slash or repeat an earlier brush's name, brushes that would open with other settings, brushes past the number or the amount of art one file's brushes can hold, and every brush on a large canvas are left out of the Brushes panel in the editing data for other apps"; /// Most brushes written (the reader's limit). const MAX_BRUSHES: usize = 4096; @@ -145,22 +145,71 @@ fn scatter_params(s: &Scatter, art: &str) -> Option { Some(format!("2 /{art}/ 5 0 0 0 / {}", nums6(&numbers))) } -/// An Art brush's parameters, its art the section `art`: `None` for options the reader doesn't read -/// (it reads an Art brush that stretches its art along the path from left to right at 100%, -/// unflipped, without overlap, in Tints or keeping its colors). -fn art_params(a: &ArtBrush, art: &str) -> Option { - let plain = a.direction == Direction::LeftToRight - && matches!(a.scale, ArtScale::Stretch) - && a.width == 100.0 - && a.width_mode.is_none() - && !a.flip_along - && !a.flip_across - && !a.overlap; - if !plain { +/// An Art brush's settings as its parameters' numbers 2 to 15 give them and a brushed stroke's filter +/// holds them ([`ART_SETTINGS`]): `None` for options the reader doesn't read (it reads an Art brush +/// that runs its art from left to right without overlap, at a fixed width, in Tints or keeping its +/// colors). The guides of Stretch Between Guides are the lengths of the art before the first and +/// after the second, measured before the art is flipped along the path. +fn art_settings(a: &ArtBrush) -> Option<[f64; 14]> { + if a.direction != Direction::LeftToRight || a.width_mode.is_some() || a.overlap { return None; } - let c = colorization_code(a.colorization)?; - let numbers = [1.0, c, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]; + let length = a.art.geometric_bounds().map_or(0.0, |b| b.width()); + let (proportional, segmented, before, after) = match a.scale { + ArtScale::Stretch => (0.0, 0.0, 0.0, 0.0), + ArtScale::Proportional => (1.0, 0.0, 0.0, 0.0), + ArtScale::BetweenGuides { start, end } if start < end => { + let (before, after) = (start * length, (1.0 - end) * length); + let (before, after) = if a.flip_along { (after, before) } else { (before, after) }; + (0.0, 1.0, before, after) + } + ArtScale::BetweenGuides { .. } => return None, + }; + let scaling = a.width / 100.0; + let flip = |on: bool| f64::from(u8::from(on)); + Some([ + colorization_code(a.colorization)?, + scaling, + 0.0, + 1.0, + flip(a.flip_along), + flip(a.flip_across), + proportional, + scaling, + scaling, + 0.0, + segmented, + before, + after, + 1.0, + ]) +} + +/// The entries of a brushed stroke's filter that hold an Art brush's settings, in the order of +/// [`art_settings`], with how each is written. +const ART_SETTINGS: [(&str, &str); 14] = [ + ("Colorization", "Int"), + ("Scaling", "Real"), + ("Spacing", "Real"), + ("Adjustment", "Int"), + ("Counterclockwise", "Int"), + ("UpsideDown", "Int"), + ("Proportional", "Bool"), + ("MinScaling", "Real"), + ("MaxScaling", "Real"), + ("ScaleYDependsOn", "Int"), + ("Segmented", "Bool"), + ("StartSegmentLength", "Real"), + ("EndSegmentLength", "Real"), + ("FoldAdjustment", "Bool"), +]; + +/// An Art brush's parameters, its art the section `art`: `None` for options the reader doesn't read +/// ([`art_settings`]). +fn art_params(a: &ArtBrush, art: &str) -> Option { + let settings = art_settings(a)?; + let mut numbers = vec![1.0]; + numbers.extend(settings); Some(format!("2 / {art}/ / / / / 0 0/ 5 0 0 0 / {}", nums6(&numbers))) } @@ -458,25 +507,10 @@ impl Writer<'_> { d.push_str("0 /Int (AngleRelativeTo) ,\n"); "(Calligraphic Brush) /Title ,\n" } - BrushKind::Art(_) => { + BrushKind::Art(a) => { d.push_str("15 /Int (Adobe Effect Expand Before Version) ,\n"); // brand-ok: the entry's name in the files - for (key, value, kind) in [ - ("StartSegmentLength", "0", "Real"), - ("EndSegmentLength", "0", "Real"), - ("Counterclockwise", "0", "Int"), - ("MinScaling", "1", "Real"), - ("MaxScaling", "1", "Real"), - ("Colorization", "1", "Int"), - ("Spacing", "0", "Real"), - ("Adjustment", "1", "Int"), - ("Proportional", "0", "Bool"), - ("Segmented", "0", "Bool"), - ("UpsideDown", "0", "Int"), - ("FoldAdjustment", "1", "Bool"), - ("ScaleYDependsOn", "0", "Int"), - ("Scaling", "1", "Real"), - ] { - let _ = writeln!(d, "{value} /{kind} ({key}) ,"); + for ((key, kind), value) in ART_SETTINGS.iter().zip(art_settings(a)?) { + let _ = writeln!(d, "{} /{kind} ({key}) ,", nums6(&[value])); } "(Art Brush) /Title ,\n" } diff --git a/crates/eps/src/import/ai/brush.rs b/crates/eps/src/import/ai/brush.rs index 5a5db47114..1180e42fa9 100644 --- a/crates/eps/src/import/ai/brush.rs +++ b/crates/eps/src/import/ai/brush.rs @@ -36,7 +36,13 @@ //! - Scatter: the size (2, 3), spacing (5, 6) and scatter (8, 9) as (low, high) factors of the //! art, the rotation in degrees (11, 12), how each varies (4, 7, 10, 13) and whether the //! rotation follows the path (14), and 1 Tints (1). -//! - Art: 1 Tints (2). +//! - Art: numbers 2 to 15 are the settings a path's art style holds as entries ([`ArtSettings`]): +//! the colorization (2: 0 None, 1 Tints; other codes color the art otherwise), the width as a factor +//! (3), `Spacing` (4), `Adjustment` (5), flipped along (6) and across (7), Scale Proportionately +//! (8), the lowest and highest width when the width follows the pen (9, 10, not drawn otherwise), +//! how the width varies (11, 0 Fixed), Stretch Between Guides (12) and the lengths of the art +//! before its first guide and after its second (13, 14, in the art's units, measured before +//! the art is flipped along the path), and `FoldAdjustment` (15). //! - Pattern: its scale as a factor (3). //! - A Bristle brush's parameters start with 8 numbers: two codes, then the stiffness, bristle //! thickness, density, paint opacity and bristle length as fractions and the size in @@ -45,14 +51,15 @@ //! [`super::read`]), usually in a group after its frame, an unpainted rectangle around it. A frame //! at the art's bounds isn't kept ([`without_frame`]); a wider one is. //! -//! What isn't read keeps its default: the Art brush's other options, the pattern brush's tiles +//! What isn't read keeps its default: an Art brush's settings that VectorCraft doesn't draw as the +//! file does ([`art_options`]) apart from its colorization, the pattern brush's tiles //! (which slot is which tile isn't known) and the colorization of the other brushes, which is left //! at None so the art keeps its own colors. use std::collections::{HashMap, HashSet}; use kurbo::Affine; -use vectorcraft_brush::{ArtBrush, Bristle, Brush, BrushKind, Calligraphic, Colorization, Scatter, Variation}; +use vectorcraft_brush::{ArtBrush, ArtScale, Bristle, Brush, BrushKind, Calligraphic, Colorization, Scatter, Variation}; use vectorcraft_color::Paint; use vectorcraft_doc::{AppearanceItem, Node, NodeKind}; @@ -118,13 +125,13 @@ pub(super) enum DraftKind { brush: Scatter, art: String, }, - /// The art is the section named `art`. `plain`: the brush stretches its art along the path, - /// unflipped, at 100% in Tints (see [`art_is_plain`]), so [`Self::Art`]'s `brush` draws as the - /// file's brush does. + /// The art is the section named `art`. `settings`: the brush's settings when [`art_options`] + /// reads them, so [`Self::Art`]'s `brush` draws as the file's brush does (its guides set from + /// its art's length by [`finish_brushes`]). Art { brush: ArtBrush, art: String, - plain: bool, + settings: Option, }, /// The five art sections the brush names, in its order (which is which tile isn't known), and /// its scale (%). @@ -226,12 +233,17 @@ pub(super) fn calligraphic(values: [f64; 3], variation: [f64; 3], codes: [f64; 3 Calligraphic { angle, roundness, size, variation, modes: Some(modes) } } +/// An Art brush's settings: its object's numbers 2 to 15, which a path's art style holds as the +/// entries `Colorization`, `Scaling`, `Spacing`, `Adjustment`, `Counterclockwise`, `UpsideDown`, +/// `Proportional`, `MinScaling`, `MaxScaling`, `ScaleYDependsOn`, `Segmented`, +/// `StartSegmentLength`, `EndSegmentLength` and `FoldAdjustment`, in that order. +pub(super) type ArtSettings = [f64; 14]; + /// The settings a path's art style gives its stroke's brush. #[derive(Clone, Debug, PartialEq)] pub(super) enum PathBrush { Calligraphic(Calligraphic), - /// An Art brush whose art stretches along the path, at the brush's own width, in Tints. - Art, + Art(ArtSettings), } /// Does an Art brush draw `art` as the file does? It bends paths in solid colors: other objects @@ -250,8 +262,9 @@ pub(super) fn art_bends(art: &Node) -> bool { bends } -/// The art sections of `drafts`' Art brushes that an Art brush draws as the file does. -pub(super) fn bending_art(drafts: &[BrushDraft], art: &HashMap) -> HashSet { +/// The art sections of `drafts`' Art brushes that an Art brush draws as the file does, with the +/// length of each (its width, the art running left to right). +pub(super) fn bending_art(drafts: &[BrushDraft], art: &HashMap) -> HashMap { let names: HashSet<&String> = drafts .iter() .filter_map(|d| match &d.kind { @@ -259,13 +272,15 @@ pub(super) fn bending_art(drafts: &[BrushDraft], art: &HashMap) -> _ => None, }) .collect(); - names.into_iter().filter(|n| art.get(*n).is_some_and(art_bends)).cloned().collect() + names.into_iter().filter_map(|n| art.get(n).filter(|a| art_bends(a)).and_then(|a| a.geometric_bounds()).map(|b| (n.clone(), b.width()))).collect() } /// Does the brush `draft` draw as `settings`, the settings a path's art style gives it? A path whose /// settings differ (Options of Selected Object) keeps its drawn look: a stroke names a brush, and -/// has no settings of its own. `bending`: [`bending_art`]. -pub(super) fn draws_as(draft: &BrushDraft, settings: &PathBrush, bending: &HashSet) -> bool { +/// has no settings of its own. An Art brush's settings that don't draw ([`drawn`]) may differ, and +/// the path's own settings must be ones the brush would open with ([`art_options`], [`opens_live`]). +/// `bending`: [`bending_art`]. +pub(super) fn draws_as(draft: &BrushDraft, settings: &PathBrush, bending: &HashMap) -> bool { let close = |a: f64, b: f64| (a - b).abs() <= 1e-3 * a.abs().max(b.abs()).max(1.0); match (&draft.kind, settings) { (DraftKind::Calligraphic(d), PathBrush::Calligraphic(c)) => { @@ -275,16 +290,90 @@ pub(super) fn draws_as(draft: &BrushDraft, settings: &PathBrush, bending: &HashS && d.variation.iter().zip(&c.variation).all(|(a, b)| close(*a, *b)) && d.modes() == c.modes() } - (DraftKind::Art { plain: true, art: name, .. }, PathBrush::Art) => bending.contains(name), + (DraftKind::Art { settings: Some(d), art: name, .. }, PathBrush::Art(s)) => { + let (dd, ds) = (drawn(d), drawn(s)); + art_options(s).is_some() + && bending.get(name).is_some_and(|length| { + // The guides match as lengths and as fractions of the art's length, so they differ + // neither on short art nor near the ends of long art. + dd.iter().zip(ds).all(|(a, b)| close(*a, b)) + && [11, 12].iter().all(|&i| close(dd[i] / length, ds[i] / length)) + && opens_live(d, *length) + && opens_live(s, *length) + }) + } _ => false, } } -/// Are an Art brush's last 15 numbers those of a brush that stretches its art along the path, -/// unflipped, at 100% in Tints? The numbers aren't read otherwise: any other value keeps its paths' -/// drawn look. -fn art_is_plain(numbers: &[f64; 15]) -> bool { - *numbers == [1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] +/// Art brush settings `s` with those that don't draw set to 0: the lowest and highest width while the +/// width is fixed, and the guides' lengths without Stretch Between Guides. +fn drawn(s: &ArtSettings) -> ArtSettings { + let mut out = *s; + if s[9] == 0.0 { + (out[7], out[8]) = (0.0, 0.0); + } + if s[10] == 0.0 { + (out[11], out[12]) = (0.0, 0.0); + } + out +} + +/// What an Art brush's settings give VectorCraft's Art brush, its guides apart ([`guides`]): `None` +/// when VectorCraft doesn't draw them as the file does: a colorization other than None or Tints, a +/// width outside [`ArtBrush::WIDTH_RANGE`] or one that varies, both Scale Proportionately and Stretch +/// Between Guides, a flip or scale that isn't 0 or 1, or a `Spacing`, `Adjustment` or +/// `FoldAdjustment` other than 0, 1 and 1 (what other values change isn't known). +fn art_options(s: &ArtSettings) -> Option { + let [colorization, scaling, spacing, adjustment, along, across, proportional, _, _, varies, segmented, _, _, fold] = *s; + let switch = |v: f64| match v { + 0.0 => Some(false), + 1.0 => Some(true), + _ => None, + }; + let colorization = match colorization { + 0.0 => Colorization::None, + 1.0 => Colorization::Tints, + _ => return None, + }; + let width = scaling * 100.0; + let (low, high) = ArtBrush::WIDTH_RANGE; + if !(low..=high).contains(&width) || varies != 0.0 || spacing != 0.0 || adjustment != 1.0 || fold != 1.0 { + return None; + } + let scale = match (switch(proportional)?, switch(segmented)?) { + (false, false) => ArtScale::Stretch, + (true, false) => ArtScale::Proportional, + (false, true) => ArtScale::BetweenGuides { start: 0.0, end: 1.0 }, + (true, true) => return None, + }; + Some(ArtBrush { scale, width, flip_along: switch(along)?, flip_across: switch(across)?, colorization, ..ArtBrush::default() }) +} + +/// The guides of Art brush settings `s` (with Stretch Between Guides) for art of length `length`, as +/// [`ArtScale::BetweenGuides`] fractions of the art as it runs along the path: `None` when they leave +/// no art between them. The settings measure the art before it is flipped along the path. +fn guides(s: &ArtSettings, length: f64) -> Option<(f64, f64)> { + let (before, after) = (s[11], s[12]); + if !(before >= 0.0 && after >= 0.0 && before + after < length) { + return None; + } + let (before, after) = if s[4] == 1.0 { (after, before) } else { (before, after) }; + Some((before / length, 1.0 - after / length)) +} + +/// Do Art brush settings `s` draw art of length `length`: no Stretch Between Guides, or guides with +/// art between them? +fn guides_fit(s: &ArtSettings, length: f64) -> bool { + s[10] == 0.0 || guides(s, length).is_some() +} + +/// Does a path with Art brush settings `s`, its art of length `length`, open with the brush on its +/// stroke? Its guides must fit ([`guides_fit`]). Scale Proportionately keeps the drawn look: +/// VectorCraft's Art brush sizes proportional art by the path's length and not by the stroke's +/// weight. +fn opens_live(s: &ArtSettings, length: f64) -> bool { + s[6] == 0.0 && guides_fit(s, length) } /// The brush named `name` of kind `kind` from its parameters `params`. @@ -342,15 +431,22 @@ pub(super) fn parse_tool_object(kind: Tool, name: &str, params: &str) -> Result< Tool::Art => { // The older form's 8 numbers aren't known: its brush keeps the defaults and its // paths their drawn look. The count tells the form, not the first number. - // Any other count isn't a form this reads. - let (colorization, plain) = if let Ok(n15) = <&[f64; 15]>::try_from(n.as_slice()) { - (if n15[1] == 1.0 { Colorization::Tints } else { Colorization::None }, art_is_plain(n15)) + // Any other count isn't a form this reads. Settings that aren't read keep the + // defaults but the colorization, and their paths their drawn look. + let (brush, settings) = if let Ok(&[first, ref settings @ ..]) = <&[f64; 15]>::try_from(n.as_slice()) { + match art_options(settings).filter(|_| first == 1.0) { + Some(brush) => (brush, Some(*settings)), + None => { + let colorization = if settings[0] == 1.0 { Colorization::Tints } else { Colorization::None }; + (ArtBrush { colorization, ..ArtBrush::default() }, None) + } + } } else if n.len() == 8 { - (ArtBrush::default().colorization, false) + (ArtBrush::default(), None) } else { return Err(BrushError::Values); }; - DraftKind::Art { brush: ArtBrush { colorization, ..ArtBrush::default() }, art: art()?, plain } + DraftKind::Art { brush, art: art()?, settings } } _ => { let [_, _, p3, ..] = n.as_slice() else { return Err(BrushError::Values) }; @@ -453,10 +549,15 @@ pub(super) fn finish_brushes_within( _ if !kept => None, DraftKind::Calligraphic(c) => Some(BrushKind::Calligraphic(c)), DraftKind::Scatter { brush, art: name } => copy(&name).map(|a| BrushKind::Scatter(Scatter { art: a, ..brush })), - DraftKind::Art { brush, art: name, .. } => copy(&name).map(|mut a| { + DraftKind::Art { mut brush, art: name, settings } => copy(&name).map(|mut a| { // The side of the section with y up draws on the right of the path, and a brush // draws art's y (down here) on the left of the path. a.transform(Affine::scale_non_uniform(1.0, -1.0), false); + if let ArtScale::BetweenGuides { start, end } = &mut brush.scale { + // Guides that leave no art between them stretch the whole art. + let length = a.geometric_bounds().map_or(0.0, |b| b.width()); + (*start, *end) = settings.and_then(|s| guides(&s, length)).unwrap_or((0.0, 1.0)); + } BrushKind::Art(ArtBrush { art: a, ..brush }) }), DraftKind::Pattern { .. } => None, @@ -586,8 +687,8 @@ pub(crate) struct BrushRead { pub brush: Brush, /// A path whose art style names the brush, at the brush's own settings, opens with the brush on /// its stroke: a Calligraphic brush, or an Art brush whose art an Art brush draws as the file - /// does ([`art_bends`]) and whose numbers are those of a brush that stretches its art along the - /// path, unflipped, at 100% in Tints. + /// does ([`art_bends`]) and whose settings VectorCraft draws as the file does ([`art_options`], + /// with guides that leave art between them and without Scale Proportionately: [`opens_live`]). pub live: bool, } @@ -604,7 +705,9 @@ pub(crate) fn read_brush_object(plugin: &str, name: &str, params: &str, art: Opt let live = kept(&drafts, §ions).first().copied().unwrap_or(false) && match &drafts[0].kind { DraftKind::Calligraphic(_) => true, - DraftKind::Art { plain: true, art: section, .. } => bending_art(&drafts, §ions).contains(section), + DraftKind::Art { settings: Some(s), art: section, .. } => { + bending_art(&drafts, §ions).get(section).is_some_and(|length| opens_live(s, *length)) + } _ => false, }; let [draft] = drafts; @@ -643,8 +746,8 @@ pub(crate) struct BrushesRead { /// The brush a stroke that names a brush is drawn with, by name: the first one kept /// ([`kept`]), as an index in [`Self::drafts`]. by_name: HashMap, - /// The art an Art brush draws as the file does ([`bending_art`]). - bending: HashSet, + /// The art an Art brush draws as the file does, with its length ([`bending_art`]). + bending: HashMap, } impl BrushesRead { @@ -681,7 +784,7 @@ impl BrushesRead { st.items.iter().all(|item| match item { StyleItem::Stroke { brush: Some((name, settings)), .. } => { let Some(draft) = self.by_name.get(name).and_then(|i| self.drafts.get(*i)) else { return false }; - draws_as(draft, settings, &self.bending) && !(matches!(settings, PathBrush::Art) && !st.effects.is_empty()) + draws_as(draft, settings, &self.bending) && !(matches!(settings, PathBrush::Art(_)) && !st.effects.is_empty()) } _ => true, }) diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index d31ada250c..9d66bd2d17 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -438,8 +438,8 @@ struct BrushesKept { read: (usize, usize), /// Which of [`Reader::brush_drafts`] are kept as brushes. kept: Vec, - /// The art an Art brush draws as the file does. - bending: HashSet, + /// The art an Art brush draws as the file does, with its length. + bending: HashMap, } struct Reader<'a> { diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index 911d8340cc..f86ee6603b 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -1181,7 +1181,7 @@ mod tests { assert!(nodes > 0); let draft = |i: usize| brush::BrushDraft { name: format!("Brush {i}"), - kind: brush::DraftKind::Art { brush: vectorcraft_brush::ArtBrush::default(), art: "A".to_string(), plain: false }, + kind: brush::DraftKind::Art { brush: vectorcraft_brush::ArtBrush::default(), art: "A".to_string(), settings: None }, }; let drafts: Vec<_> = (0..5).map(draft).collect(); let (kept, left_out) = brush::finish_brushes_within(drafts.clone(), &reader.brush_art, None, 3 * nodes); diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 87ba9752ef..f10682b592 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -992,8 +992,8 @@ struct StylePaints<'a> { brushes: &'a [super::super::brush::BrushDraft], /// Which of [`Self::brushes`] are kept as brushes ([`super::super::brush::kept`]). kept: &'a [bool], - /// The art an Art brush draws as the file does ([`super::super::brush::bending_art`]). - bending: &'a HashSet, + /// The art an Art brush draws as the file does, with its length ([`super::super::brush::bending_art`]). + bending: &'a HashMap, space: Affine, } @@ -1270,7 +1270,7 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p if !super::super::brush::draws_as(draft, settings, paints.bending) { return None; } - let art = matches!(settings, super::super::brush::PathBrush::Art); + let art = matches!(settings, super::super::brush::PathBrush::Art(_)); let on_a_path = matches!(object.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }); if !on_a_path && (art || !slot) { return None; @@ -1570,7 +1570,7 @@ mod tests { let st = &styles["Inked"]; let brushes = [parse_tool_object(Tool::Calligraphic, "N", "1 4 8 30 50 -150 210 2 5 6 0").unwrap()]; let (gradients, patterns, names, v7_patterns, bending) = - (HashMap::new(), HashMap::new(), ShownNames::default(), HashSet::new(), HashSet::new()); + (HashMap::new(), HashMap::new(), ShownNames::default(), HashSet::new(), HashMap::new()); let paints = StylePaints { gradients: &gradients, patterns: &patterns, @@ -1616,7 +1616,7 @@ mod tests { // GUARDS: an image with a Warp in its art style came in as its drawn look. let warp = ("Adobe Deform", "1 /Int (DeformStyle) ,\n0.4 /Real (DeformValue) ,\n"); // brand-ok: the filter's name in the files let (gradients, patterns, names, v7_patterns, bending) = - (HashMap::new(), HashMap::new(), ShownNames::default(), HashSet::new(), HashSet::new()); + (HashMap::new(), HashMap::new(), ShownNames::default(), HashSet::new(), HashMap::new()); let paints = StylePaints { gradients: &gradients, patterns: &patterns, diff --git a/crates/eps/src/import/ai/style.rs b/crates/eps/src/import/ai/style.rs index d27a297925..891c058bb6 100644 --- a/crates/eps/src/import/ai/style.rs +++ b/crates/eps/src/import/ai/style.rs @@ -1500,11 +1500,30 @@ const ART_ENTRIES: [&str; 18] = [ "Adobe Effect Expand Before Version", // brand-ok: the entry's name in the files ]; -/// An Art brush's filter `f` as the stroke it paints: `None` when it isn't a stroke in a color, names -/// no Art brush, holds an entry that isn't read, or its settings aren't those of art that stretches -/// along the path, unflipped, at the brush's own width (`Scaling` 1), in Tints (`Colorization` 1), -/// with `FoldAdjustment` 1, which is [`vectorcraft_brush::ArtBrush`]'s `overlap: false`: its drawn -/// look stays. +/// The entries of an Art brush's filter that hold its settings, in the order of +/// [`super::brush::ArtSettings`], with how each is written and its value when the filter leaves it +/// out (`None`: the filter must hold it). +const ART_SETTINGS: [(&str, Kind, Option); 14] = [ + ("Colorization", Kind::Int, None), + ("Scaling", Kind::Real, None), + ("Spacing", Kind::Real, Some(0.0)), + ("Adjustment", Kind::Int, Some(1.0)), + ("Counterclockwise", Kind::Int, Some(0.0)), + ("UpsideDown", Kind::Int, Some(0.0)), + ("Proportional", Kind::Bool, Some(0.0)), + ("MinScaling", Kind::Real, Some(1.0)), + ("MaxScaling", Kind::Real, Some(1.0)), + ("ScaleYDependsOn", Kind::Int, Some(0.0)), + ("Segmented", Kind::Bool, Some(0.0)), + ("StartSegmentLength", Kind::Real, Some(0.0)), + ("EndSegmentLength", Kind::Real, Some(0.0)), + ("FoldAdjustment", Kind::Bool, Some(1.0)), +]; + +/// An Art brush's filter `f` as the stroke it paints, with its settings: `None` when it isn't a +/// stroke in a color, names no Art brush, holds an entry that isn't read or a setting that isn't a +/// number of its kind, or tints its art with a swatch's color: its drawn look stays. Whether the +/// brush draws as these settings is [`super::brush::draws_as`]'s to say. fn art_stroke(f: &Filter) -> Option { if !f.paints.0 || f.gradient @@ -1519,28 +1538,13 @@ fn art_stroke(f: &Filter) -> Option { if plugin != ART_BRUSH { return None; } - let off = |key: &str, kind: Kind| matches!(entry(f, key, kind), Ok(None | Some(0.0))); - let plain = off("Proportional", Kind::Bool) - && off("Segmented", Kind::Bool) - && off("UpsideDown", Kind::Int) - && off("Counterclockwise", Kind::Int) - && off("ScaleYDependsOn", Kind::Int) - && off("Spacing", Kind::Real) - && off("StartSegmentLength", Kind::Real) - && off("EndSegmentLength", Kind::Real); - if !plain || entry(f, "Adjustment", Kind::Int).ok()?.unwrap_or(1.0) != 1.0 || entry(f, "FoldAdjustment", Kind::Bool).ok()?.unwrap_or(1.0) != 1.0 { - return None; - } - let one = |key: &str| entry(f, key, Kind::Real).is_ok_and(|v| v.unwrap_or(1.0) == 1.0); - if entry(f, "Scaling", Kind::Real).ok()?? != 1.0 || !one("MinScaling") || !one("MaxScaling") { - return None; - } - if entry(f, "Colorization", Kind::Int).ok()?? != 1.0 { - return None; + let mut settings = [0.0; 14]; + for (slot, (key, kind, missing)) in settings.iter_mut().zip(ART_SETTINGS) { + *slot = entry(f, key, kind).ok()?.or(missing)?; } let paint = f.stroke.clone()?; // The tinted pieces are process colors, so a swatch's ink would be lost. - if matches!(paint, Paint::Solid { swatch: Some(_), .. }) { + if settings[0] == 1.0 && matches!(paint, Paint::Solid { swatch: Some(_), .. }) { return None; } Some(StyleItem::Stroke { @@ -1552,7 +1556,7 @@ fn art_stroke(f: &Filter) -> Option { dash: None, profile: None, align: StrokeAlign::Center, - brush: Some((name.clone(), PathBrush::Art)), + brush: Some((name.clone(), PathBrush::Art(settings))), }) } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 8a9bab7530..7a821bf184 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -9797,7 +9797,7 @@ fn a_pattern_brushs_five_slots_are_kept() { fn an_older_art_brush_comes_in_with_its_art() { const OLD_RIBBON: &str = "1 / Ribbon art/ / / / / 0 1 / 1 0 1 0 1 0 0 0"; let d = brush::parse_tool_object(brush::Tool::Art, "Ribbon", OLD_RIBBON).unwrap(); - assert!(matches!(d.kind, brush::DraftKind::Art { ref art, plain: false, .. } if art == "Ribbon art"), "{d:?}"); + assert!(matches!(d.kind, brush::DraftKind::Art { ref art, settings: None, .. } if art == "Ribbon art"), "{d:?}"); let brush::DraftKind::Art { ref brush, .. } = d.kind else { panic!("art") }; assert_eq!(brush.colorization, vectorcraft_brush::ArtBrush::default().colorization, "the default colorization"); // A count of numbers neither form has isn't read. @@ -9807,8 +9807,8 @@ fn an_older_art_brush_comes_in_with_its_art() { assert_eq!(brush::parse_tool_object(brush::Tool::Scatter, "Spray", "1 /Dot art/ 0 1 / 1 0 1 0 1 0 0 0"), Err(brush::BrushError::Values)); // 8 numbers are the older form whatever the first number says: read with the defaults. let d = brush::parse_tool_object(brush::Tool::Art, "Ribbon", "0 / Ribbon art/ / / / / 0 1 / 1 0 1 0 1 0 0 0").unwrap(); - let brush::DraftKind::Art { ref brush, ref art, plain } = d.kind else { panic!("art") }; - assert_eq!((art.as_str(), plain, brush.colorization), ("Ribbon art", false, vectorcraft_brush::ArtBrush::default().colorization)); + let brush::DraftKind::Art { ref brush, ref art, settings } = d.kind else { panic!("art") }; + assert_eq!((art.as_str(), settings, brush.colorization), ("Ribbon art", None, vectorcraft_brush::ArtBrush::default().colorization)); let objects = [plugin_object(ART_BRUSH, "Ribbon", OLD_RIBBON), plugin_object(ART_BRUSH, "Bare", "1 / / / / / / 0 1 / 1 0 1 0 1 0 0 0")].concat(); let r = read_ok(&format!("{}{objects}{}", brush_art("Ribbon art", 4.0), layer(""))); let lib = vectorcraft_brush::library(&r.doc); @@ -10471,9 +10471,109 @@ fn an_art_brush_bends_only_paths_in_solid_fills() { assert!(wrong.is_empty(), "{wrong:?}"); } +/// The entries of an Art brush's filter that hold its settings, in the order of its object's numbers 2 +/// to 15, with how each is written. +const ART_KEYS: [(&str, &str); 14] = [ + ("Colorization", "Int"), + ("Scaling", "Real"), + ("Spacing", "Real"), + ("Adjustment", "Int"), + ("Counterclockwise", "Int"), + ("UpsideDown", "Int"), + ("Proportional", "Bool"), + ("MinScaling", "Real"), + ("MaxScaling", "Real"), + ("ScaleYDependsOn", "Int"), + ("Segmented", "Bool"), + ("StartSegmentLength", "Real"), + ("EndSegmentLength", "Real"), + ("FoldAdjustment", "Bool"), +]; + +/// The Art brush `Ribbon` (its art 40 long: [`lopsided_art`]) whose settings, numbers 2 to 15 of its +/// object, are those of [`RIBBON`] with `changes` (index in the 14, value), and the style `Inked` that +/// gives a path the same settings. +fn ribbon_with(changes: &[(usize, &str)]) -> (String, String) { + let mut settings = ["1", "1", "0", "1", "0", "0", "0", "1", "1", "0", "0", "0", "0", "1"]; + for (i, v) in changes { + settings[*i] = v; + } + let brush = format!("2 / Ribbon art/ / / / / 0 0/ 5 0 0 0 / 1 {}", settings.join(" ")); + let entries: Vec<(&str, &str, &str)> = ART_KEYS.iter().zip(settings).map(|((key, kind), value)| (*key, value, *kind)).collect(); + (format!("{}{}", lopsided_art("Ribbon art"), plugin_object(ART_BRUSH, "Ribbon", &brush)), art_style("Ribbon", 1.0, &entries, "")) +} + +/// A path with an Art brush whose settings are its own comes in brushed, the brush with those +/// settings: None, another width (the lowest and highest widths aren't drawn while the width is +/// fixed), flipped along or across, and Stretch Between Guides, whose guides come in as fractions of +/// the art as it runs along the path (measured before it is flipped along it, so a flip along swaps +/// them). Lengths without Stretch Between Guides aren't guides. A brush that scales its art +/// proportionately comes into the panel with that option, and its path keeps its drawn look. +#[test] +fn an_art_brush_with_options_comes_in_brushed_with_them() { + use vectorcraft_brush::{ArtBrush, ArtScale, Colorization}; + let check_file = |why: &str, (def, style): (String, String), want: &dyn Fn(&ArtBrush) -> bool| { + let r = read_ok(&format!("{def}{style}{}", layer(&brushed_path()))); + let a = art(&r.doc); + assert!(matches!(a[0].kind, NodeKind::Path { .. }), "{why}: the path, not its look: {a:#?}"); + assert_eq!(a[0].appearance.stroke().and_then(|s| s.brush.as_deref()), Some("Ribbon"), "{why}"); + let lib = vectorcraft_brush::library(&r.doc); + let vectorcraft_brush::BrushKind::Art(brush) = brush_named(&lib, "Ribbon") else { panic!("{why}: art") }; + assert!(want(brush), "{why}: {brush:?}"); + r + }; + let check = |why: &str, changes: &[(usize, &str)], want: &dyn Fn(&ArtBrush) -> bool| check_file(why, ribbon_with(changes), want); + let near = |a: f64, b: f64| (a - b).abs() < 1e-9; + // Settings that don't draw may differ between the brush and the path: the lowest and highest + // width while the width is fixed, and lengths without Stretch Between Guides. + let (def, style) = ribbon_with(&[(1, "2"), (7, "2"), (8, "2")]); + let style = style.replace("2 /Real (MinScaling) ,\n", "").replace("2 /Real (MaxScaling) ,\n", ""); + check_file("the path leaves out the widths the pen would give", (def, style), &|b| b.width == 200.0); + let (def, _) = ribbon_with(&[]); + let (_, style) = ribbon_with(&[(11, "5"), (12, "10")]); + check_file("lengths on the path alone", (def, style), &|b| b.scale == ArtScale::Stretch); + check("its art's colors", &[(0, "0")], &|b| b.colorization == Colorization::None); + check("twice as wide", &[(1, "2"), (7, "2"), (8, "2")], &|b| b.width == 200.0); + check("half as wide, the pen's widths left", &[(1, "0.5")], &|b| b.width == 50.0 && b.width_mode.is_none()); + let along = check("flipped along", &[(4, "1")], &|b| b.flip_along && !b.flip_across); + let across = check("flipped across", &[(5, "1")], &|b| b.flip_across && !b.flip_along); + let (def, style) = ribbon_with(&[(6, "1")]); + let r = read_ok(&format!("{def}{style}{}", layer(&brushed_path()))); + assert!(matches!(art(&r.doc)[0].kind, NodeKind::Group { .. }), "proportional: the drawn look"); + let lib = vectorcraft_brush::library(&r.doc); + let vectorcraft_brush::BrushKind::Art(brush) = brush_named(&lib, "Ribbon") else { panic!("proportional: art") }; + assert_eq!(brush.scale, ArtScale::Proportional); + check( + "between guides", + &[(10, "1"), (11, "5"), (12, "10")], + &|b| matches!(b.scale, ArtScale::BetweenGuides { start, end } if near(start, 5.0 / 40.0) && near(end, 30.0 / 40.0)), + ); + check("between guides, flipped along", &[(10, "1"), (11, "5"), (12, "10"), (4, "1")], &|b| { + b.flip_along && matches!(b.scale, ArtScale::BetweenGuides { start, end } if near(start, 10.0 / 40.0) && near(end, 35.0 / 40.0)) + }); + check("lengths without guides", &[(11, "5"), (12, "10")], &|b| b.scale == ArtScale::Stretch); + // Along a straight line, y down: unflipped, the wide block is above the line and the narrow one + // below it, at the start. + let line = kurbo::BezPath::from_vec(vec![kurbo::PathEl::MoveTo(Point::new(0.0, 0.0)), kurbo::PathEl::LineTo(Point::new(100.0, 0.0))]); + let filled = |r: &Structure, x: f64, y: f64| { + let lib = vectorcraft_brush::library(&r.doc); + let st = art(&r.doc)[0].appearance.stroke().cloned().unwrap(); + let ribbon = lib.iter().find(|b| b.name == "Ribbon").unwrap(); + let pieces = vectorcraft_brush::stroke_pieces(&r.doc, ribbon, &line, &st); + pieces.iter().any(|n| { + let mut hit = false; + n.walk(&mut |c| hit |= c.path_data().is_some_and(|p| kurbo::Shape::winding(&p.to_bezpath(), Point::new(x, y)) != 0)); + hit + }) + }; + // At weight 1 the art is 8 across: each block is 4 across, on its side of the line. + assert!(filled(&across, 50.0, 2.0) && filled(&across, 10.0, -2.0) && !filled(&across, 50.0, -2.0), "across: the wide block below"); + assert!(filled(&along, 50.0, -2.0) && filled(&along, 90.0, 2.0) && !filled(&along, 10.0, 2.0), "along: the narrow block at the end"); +} + /// An Art brushed path keeps its drawn look when the brush wouldn't draw it the way the file does: -/// its settings aren't those of art that stretches along the path at the brush's own width and -/// color, the brush's own aren't, its art isn't read, or the brush isn't one the panel keeps. +/// its settings differ from the brush's, the brush's are ones VectorCraft doesn't draw as the file +/// does, its art isn't read, or the brush isn't one the panel keeps. #[test] fn an_art_brushed_path_the_brush_cant_draw_keeps_its_drawn_look() { let good = format!("{}{}", lopsided_art("Ribbon art"), plugin_object(ART_BRUSH, "Ribbon", RIBBON)); @@ -10516,21 +10616,7 @@ fn an_art_brushed_path_the_brush_cant_draw_keeps_its_drawn_look() { ), plain_style.clone(), ), - ( - "a width other than the brush's 100%", - format!("{}{}", lopsided_art("Ribbon art"), plugin_object(ART_BRUSH, "Ribbon", wide)), - art_style( - "Ribbon", - 1.0, - &STRETCHED.map(|e| if matches!(e.0, "Scaling" | "MinScaling" | "MaxScaling") { (e.0, "2", e.2) } else { e }), - "", - ), - ), - ( - "a brush that keeps its art's colors", - format!("{}{}", lopsided_art("Ribbon art"), plugin_object(ART_BRUSH, "Ribbon", own_colors)), - art_style("Ribbon", 1.0, &STRETCHED.map(|e| if e.0 == "Colorization" { (e.0, "0", e.2) } else { e }), ""), - ), + ("a brush that keeps its art's colors, the path in Tints", format!("{}{}", lopsided_art("Ribbon art"), plugin_object(ART_BRUSH, "Ribbon", own_colors)), plain_style.clone()), ("an entry that isn't read", good.clone(), art_style("Ribbon", 1.0, &STRETCHED, "1 /Int (SomeOtherSetting) ,\n")), ( "an effect on the path", @@ -10543,17 +10629,16 @@ fn an_art_brushed_path_the_brush_cant_draw_keeps_its_drawn_look() { ("proportional", good.clone(), style_with("Proportional", "1")), ("between guides", good.clone(), style_with("Segmented", "1")), ("its own width", good.clone(), style_with("Scaling", "2")), - ("a width that varies", good.clone(), style_with("MaxScaling", "2")), ("a width on the pen", good.clone(), style_with("ScaleYDependsOn", "2")), ("spacing", good.clone(), style_with("Spacing", "1")), - ("flipped", good.clone(), style_with("UpsideDown", "1")), + ("a spacing close to the brush's 0", good.clone(), style_with("Spacing", "0.0005")), + ("flipped",good.clone(), style_with("UpsideDown", "1")), ("flipped the other way", good.clone(), style_with("Counterclockwise", "1")), ("another adjustment", good.clone(), style_with("Adjustment", "2")), ("no fold adjustment", good.clone(), style_with("FoldAdjustment", "0")), ("its own colorization", good.clone(), style_with("Colorization", "0")), ("tints and shades", good.clone(), style_with("Colorization", "2")), ("hue shift", good.clone(), style_with("Colorization", "3")), - ("start of a guide", good.clone(), art_style("Ribbon", 1.0, &STRETCHED, "5 /Real (StartSegmentLength) ,\n")), ("variable width", good.clone(), art_style("Ribbon", 1.0, &STRETCHED, "2 /Int (BSWidthSource) ,\n")), ( "width arrays", @@ -10570,6 +10655,35 @@ fn an_art_brushed_path_the_brush_cant_draw_keeps_its_drawn_look() { ("art not read", plugin_object(ART_BRUSH, "Ribbon", RIBBON), plain_style.clone()), ("no such brush", good.clone(), art_style("Other", 1.0, &STRETCHED, "")), ]; + // The brush's own settings, which the path shares, but VectorCraft doesn't draw as the file does. + let own = [ + ("both brush and path: tints and shades", &[(0, "2")][..]), + ("both: hue shift", &[(0, "3")]), + ("both: spacing", &[(2, "1")]), + ("both: another adjustment", &[(3, "2")]), + ("both: no fold adjustment", &[(13, "0")]), + ("both: a width on the pen", &[(9, "1"), (7, "0.5"), (8, "2")]), + ("both: a width past the range", &[(1, "20"), (7, "20"), (8, "20")]), + ("both: a flip of another value", &[(4, "2")]), + ("both: proportional", &[(6, "1")]), + ("both: proportional and between guides", &[(6, "1"), (10, "1"), (11, "5"), (12, "10")]), + ("both: guides that leave no art between them", &[(10, "1"), (11, "30"), (12, "10")]), + ("both: a guide past the start", &[(10, "1"), (11, "-5"), (12, "10")]), + ]; + let mut cases: Vec<(&str, String, String)> = cases + .into_iter() + .chain(own.iter().map(|(why, changes)| { + let (def, style) = ribbon_with(changes); + (*why, def, style) + })) + .collect(); + // The brush's guides leave art between them, and the path's, close to them, leave none. + let (def, style) = ribbon_with(&[(10, "1"), (11, "20"), (12, "19.99")]); + cases.push(("path guides close to the brush's that leave no art between them", def, style.replace("19.99 /", "20 /"))); + // On art 0.01 long, a guide at 0.0035 is 35% of the way along it, and the brush's at 0.003 is 30%. + let (def, style) = ribbon_with(&[(10, "1"), (11, "0.003"), (12, "0.002")]); + let short = format!("%AI8_BeginBrushPattern\n(Ribbon art)\n0 A\n0 g\n{}f\n%AI8_EndBrushPattern\n", rect_path(0.0, 0.0, 0.01, 4.0)); + cases.push(("path guides elsewhere on short art", def.replace(&lopsided_art("Ribbon art"), &short), style.replace("0.003 /", "0.0035 /"))); let mut wrong = vec![]; for (why, def, style) in cases { let r = read_ok(&format!("{def}{style}{}", layer(&brushed_path()))); diff --git a/crates/eps/src/scene.rs b/crates/eps/src/scene.rs index 75a7437e1d..023dbdcbe1 100644 --- a/crates/eps/src/scene.rs +++ b/crates/eps/src/scene.rs @@ -92,6 +92,19 @@ struct ImageCode { } impl ImageCode { + /// A hash of everything that is written for the image: equal images have equal keys. + fn content_key(&self) -> u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + (&self.prelude, &self.dict, self.filter, self.op, &self.ascii).hash(&mut h); + h.finish() + } + + /// Is everything written for `other` the same as for this image? + fn same_image(&self, other: &ImageCode) -> bool { + self.prelude == other.prelude && self.dict == other.dict && self.filter == other.filter && self.op == other.op && self.ascii == other.ascii + } + /// The operators and data that draw the image where it is placed. fn push_inline(&self, s: &mut String) { s.push_str(&self.prelude); @@ -253,6 +266,9 @@ pub(crate) struct Scene<'a> { images: HashMap>, /// The source key and solid paint together identify a colorized encoding. colorized_images: HashMap<(String, String), Option>, + /// Freeform gradients' images by [`ImageCode::content_key`] → their indices in `codes`: a field + /// encoded the same as one already kept draws from it. + fields: HashMap>, /// The encoded images. codes: Vec, /// Where in `body` each placement of an encoded image goes (in order) and the index of its code. @@ -281,6 +297,7 @@ impl<'a> Scene<'a> { brushes: None, images: HashMap::new(), colorized_images: HashMap::new(), + fields: HashMap::new(), codes: vec![], placements: vec![], pending: 0, @@ -478,7 +495,7 @@ impl<'a> Scene<'a> { _ => {} } self.note_losses(n); - let (was, placed) = (self.body.len(), self.placements.len()); + let (was, placed, codes) = (self.body.len(), self.placements.len(), self.codes.len()); match &n.kind { NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => { children.iter().for_each(|c| self.node(c, false)); @@ -536,6 +553,7 @@ impl<'a> Scene<'a> { if self.size() > self.max_body { self.body.truncate(was); self.unplace(placed); + self.forget_fields(codes); self.warn(TOO_LARGE); } } @@ -1364,6 +1382,14 @@ impl<'a> Scene<'a> { if !(long > 1e-9 && long.is_finite()) { return; } + let place = Affine::translate(area.origin().to_vec2()) * Affine::scale_non_uniform(area.width(), area.height()); + if !invertible(place) { + return; + } + if self.size().saturating_add(64) > self.max_body { + self.warn(TOO_LARGE); + return; + } let k = (self.doc.raster_effects_ppi / 72.0).min(MAX_FIELD_PX / long); let along = |len: f64| (len * k).ceil().clamp(1.0, MAX_FIELD_PX) as u16; let (cols, rows) = (along(area.width()), along(area.height())); @@ -1383,9 +1409,43 @@ impl<'a> Scene<'a> { let rgba: Vec = field.display_grid(area, cols, rows).flat_map(|(c, a)| [q(c[0]), q(c[1]), q(c[2]), q(a)]).collect(); self.encode_image(&rgba, w, h) }; - let place = Affine::translate(area.origin().to_vec2()) * Affine::scale_non_uniform(area.width(), area.height()); - if let Some(image) = image { - self.place_inline(&image, place); + let Some(image) = image else { return }; + let key = image.content_key(); + let found = + self.fields.get(&key).and_then(|bucket| bucket.iter().copied().find(|&i| self.codes.get(i).is_some_and(|c| c.same_image(&image)))); + let i = match found { + Some(i) => i, + None => { + // A field that can't be placed isn't kept. + if self.size().saturating_add(image.inline_len).saturating_add(64) > self.max_body { + self.warn(TOO_LARGE); + return; + } + let i = self.add_code(image); + self.fields.entry(key).or_default().push(i); + i + } + }; + self.place_image(i, place); + } + + /// Forget the field images after the first `n` codes that no placement draws any longer, last + /// first, up to the first code that is drawn or isn't a field image. + fn forget_fields(&mut self, n: usize) { + while self.codes.len() > n { + let i = self.codes.len() - 1; + let Some(code) = self.codes.get(i) else { break }; + if code.uses > 0 { + break; + } + let key = code.content_key(); + let Some(bucket) = self.fields.get_mut(&key) else { break }; + let Some(at) = bucket.iter().position(|&j| j == i) else { break }; + bucket.swap_remove(at); + if bucket.is_empty() { + self.fields.remove(&key); + } + self.codes.pop(); } } @@ -1595,21 +1655,6 @@ impl<'a> Scene<'a> { self.line("Q"); } - /// Draw `code`, an image drawn nowhere else, on the unit square mapped by `place`. - fn place_inline(&mut self, code: &ImageCode, place: Affine) { - if !invertible(place) { - return; - } - if self.size().saturating_add(code.inline_len).saturating_add(64) > self.max_body { - self.warn(TOO_LARGE); - return; - } - self.line("q"); - let _ = writeln!(self.body, "{} cm", ps::matrix(place)); - code.push_inline(&mut self.body); - self.line("Q"); - } - /// Straight RGBA pixels (`w` × `h`, rows from the top) as the operators and data that draw /// them on the unit square: in the process colour space, transparent pixels masked out at /// Level 3 (a key colour no opaque pixel has), white at Level 2; partly transparent ones over diff --git a/crates/eps/src/tests.rs b/crates/eps/src/tests.rs index 2ab96d3555..e9d6f79521 100644 --- a/crates/eps/src/tests.rs +++ b/crates/eps/src/tests.rs @@ -1264,6 +1264,104 @@ fn the_page_limit_counts_a_repeated_images_samples_once() { assert!(page.body.len() + page.setup.len() <= inline + 1000 + 400, "{}", page.body.len()); } +/// A freeform gradient of `points` (x, y and color). +fn freeform(points: &[(f64, f64, Color)]) -> Paint { + let mut g = GradientPaint::new(Gradient { kind: GradientKind::Freeform, ..Default::default() }); + g.freeform = Some(Freeform { points: points.iter().map(|&(x, y, c)| FreeformPoint::new(Point::new(x, y), c)).collect(), ..Default::default() }); + Paint::Gradient(Box::new(g)) +} + +/// A square at (`x`, `y`) of side `side`, filled with a freeform gradient of `a` and `b` at a +/// fifth and four fifths of its width. +fn freeform_square(d: &mut Document, x: f64, y: f64, side: f64, a: Color, b: Color) -> Node { + let mid = y + side / 2.0; + let paint = freeform(&[(x + side / 5.0, mid, a), (x + side * 0.8, mid, b)]); + path(d, shapes::rectangle(Rect::new(x, y, x + side, y + side)), paint, Paint::None, 0.0) +} + +/// A freeform gradient on a shape and on a moved copy of it is sampled the same for both: its +/// image's samples are written once, at both levels, in RGB and in CMYK. +#[test] +fn a_freeform_gradients_image_drawn_twice_writes_its_samples_once() { + let rgb = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)); + let cmyk = (Color::cmyk(1.0, 0.0, 0.0, 0.0), Color::cmyk(0.0, 0.0, 0.0, 1.0)); + for (a, b) in [rgb, cmyk] { + let single = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, a, b)]); + let twice = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, a, b), freeform_square(d, 64.0, 64.0, 64.0, a, b)]); + for level in Level::ALL { + let one = export(&single, &EpsOptions { level, ..opts(&single) }, None, None).unwrap(); + let one_ps = text(&one); + assert!(!one_ps.contains("VCim"), "a field drawn once is written where it is drawn: {one_ps}"); + let out = export(&twice, &EpsOptions { level, ..opts(&twice) }, None, None).unwrap(); + let ps = text(&out); + checked_page(&ps); + assert_eq!(reuses(&ps, 0, level), 2, "{ps}"); + assert!(!ps.contains("VCim1"), "{ps}"); + let body = &ps[ps.find("%%EndSetup").unwrap()..]; + assert!(!body.contains("currentfile") && !body.contains("~>"), "no samples in the page: {ps}"); + assert_eq!(reused_samples(&ps, level), inline_samples(&one_ps, level), "{level:?}"); + assert_eq!(out.warnings, one.warnings); + } + } +} + +/// Fields sampled differently are each written where they are drawn: different gradients, and one +/// gradient on shapes of different sizes. +#[test] +fn freeform_gradients_sampled_differently_dont_share_an_image() { + let (red, blue, green) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0), Color::rgb(0.0, 1.0, 0.0)); + let colors = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, green)]); + let sizes = doc_with(|d| { + let paint = freeform(&[(20.0, 50.0, red), (80.0, 50.0, blue)]); + vec![ + path(d, shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), paint.clone(), Paint::None, 0.0), + path(d, shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 100.0)), paint, Paint::None, 0.0), + ] + }); + for d in [colors, sizes] { + for level in Level::ALL { + let ps = page_of(&d, &EpsOptions { level, ..opts(&d) }); + assert!(!ps.contains("VCim"), "{ps}"); + assert_eq!(ps.matches("} exec\n").count(), 2, "{ps}"); + } + } +} + +/// A field's second placement that passes the page limit is left out, and the first is written +/// where it is drawn. A field taken back with the shape that passed the limit leaves nothing in the +/// setup. +#[test] +fn a_field_taken_back_at_the_page_limit_leaves_no_image_behind() { + let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)); + let single = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, red, blue)]); + let o = opts(&single); + let inline = crate::scene::Scene::new(&single, &o).run().body; + let twice = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, blue)]); + let page = crate::scene::Scene::new(&twice, &o).with_max_body(inline.len() + 50).run(); + assert!(page.warnings.iter().any(|w| w.contains("too large")), "{:?}", page.warnings); + assert!(page.body.starts_with(&inline), "{}", page.body); + assert_eq!(page.body.matches("} exec\n").count(), 1, "{}", page.body); + assert!(!page.setup.contains("VCim"), "{}", page.setup); + // A many-sided shape whose stroke passes the limit after its fill's field was placed. + let big = |d: &mut Document, stroke: Paint| { + let paint = freeform(&[(60.0, 100.0, Color::rgb(0.0, 1.0, 0.0)), (140.0, 100.0, red)]); + path(d, shapes::polygon(Point::new(100.0, 100.0), 90.0, 2000, 0.0), paint, stroke, 1.0) + }; + let filled = doc_with(|d| vec![big(d, Paint::None)]); + let stroked = doc_with(|d| vec![big(d, Paint::solid(Color::rgb(0.0, 0.0, 0.0)))]); + let fill = crate::scene::Scene::new(&filled, &o).run().body.len(); + let both = crate::scene::Scene::new(&stroked, &o).run().body.len(); + assert!(both > fill + 20_000, "{fill} {both}"); + let d = doc_with(|d| { + let s = big(d, Paint::solid(Color::rgb(0.0, 0.0, 0.0))); + vec![s, freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, blue)] + }); + let page = crate::scene::Scene::new(&d, &o).with_max_body(fill + (both - fill) / 2).run(); + assert!(page.warnings.iter().any(|w| w.contains("too large")), "{:?}", page.warnings); + assert_eq!(reuses(&page.body, 0, Level::Three), 2, "{}", page.body); + assert!(page.setup.contains("/VCim0 VCsrc") && !page.setup.contains("VCim1"), "{}", page.setup); +} + #[test] fn run_length_and_ascii85_round_trip() { let cycle: Vec = (0..=255u8).cycle().take(1000).collect(); diff --git a/crates/pathops/src/sweep_axis.rs b/crates/pathops/src/sweep_axis.rs index fb544ad163..8940515dcc 100644 --- a/crates/pathops/src/sweep_axis.rs +++ b/crates/pathops/src/sweep_axis.rs @@ -259,8 +259,9 @@ mod tests { #[test] fn offset_path_sweeps_a_long_low_path_turned_with_the_result_of_the_path_on_end() { let mut rng = Rng(17); - // Grown past the zigzag's bends, where the stroker leaves loops whose faces are dropped by - // the distance test; grown a little; and inset. + // An open zigzag grown by 1.5, a closed one grown by 0.5 and a closed one inset by 0.3. Each + // is swept turned when low and as it is when stood on end, and the two results cover the + // same region. None of the three leaves a face for the distance test to drop. for (closed, delta) in [(false, 1.5), (true, 0.5), (true, -0.3)] { let what = format!("closed {closed}, offset {delta}"); let low = zigzag(3, 300, 0.4, &mut rng, closed); @@ -274,7 +275,8 @@ mod tests { assert_same_region(&turned, &upright(&on_end), &what); } // A long plate with a row of round holes: growing it by 1 closes the narrow holes, which - // the distance test drops, and keeps the wide ones. + // the distance test drops when it measures each hole's point turned back into the plate's + // own frame, and keeps the wide ones. use kurbo::Shape; let mut plate = kurbo::Rect::new(0.0, 0.0, 4000.0, 20.0).to_path(0.1); for i in 0..200u32 { diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 4ba4aa123d..4ba361d29b 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -46,7 +46,7 @@ use exporter::{MAX_COORD, writable, xf}; #[cfg(test)] use images::{png_8_bit, png_16_bit}; #[cfg(test)] -pub(crate) use nodes::MEASURES; +pub(crate) use nodes::{KNOCKOUT_BRUSHES, KNOCKOUT_SHAPES, MAX_KNOCKOUT_MASK_OBJECTS, MEASURES}; pub(crate) use writer::{Sheet, Writer}; pub(crate) struct Exporter<'a> { @@ -127,6 +127,14 @@ pub(crate) struct Exporter<'a> { /// The most [`Self::symbol_nodes`] may reach: [`vectorcraft_doc::MAX_SYMBOL_NODES`], or less /// while one of the copies on a sheet is drawn ([`Writer::page`]). pub(crate) symbol_limit: u64, + /// Objects the symbol instances in the copies drawn in knockout masks held, together: counted + /// apart from [`Self::symbol_nodes`], within [`vectorcraft_doc::MAX_SYMBOL_NODES`] of their own. + mask_symbol_nodes: u64, + /// Whether a knockout mask is being drawn: [`Self::symbol_nodes`] then counts + /// [`Self::mask_symbol_nodes`]. + in_knockout_mask: bool, + /// Whether a knockout mask left out symbol art because the objects its copies may hold ran out. + mask_art_left_out: bool, } #[cfg(test)] diff --git a/crates/pdf/src/export/exporter.rs b/crates/pdf/src/export/exporter.rs index 22109b3e82..f102e0a0ab 100644 --- a/crates/pdf/src/export/exporter.rs +++ b/crates/pdf/src/export/exporter.rs @@ -54,6 +54,9 @@ impl<'a> Exporter<'a> { paper: Rect::ZERO, symbol_nodes: 0, symbol_limit: vectorcraft_doc::MAX_SYMBOL_NODES, + mask_symbol_nodes: 0, + in_knockout_mask: false, + mask_art_left_out: false, overprint: false, overprinted: false, invisible: false, diff --git a/crates/pdf/src/export/nodes.rs b/crates/pdf/src/export/nodes.rs index 097ae52bba..78930c7500 100644 --- a/crates/pdf/src/export/nodes.rs +++ b/crates/pdf/src/export/nodes.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; + use super::exporter::{UNDECODED, address, blend, color, constant_mask, cover, in_range, norm, rects_overlap, rule, to_path, writable, xf}; use super::{ - Affine, AppearanceItem, BezPath, BlendMode, Exporter, Fill, FillRule, GradientKind, LineCap, LineJoin, Mark, Node, NodeKind, NormalizedF32, - Paint, Path, Rect, Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke, + Affine, AppearanceItem, BezPath, BlendMode, Document, Exporter, Fill, FillRule, GradientKind, LineCap, LineJoin, Mark, Node, NodeKind, + NormalizedF32, Paint, Path, Rect, Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke, }; /// An object's visual box for leaving out what is off the page. A plain group or layer also holds @@ -57,88 +59,146 @@ impl Exporter<'_> { /// The children of a group, as the elements of a knockout group when it is one. pub(super) fn children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect) { - self.measured_children(s, children, page, &[]); + self.measured_children(s, children, page, &[], false); } - /// [`Self::children`] with the boxes of the last members in `measured`. - fn measured_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured]) { + /// [`Self::children`] with the boxes of the last members in `measured`, in an isolated group + /// when `isolated`. + fn measured_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured], isolated: bool) { if !self.knockout { + if isolated { + s.push_isolated(); + } let unmeasured = children.len().saturating_sub(measured.len()); for (i, c) in children.iter().enumerate() { self.measured_node(s, c, page, false, i.checked_sub(unmeasured).and_then(|i| measured.get(i))); } + if isolated { + s.pop(); + } return; } - let elements: Vec<_> = Node::knockout_elements(children) + let elements: Vec<(&Node, Measured)> = Node::knockout_elements(children) .into_iter() - .map(|c| (c, self.measure(c))) + .map(|c| (&**c, self.measure(c))) .filter(|(_, m)| m.bounds.is_some_and(|b| rects_overlap(b.inflate(1.0, 1.0), page))) .collect(); - if !elements.is_empty() && elements.iter().all(|(c, _)| c.knockout_outline().is_some()) { - let pieces: Vec<&Node> = elements.iter().map(|(c, _)| &***c).collect(); - if vectorcraft_effects::warp_pieces_translucent(self.doc, &pieces) { - return self.warp_pieces(s, &elements, page); + if elements.is_empty() { + return; + } + if elements.iter().all(|(c, _)| c.knockout_outline().is_some()) { + let pieces: Vec<&Node> = elements.iter().map(|(c, _)| *c).collect(); + if isolated { + s.push_isolated(); } - for (c, m) in &elements { - self.measured_node(s, c, page, false, Some(m)); + if vectorcraft_effects::warp_pieces_translucent(self.doc, &pieces) { + self.warp_pieces(s, &elements, page); + } else { + for (c, m) in &elements { + self.measured_node(s, c, page, false, Some(m)); + } + } + if isolated { + s.pop(); } return; } - // The PDF writer has no knockout groups: each element is drawn through a soft mask of - // where the elements above it don't paint (the same look for Normal blending). The masks - // nest, so element i sits inside the masks of elements i+1…n: open them outermost first. - self.warn("knockout groups are written as soft-masked groups (same look, but not editable as knockout groups)"); - let Some(((first, measured), rest)) = elements.split_first() else { return }; - for (c, m) in rest.iter().rev() { - let mask = self.knockout_mask(s, c, page, m); - s.push_mask(mask); + let several = elements.len() > 1; + // A PDF knockout group (`/K`) knocks out by the area each element paints. Where an element + // above another knocks out by less (its opacity or mask defines its knockout shape, or it + // paints translucently), each element is drawn through one soft mask of where the + // elements above it that overlap it paint, while those masks hold no more than + // [`MAX_KNOCKOUT_MASK_OBJECTS`] objects and no brushed stroke. + if several && !elements.iter().skip(1).all(|(c, _)| shape_is_coverage(c)) { + let doc = self.doc; + let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc)); + match knockout_masks(BrushSearch::new(doc, brushes), &elements) { + Ok(above) => { + // Every mask is made before anything is drawn: when the symbol objects the + // masks may hold run out, one would leave out art its element covers, and + // the group is written as a PDF knockout group instead. + let outer = std::mem::take(&mut self.mask_art_left_out); + let before = *self.mask_counter(); + let mut masks = Vec::with_capacity(above.len()); + for above in above { + let shapes: Vec<_> = above.into_iter().map(|j| &elements[j]).collect(); + masks.push((!shapes.is_empty()).then(|| self.knockout_mask(s, &shapes, page))); + } + let left_out = std::mem::replace(&mut self.mask_art_left_out, outer); + if left_out { + *self.mask_counter() = before; + self.warn(KNOCKOUT_SHAPES); + } else { + if isolated { + s.push_isolated(); + } + for ((c, m), mask) in elements.iter().zip(masks) { + if let Some(mask) = mask { + s.push_mask(mask); + self.measured_node(s, c, page, false, Some(m)); + s.pop(); + } else { + self.measured_node(s, c, page, false, Some(m)); + } + } + if isolated { + s.pop(); + } + return; + } + } + Err(warning) => self.warn(warning), + } } - self.measured_node(s, first, page, false, Some(measured)); - for (c, m) in rest { - s.pop(); + let group = several || isolated; + if group { + s.push_group(krilla::blend::GroupOptions { isolated, knockout: several }); + } + for (c, m) in &elements { + // In a PDF knockout group each painting operation knocks out the ones before it. An + // element drawn with several is one non-isolated group (drawn through a full alpha + // mask), so that its own fills and strokes don't knock each other out; its opacity + // or opacity mask makes it one already. + let unit = several && !one_paint(c) && c.opacity >= 1.0 && c.extras().mask.as_deref().is_none_or(|m| m.disabled); + if unit { + let all = constant_mask(s, page, 1.0); + s.push_mask(all); + } self.measured_node(s, c, page, false, Some(m)); + if unit { + s.pop(); + } + } + if group { + s.pop(); } } /// [`Self::measured_children`] of a plain group or layer. When they are the pieces of a /// mesh-warped image, every piece, on the page or not, takes part in the one size its pixels /// are resampled for ([`Self::warp_image_size`]). - fn group_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured]) { + fn group_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured], isolated: bool) { let outer = self.warp_image_size(children).map(|w| self.warp_size.replace(w)); - self.measured_children(s, children, page, measured); + self.measured_children(s, children, page, measured, isolated); if let Some(outer) = outer { self.warp_size = outer; } } - /// A luminosity mask that is 1 − the knockout shape of `c`, whose box is in `measured`: white, - /// then black through an alpha mask of `c` (at full object opacity without its own mask, unless - /// those define its shape). - fn knockout_mask(&mut self, s: &mut Surface, c: &Node, page: Rect, measured: &Measured) -> krilla::mask::Mask { - let shape = if c.knockout_shape { - c.clone() - } else { - let mut s = c.clone().with_mask(None); - s.opacity = 1.0; - s - }; - self.shapes_mask(s, &[(&shape, Some(measured))], page) - } - /// The pieces of a partly transparent mesh-warped image (`elements`, each with its box) as the /// elements of a knockout group ([`vectorcraft_effects::warp_pieces_translucent`]). Each piece /// is drawn through one soft mask of where the pieces above it that reach its box don't cover /// ([`vectorcraft_effects::knockout_silhouette`]): only neighbors overlap, so each mask holds a /// few triangles and no mask nests in another. A piece above that overlaps it only over opaque /// pixels ([`vectorcraft_effects::WarpSeams`]) is left out of its mask. - fn warp_pieces(&mut self, s: &mut Surface, elements: &[(&std::sync::Arc, Measured)], page: Rect) { + fn warp_pieces(&mut self, s: &mut Surface, elements: &[(&Node, Measured)], page: Rect) { // Each piece's triangle and its box: a piece paints inside its triangle only. let silhouette = |c: &Node| { let b = c.knockout_outline()?.geometric_bounds()?; Some((vectorcraft_effects::knockout_silhouette(c, vectorcraft_doc::color::Color::WHITE)?, b)) }; let silhouettes: Vec> = elements.iter().map(|(c, _)| silhouette(c)).collect(); - let pieces: Vec<&Node> = elements.iter().map(|(c, _)| &***c).collect(); + let pieces: Vec<&Node> = elements.iter().map(|(c, _)| *c).collect(); let seams = vectorcraft_effects::WarpSeams::new(self.doc, &pieces); for (i, (c, measured)) in elements.iter().enumerate() { let Some((_, reach)) = &silhouettes[i] else { continue }; @@ -191,6 +251,67 @@ impl Exporter<'_> { krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity) } + /// A luminosity mask that is 1 − the knockout shapes of `shapes` (each with its box): white, + /// then black through an alpha mask of each (at full object opacity without its own mask, + /// unless those define its shape). The symbol instances in the copies count toward + /// [`Exporter::mask_symbol_nodes`], not toward the objects the visible art may draw. A + /// knockout mask drawn inside another one counts toward the same objects as the outer one. + fn knockout_mask(&mut self, s: &mut Surface, shapes: &[&(&Node, Measured)], page: Rect) -> krilla::mask::Mask { + if self.in_knockout_mask { + return self.knockout_mask_art(s, shapes, page); + } + self.in_knockout_mask = true; + let visible = std::mem::replace(&mut self.symbol_nodes, self.mask_symbol_nodes); + let limit = std::mem::replace(&mut self.symbol_limit, vectorcraft_doc::MAX_SYMBOL_NODES); + let mask = self.knockout_mask_art(s, shapes, page); + self.mask_symbol_nodes = std::mem::replace(&mut self.symbol_nodes, visible); + self.symbol_limit = limit; + self.in_knockout_mask = false; + mask + } + + /// The symbol objects the copies drawn in knockout masks held, together: [`Self::symbol_nodes`] + /// while a knockout mask is being drawn, [`Exporter::mask_symbol_nodes`] otherwise. + fn mask_counter(&mut self) -> &mut u64 { + if self.in_knockout_mask { &mut self.symbol_nodes } else { &mut self.mask_symbol_nodes } + } + + /// [`Self::knockout_mask`], drawn within the symbol objects counted at the time. + fn knockout_mask_art(&mut self, s: &mut Surface, shapes: &[&(&Node, Measured)], page: Rect) -> krilla::mask::Mask { + let transform = s.group_transform(); + let mut sb = s.stream_builder(); + let mut ms = Surface::stream(sb.surface()); + ms.push_transform(&transform); + cover(&mut ms, page, 255, 1.0); + for (c, measured) in shapes.iter().map(|e| (e.0, &e.1)) { + let shape = if c.knockout_shape { + c.clone() + } else { + let mut s = c.clone().with_mask(None); + s.opacity = 1.0; + s + }; + let alpha = { + let transform = ms.group_transform(); + let mut ab = ms.stream_builder(); + let mut als = Surface::stream(ab.surface()); + als.push_transform(&transform); + let overprint = std::mem::take(&mut self.overprint); + self.measured_node(&mut als, &shape, page, false, Some(measured)); + self.overprint = overprint; + als.pop(); + als.finish(); + krilla::mask::Mask::new(ab.finish(), krilla::mask::MaskType::Alpha) + }; + ms.push_mask(alpha); + cover(&mut ms, page, 0, 1.0); + ms.pop(); + } + ms.pop(); + ms.finish(); + krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity) + } + /// The box of `n`, and of the members of a plain group or layer. fn measure(&self, n: &Node) -> Measured { if !matches!(n.kind, NodeKind::Layer { clip: false, .. } | NodeKind::Group { clip: false, .. }) { @@ -242,23 +363,7 @@ impl Exporter<'_> { // Whether this container's children knock each other out (a knockout group is written as a group). let knockout = n.knocks_out(self.knockout); let enclosing = std::mem::replace(&mut self.knockout, knockout); - let mut pushes = 0; - if n.blend != BlendMode::Normal { - s.push_blend_mode(blend(n.blend)); - pushes += 1; - } - if n.opacity < 1.0 || (container && (n.isolate || n.blend != BlendMode::Normal || knockout)) { - if !n.isolate && n.blends_through() { - // Not isolated: blending inside reaches the art below the group. - let alpha = constant_mask(s, page, n.opacity); - s.push_mask(alpha); - } else if n.opacity < 1.0 { - s.push_opacity(norm(n.opacity)); - } else { - s.push_isolated(); - } - pushes += 1; - } + let (mut pushes, isolated_children) = transparency(s, n, page, container, knockout); let mask = n.extras().mask.as_deref().filter(|m| !m.disabled); let alpha = match (&n.kind, mask) { (NodeKind::Image(im), Some(m)) => self.masked_image(im, m, !n.extras().pixelated), @@ -279,7 +384,7 @@ impl Exporter<'_> { } match &n.kind { NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => { - self.group_children(s, children, page, measured.map_or(&[], |m| m.members.as_slice())); + self.group_children(s, children, page, measured.map_or(&[], |m| m.members.as_slice()), isolated_children); } NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => { // The region every output clips to; nothing to clip by hides the clipped art. The @@ -366,8 +471,10 @@ impl Exporter<'_> { (art, count) }) }); - if let Some(art) = placed.flatten() { - self.node(s, &art, page, true); + match placed.flatten() { + Some(art) => self.node(s, &art, page, true), + // A knockout mask that leaves the art out no longer matches what it masks. + None => self.mask_art_left_out |= self.in_knockout_mask, } } } @@ -741,3 +848,379 @@ impl Exporter<'_> { true } } + +/// Opens the blend mode and the transparency group `n` is drawn with (`container` when it is a +/// group or layer whose children `knockout` each other). Returns how many were opened, and +/// whether the children are to be drawn in an isolated group of their own: the knockout group of +/// an unclipped group's elements is its isolated group too. +fn transparency(s: &mut Surface, n: &Node, page: Rect, container: bool, knockout: bool) -> (usize, bool) { + let mut pushes = 0; + if n.blend != BlendMode::Normal { + s.push_blend_mode(blend(n.blend)); + pushes += 1; + } + let mut isolated_children = false; + if n.opacity < 1.0 || (container && (n.isolate || n.blend != BlendMode::Normal || knockout)) { + if !n.isolate && n.blends_through() { + // Not isolated: blending inside reaches the art below the group. + let alpha = constant_mask(s, page, n.opacity); + s.push_mask(alpha); + pushes += 1; + } else if n.opacity < 1.0 { + s.push_opacity(norm(n.opacity)); + pushes += 1; + } else if knockout && matches!(n.kind, NodeKind::Layer { clip: false, .. } | NodeKind::Group { clip: false, .. }) { + isolated_children = true; + } else { + s.push_isolated(); + pushes += 1; + } + } + (pushes, isolated_children) +} + +/// The warning for a knockout group whose elements are too many to draw through soft masks: it is +/// written as a PDF knockout group, where an element knocks out all of the area it paints. +pub(crate) const KNOCKOUT_SHAPES: &str = "knockout groups with too many overlapping translucent elements are written as PDF knockout groups: \ + their translucent elements knock out what they cover fully"; + +/// The most objects the soft masks of one knockout group's elements draw between them: each mask +/// draws the elements above its own element that overlap it. +pub(crate) const MAX_KNOCKOUT_MASK_OBJECTS: usize = 1 << 16; + +/// The most elements of one knockout group drawn through soft masks: finding the elements that +/// overlap compares every pair. +pub(crate) const MAX_MASKED_ELEMENTS: usize = 2048; + +/// The warning for a knockout group in which a soft mask would draw an element with a brushed +/// stroke: it is written as a PDF knockout group, where an element knocks out all of the area it +/// paints. +pub(crate) const KNOCKOUT_BRUSHES: &str = "knockout groups with brushed strokes over other translucent elements are written as PDF knockout groups: \ + their translucent elements knock out what they cover fully"; + +/// For each element of a knockout group (with its box), the indices of the elements above it whose +/// boxes overlap its own. The warning to give instead ([`KNOCKOUT_SHAPES`]) when there are more than +/// [`MAX_MASKED_ELEMENTS`] elements or the masks would draw more than +/// [`MAX_KNOCKOUT_MASK_OBJECTS`] objects, or ([`KNOCKOUT_BRUSHES`]) when a mask would draw a +/// brushed stroke ([`BrushSearch`]), whose brush art the object count leaves out. +fn knockout_masks(mut search: BrushSearch, elements: &[(&Node, Measured)]) -> Result>, &'static str> { + if elements.len() > MAX_MASKED_ELEMENTS { + return Err(KNOCKOUT_SHAPES); + } + let counts: Vec = elements.iter().map(|(c, _)| c.count_with_masks()).collect(); + let mut checked: Vec> = vec![None; elements.len()]; + let mut objects = 0usize; + let mut out = Vec::with_capacity(elements.len()); + for (i, (_, below)) in elements.iter().enumerate() { + let mut above = vec![]; + for (j, (over_node, over)) in elements.iter().enumerate().skip(i + 1) { + if let (Some(a), Some(b)) = (below.bounds, over.bounds) + && rects_overlap(a.inflate(1.0, 1.0), b.inflate(1.0, 1.0)) + { + // The mask draws a copy of the element without its own opacity mask, unless the + // mask defines its knockout shape. + if *checked[j].get_or_insert_with(|| search.node(over_node, over_node.knockout_shape, 0)) { + return Err(if search.exhausted { KNOCKOUT_SHAPES } else { KNOCKOUT_BRUSHES }); + } + objects = objects.checked_add(counts[j]).filter(|&o| o <= MAX_KNOCKOUT_MASK_OBJECTS).ok_or(KNOCKOUT_SHAPES)?; + above.push(j); + } + } + out.push(above); + } + Ok(out) +} + +/// The art a [`BrushSearch`] looks through once: a symbol's or a pattern's, by name, or a +/// symbol's with one instance's appearance changes, by the symbol's name and the changes. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +enum SharedArt { + Symbol, + Pattern, + Dynamic, +} + +/// A search for the brushed strokes drawing an object draws, whose brush art can hold many more +/// objects than the stroked path. +struct BrushSearch<'a> { + doc: &'a Document, + /// The brushes the export draws strokes with ([`Exporter::brush_art`]); a stroke whose brush + /// is not among them is drawn as a plain stroke. + brushes: &'a [vectorcraft_brush::Brush], + /// The answer for each symbol's and pattern's art, and each symbol's art with an instance's + /// appearance changes, once found: false while it is being looked through, so that art that + /// uses itself ends the search. + seen: HashMap<(SharedArt, String), bool>, + /// The objects the search may still visit or copy: [`MAX_KNOCKOUT_MASK_OBJECTS`] at the start. + left: usize, + /// Whether the search stopped because [`Self::left`] ran out; it then answers true. + exhausted: bool, +} + +impl<'a> BrushSearch<'a> { + fn new(doc: &'a Document, brushes: &'a [vectorcraft_brush::Brush]) -> Self { + Self { doc, brushes, seen: HashMap::new(), left: MAX_KNOCKOUT_MASK_OBJECTS, exhausted: false } + } + + /// Takes `n` objects from [`Self::left`]; false, and the search exhausted, when fewer are left. + fn spend(&mut self, n: usize) -> bool { + match self.left.checked_sub(n) { + Some(left) if !self.exhausted => { + self.left = left; + true + } + _ => { + (self.left, self.exhausted) = (0, true); + false + } + } + } + + /// Takes the objects of `n` (its members and opacity-mask art included) from [`Self::left`], + /// counting no further than what is left; false when they don't fit. + fn spend_tree(&mut self, n: &Node) -> bool { + self.spend(1) + && n.extras().mask.as_deref().is_none_or(|m| self.spend_tree(&m.art)) + && n.children().into_iter().flatten().all(|c| self.spend_tree(c)) + } + + /// Whether drawing `n` draws a brushed stroke: on `n` or its visible members (a compound + /// path's members are drawn with its own paint), in the art of the opacity masks that are on + /// (`n`'s own when `own_mask`), of symbol instances and inline graphics, of live objects as + /// evaluated, and of the patterns that fills, strokes and type paint with. `depth` counts the + /// objects and the mask, symbol and pattern art above `n`: past + /// [`vectorcraft_doc::depth::MAX_DRAWN_DEPTH`] the search stops and counts as a brush, and so + /// it does when the objects it may visit or copy ([`Self::left`]) run out. + fn node(&mut self, n: &Node, own_mask: bool, depth: usize) -> bool { + if depth > vectorcraft_doc::depth::MAX_DRAWN_DEPTH || !self.spend(1) { + return true; + } + let depth = depth + 1; + // A fill is drawn when it is visible, and a stroke when it also paints and has a width + // (as `Exporter::shape` draws them). + for item in n.appearance.items.iter().filter(|i| i.visible()) { + let paint = match item { + AppearanceItem::Fill(f) => &f.paint, + AppearanceItem::Stroke(st) if st.paint.is_none() || st.width <= 0.0 => continue, + AppearanceItem::Stroke(st) => { + if st.brush.as_deref().is_some_and(|name| self.brushes.iter().any(|b| b.name == name)) { + return true; + } + &st.paint + } + }; + if self.paint(paint, depth) { + return true; + } + } + if own_mask && n.extras().mask.as_deref().is_some_and(|m| !m.disabled && self.node(&m.art, true, depth)) { + return true; + } + match &n.kind { + NodeKind::SymbolInstance { symbol, .. } => { + // A dynamic symbol's instance draws its art with its own appearance changes: the + // art is copied once for each set of changes, counted before it is copied. + let doc = self.doc; + match doc.symbols.iter().find(|s| &s.name == symbol) { + Some(s) if !n.extras().symbol_overrides.is_empty() => { + let key = format!("{symbol}\u{0}{:?}", n.extras().symbol_overrides); + self.shared(SharedArt::Dynamic, &key, |search| { + !search.spend_tree(&s.art) || search.node(&vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n), true, depth) + }) + } + _ => self.symbol(symbol, depth), + } + } + NodeKind::Text(t) => t.runs.iter().any(|r| { + let stroke = r.style.stroke_width > 0.0 && self.paint(&r.style.stroke, depth); + stroke || self.paint(&r.style.fill, depth) || r.inline.as_ref().is_some_and(|ig| self.symbol(&ig.symbol, depth)) + }), + NodeKind::Compound { .. } => false, + // Live objects are drawn as their evaluated art, as `Exporter::measured_node` draws them. + NodeKind::Blend { .. } + | NodeKind::Envelope { .. } + | NodeKind::Mesh(_) + | NodeKind::Repeat(_) + | NodeKind::PlacedDocument(_) + | NodeKind::Intertwine { .. } => { + // Each live object is reached once in the art searched (shared art is searched + // once); its evaluated art is counted before it is searched. + let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n); + !self.spend_tree(&g) || g.children().into_iter().flatten().filter(|c| c.visible).any(|c| self.node(c, true, depth)) + } + _ => n.children().into_iter().flatten().filter(|c| c.visible).any(|c| self.node(c, true, depth)), + } + } + + /// Whether the art of the symbol named `name` draws a brushed stroke. + fn symbol(&mut self, name: &str, depth: usize) -> bool { + let doc = self.doc; + let Some(s) = doc.symbols.iter().find(|s| s.name == name) else { return false }; + self.shared(SharedArt::Symbol, name, |search| search.node(&s.art, true, depth)) + } + + /// Whether `paint` is a pattern whose tile art draws a brushed stroke. + fn paint(&mut self, paint: &Paint, depth: usize) -> bool { + let Paint::Pattern { pattern, .. } = paint else { return false }; + let doc = self.doc; + let Some(def) = doc.pattern(pattern) else { return false }; + self.shared(SharedArt::Pattern, pattern, |search| def.art.iter().filter(|c| c.visible).any(|c| search.node(c, true, depth))) + } + + /// The answer of `search` for the shared art `kind` named `name`, found once. + fn shared(&mut self, kind: SharedArt, name: &str, search: impl FnOnce(&mut Self) -> bool) -> bool { + let key = (kind, name.to_string()); + if let Some(&b) = self.seen.get(&key) { + return b; + } + self.seen.insert(key.clone(), false); + let b = search(self); + self.seen.insert(key, b); + b + } +} + +/// Whether `n` is drawn with one painting operation at most: a path or compound path with at most +/// one visible fill or stroke that paints, in a solid color, without effects, a brush or +/// arrowheads. +fn one_paint(n: &Node) -> bool { + let mut items = n.appearance.items.iter().filter(|i| i.visible() && !matches!(i.paint(), Paint::None)); + let simple = items.next().is_none_or(|i| { + let plain = match i { + AppearanceItem::Fill(_) => true, + AppearanceItem::Stroke(st) => st.brush.is_none() && st.start_arrow.is_none() && st.end_arrow.is_none(), + }; + plain && i.effects().is_empty() && matches!(i.paint(), Paint::Solid { .. }) + }); + matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) && n.appearance.effects.is_empty() && simple && items.next().is_none() +} + +/// Whether element `c` of a knockout group knocks out all of the area it paints, as an element of +/// a PDF knockout group does: its opacity and opacity mask are full or don't define its knockout +/// shape, and every part of it paints opaquely. +fn shape_is_coverage(c: &Node) -> bool { + let own = !c.knockout_shape || (c.opacity >= 1.0 && c.extras().mask.as_deref().is_none_or(|m| m.disabled)); + own && paints_opaque(c) +} + +/// Whether what `n` paints is opaque wherever it paints, at its own opacity and mask: its visible +/// fills and strokes are solid colors or gradients with opaque stops, without effects or brushes, +/// and so are the members of a group or compound path, at full opacity without an opacity mask. +fn paints_opaque(n: &Node) -> bool { + let paint = |p: &Paint| match p { + Paint::None | Paint::Solid { .. } => true, + Paint::Gradient(g) => g.gradient.stops.iter().all(|s| s.opacity >= 1.0), + _ => false, + }; + let items = n.appearance.items.iter().all(|i| { + let brush = matches!(i, AppearanceItem::Stroke(s) if s.brush.is_some()); + !i.visible() || (i.opacity() >= 1.0 && i.effects().is_empty() && !brush && paint(i.paint())) + }); + let member = + |c: &std::sync::Arc| !c.visible || (c.opacity >= 1.0 && c.extras().mask.as_deref().is_none_or(|m| m.disabled) && paints_opaque(c)); + n.appearance.effects.is_empty() + && items + && match &n.kind { + NodeKind::Path { .. } => true, + NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } | NodeKind::Compound { children, .. } => children.iter().all(member), + NodeKind::Mesh(m) => m.points.iter().all(|p| p.opacity >= 1.0), + _ => false, + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use vectorcraft_doc::{Appearance, NodeId, Symbol, SymbolOverride}; + use vectorcraft_geom::shapes; + + use super::*; + + /// An instance of a dynamic symbol whose art holds an instance of the same symbol with the same + /// appearance changes ends the search for brushed strokes where the art comes round again, + /// after a few objects, instead of recursing until the stack runs out. + #[test] + fn a_dynamic_symbol_holding_itself_ends_the_brush_search() { + let mut d = Document::new(200.0, 200.0); + let instance = || { + let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Loop".into(), xf: Affine::translate((40.0, 0.0)) }); + n.extras_mut().symbol_overrides = vec![SymbolOverride { path: vec![0], opacity: Some(0.5), ..Default::default() }]; + n + }; + let square = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::basic(Paint::None, Paint::None, 0.0)); + let art = Node::group(NodeId(0), vec![Arc::new(square), Arc::new(instance())]); + d.symbols.push(Symbol { dynamic: true, name: "Loop".into(), art: Arc::new(art) }); + let mut search = BrushSearch::new(&d, &[]); + assert!(!search.node(&instance(), true, 0)); + assert!(!search.exhausted); + assert!(MAX_KNOCKOUT_MASK_OBJECTS - search.left < 10, "{} objects", MAX_KNOCKOUT_MASK_OBJECTS - search.left); + } + + /// A group of 1,000 instances of a dynamic symbol whose art holds 100 squares, each instance + /// with appearance changes of its own: copying and looking through every instance's art would + /// take about 203,000 objects, and the search stops when [`MAX_KNOCKOUT_MASK_OBJECTS`] are + /// used, counting as a brush. With the same changes on every instance, the art is copied + /// once. + #[test] + fn the_brush_search_stops_within_its_object_limit() { + let mut d = Document::new(200.0, 200.0); + let square = + Arc::new(Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), Appearance::basic(Paint::None, Paint::None, 0.0))); + d.symbols.push(Symbol { dynamic: true, name: "Dots".into(), art: Arc::new(Node::group(NodeId(0), vec![square; 100])) }); + let group = |distinct: bool| { + let instances = (0..1000u32) + .map(|i| { + let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Dots".into(), xf: Affine::IDENTITY }); + let path = if distinct { vec![0, i] } else { vec![0] }; + n.extras_mut().symbol_overrides = vec![SymbolOverride { path, opacity: Some(0.5), ..Default::default() }]; + Arc::new(n) + }) + .collect(); + Node::group(NodeId(0), instances) + }; + let mut search = BrushSearch::new(&d, &[]); + assert!(search.node(&group(true), true, 0)); + assert!(search.exhausted); + assert_eq!(search.left, 0); + let mut search = BrushSearch::new(&d, &[]); + assert!(!search.node(&group(false), true, 0)); + assert!(!search.exhausted); + // The group, the 1,000 instances, and one copy of the art counted and looked through. + assert_eq!(MAX_KNOCKOUT_MASK_OBJECTS - search.left, 1 + 1000 + 101 + 101); + } + + /// A placed file read as one square with a 3 pt brushed stroke. + fn brushed_art() -> (Document, Node, Rect) { + let mut d = Document::new(40.0, 20.0); + d.layers.clear(); + let mut st = StrokeLayer::new(Paint::solid(vectorcraft_color::Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + let mut square = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 20.0)), Appearance::basic(Paint::None, Paint::None, 0.0)); + square.appearance.items.push(AppearanceItem::Stroke(st)); + let art = Node::group(NodeId(0), vec![Arc::new(square)]); + (d, art, Rect::new(0.0, 0.0, 40.0, 20.0)) + } + + /// A placed document is drawn as the art read from its file, and the brushed strokes in that + /// art count. + #[test] + fn brushed_strokes_in_a_placed_documents_art_count() { + let d = Document::new(200.0, 200.0); + let placed = vectorcraft_doc::PlacedDocument { + link: vectorcraft_doc::LinkInfo::new("/art/placed.vectorcraft"), + key: "placed-1".into(), + bounding: false, + page_box: vectorcraft_doc::placed_document::PageBox::Crop, + width: 40.0, + height: 20.0, + xf: Affine::IDENTITY, + placement: Default::default(), + }; + // The art the export reads for it before drawing (`Document::with_placed_art`). + vectorcraft_doc::placed_document::prime(&placed, Arc::new(vec![]), brushed_art()); + let n = Node::new(NodeId(0), NodeKind::PlacedDocument(Box::new(placed))); + let brushes = vectorcraft_brush::library(&d); + assert!(BrushSearch::new(&d, &brushes).node(&n, true, 0)); + } +} diff --git a/crates/pdf/src/import/builder.rs b/crates/pdf/src/import/builder.rs index 4a979e8742..0b32054c7b 100644 --- a/crates/pdf/src/import/builder.rs +++ b/crates/pdf/src/import/builder.rs @@ -387,6 +387,21 @@ impl<'p> Builder<'p> { only = fold_stop_opacity(&only, &m).unwrap_or_else(|| only.with_mask(Some(Box::new(m)))); } Some(only) + } else if let [only] = children.as_slice() + && matches!(only.kind, NodeKind::Group { clip: false, .. }) + && only.knockout == Knockout::On + && only.opacity >= 0.999 + && only.blend == BlendMode::Normal + && !only.isolate + && only.extras().mask.is_none() + && mask.is_none() + && !knockout + { + // A knockout group drawn in a form that carries its opacity, blend mode or + // isolation: one knockout group with them. + let mut only = (**only).clone(); + (only.opacity, only.blend, only.isolate) = (opacity, blend, isolate); + Some(only) } else { let mut g = Node::group(self.id(), children); (g.opacity, g.blend, g.isolate) = (opacity, blend, isolate); diff --git a/crates/pdf/src/surface.rs b/crates/pdf/src/surface.rs index 4229781cdd..b20b9667ab 100644 --- a/crates/pdf/src/surface.rs +++ b/crates/pdf/src/surface.rs @@ -81,6 +81,11 @@ impl<'a> Surface<'a> { self.group(|s| s.push_isolated()); } + /// A transparency group with `options` (isolated, knockout), as [`Self::push_isolated`]. + pub fn push_group(&mut self, options: krilla::blend::GroupOptions) { + self.group(|s| s.push_group(options)); + } + /// The mask's stream is already in [`Self::group_transform`]'s space, like the group's art. pub fn push_mask(&mut self, mask: Mask) { self.group(|s| s.push_mask(mask)); diff --git a/crates/pdf/src/tests/blend.rs b/crates/pdf/src/tests/blend.rs index 346b4f34d9..c55a0b7504 100644 --- a/crates/pdf/src/tests/blend.rs +++ b/crates/pdf/src/tests/blend.rs @@ -97,3 +97,466 @@ fn a_knockout_group_with_blending_elements_is_non_isolated_unless_isolated() { assert_eq!(text(&d).contains("/Itrue"), isolate, "isolate {isolate}"); } } + +/// A knockout group of `n` red, green and blue squares at 50% opacity that all overlap, over an +/// opaque backdrop. `knockout_shape`: their opacity defines their knockout shape. +fn knockout_squares(n: usize, knockout_shape: bool, isolate: bool) -> Document { + let mut d = Document::new(200.0, 200.0); + add(&mut d, rect(Rect::new(0.0, 0.0, 200.0, 200.0), (0.9, 0.8, 0.2))); + let colors = [(1.0, 0.0, 0.0), (0.0, 0.8, 0.0), (0.0, 0.0, 1.0)]; + let squares = (0..n) + .map(|i| { + let x = (i % 20) as f64 * 2.0; + let mut s = rect(Rect::new(20.0 + x, 50.0, 120.0 + x, 150.0), colors[i % 3]); + (s.id, s.opacity, s.knockout_shape) = (d.alloc_id(), 0.5, knockout_shape); + std::sync::Arc::new(s) + }) + .collect(); + let mut g = Node::group(NodeId(0), squares); + (g.knockout, g.isolate) = (vectorcraft_doc::Knockout::On, isolate); + add(&mut d, g); + d +} + +/// How many objects each knockout group of `d` holds. +fn knockout_groups(d: &Document) -> Vec { + let mut out = vec![]; + d.walk(|n| { + if n.knockout == vectorcraft_doc::Knockout::On { + out.push(n.children().map_or(0, |c| c.len())); + } + }); + out +} + +/// A knockout group whose elements knock out the area they paint is written as one PDF knockout +/// group (`/K true`), whatever the number of elements, and reads back as a knockout group that +/// draws as the original does. +#[test] +fn a_knockout_group_is_written_as_one_pdf_knockout_group() { + for n in [0, 1, 2, 49, 50, 200] { + let d = knockout_squares(n, false, false); + let t = text(&d); + assert_eq!(t.matches("/Ktrue").count(), usize::from(n > 1), "{n}"); + let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap_or_else(|e| panic!("{n}: {e}")); + assert_eq!(knockout_groups(&back), if n > 1 { vec![n] } else { vec![] }, "{n}"); + if n == 200 { + vectorcraft_testkit::raster::assert_similar( + &vectorcraft_testkit::raster::render_artboard(&back), + &vectorcraft_testkit::raster::render_artboard(&d), + 4.0, + 0.01, + ); + } + } +} + +/// The knockout group of an isolated group is isolated too (`/I true` beside `/K true`), and the +/// group reads back as isolated and knocking out. (Each translucent element is a group of its own.) +#[test] +fn an_isolated_knockout_group_is_one_isolated_pdf_knockout_group() { + for isolate in [false, true] { + let mut d = knockout_squares(3, false, isolate); + // Blending inside, which isolation keeps from the backdrop. + let g = d.layers[0].children().unwrap()[1].id; + let top = d.node(g).unwrap().children().unwrap()[2].id; + d.node_mut(top).unwrap().blend = BlendMode::Multiply; + let t = text(&d); + assert_eq!(t.matches("/Ktrue").count(), 1, "isolate {isolate}"); + assert_eq!(t.contains("/Itrue/Ktrue"), isolate, "isolate {isolate}"); + let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap(); + let mut found = vec![]; + back.walk(|n| { + if n.knockout == vectorcraft_doc::Knockout::On { + found.push(n.isolate); + } + }); + assert_eq!(found, [isolate]); + } +} + +/// A translucent knockout group is written as a form with its opacity around the `/K` group, and +/// reads back as one knockout group with that opacity and its isolation. +#[test] +fn a_translucent_knockout_group_reads_back_as_one_group() { + for isolate in [false, true] { + let mut d = knockout_squares(3, false, isolate); + let g = d.layers[0].children().unwrap()[1].id; + d.node_mut(g).unwrap().opacity = 0.5; + let top = d.node(g).unwrap().children().unwrap()[2].id; + d.node_mut(top).unwrap().blend = BlendMode::Multiply; + let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap(); + let mut found = vec![]; + back.walk(|n| { + if matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. }) { + found.push(( + n.knockout == vectorcraft_doc::Knockout::On, + n.isolate, + (n.opacity * 100.0).round(), + n.children().map_or(0, |c| c.len()), + )); + } + }); + assert_eq!(found, [(true, isolate, 50.0, 3)], "isolate {isolate}"); + } +} + +/// Elements whose opacity defines their knockout shape knock out less than a PDF knockout group +/// knocks out: each is drawn through one soft mask of the elements above it, so that 200 of them +/// still read back, every one of them. +#[test] +fn elements_whose_opacity_defines_their_knockout_shape_are_drawn_through_one_mask_each() { + let d = knockout_squares(200, true, false); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert!(!r.warnings.iter().any(|w| w.contains("knockout")), "{:?}", r.warnings); + assert!(!text(&d).contains("/Ktrue")); + let back = import(&r.bytes).unwrap(); + let mut paths = 0; + back.walk(|n| paths += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::Path { .. }))); + // The backdrop and the 200 squares (the masks' copies of them are opacity-mask art). + assert_eq!(paths, 201); +} + +/// When the masks would draw more than [`crate::export::MAX_KNOCKOUT_MASK_OBJECTS`] objects, the +/// group is written as a PDF knockout group, with a warning, and reads back as a knockout group. +#[test] +fn a_knockout_group_too_large_to_mask_is_a_pdf_knockout_group_with_a_warning() { + // 400 elements that all overlap: 79,800 objects in their masks. + const { assert!(400 * 399 / 2 > crate::export::MAX_KNOCKOUT_MASK_OBJECTS) }; + let d = knockout_squares(400, true, false); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert!(r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_SHAPES), "{:?}", r.warnings); + assert_eq!(text(&d).matches("/Ktrue").count(), 1); + assert_eq!(knockout_groups(&import(&r.bytes).unwrap()), [400]); +} + +/// In a PDF knockout group each painting operation knocks out the ones before it: an element with +/// two fills, the second in Multiply, is drawn as one group (through a full alpha mask), so that +/// its second fill blends with its first. An element with one fill needs no group. +#[test] +fn an_element_with_several_paints_is_one_group_in_a_knockout_group() { + for (fills, groups) in [(1, 0), (2, 1)] { + let mut d = Document::new(200.0, 200.0); + let below = rect(Rect::new(0.0, 0.0, 100.0, 100.0), (0.0, 0.0, 1.0)); + let mut top = rect(Rect::new(50.0, 50.0, 150.0, 150.0), (1.0, 0.0, 0.0)); + if fills == 2 { + let mut multiply = vectorcraft_doc::FillLayer::new(Paint::solid(Color::rgb(0.0, 0.0, 1.0))); + multiply.blend = BlendMode::Multiply; + top.appearance.items.push(vectorcraft_doc::AppearanceItem::Fill(multiply)); + } + top.id = d.alloc_id(); + let below = Node { id: d.alloc_id(), ..below }; + let mut g = Node::group(NodeId(0), vec![std::sync::Arc::new(below), std::sync::Arc::new(top)]); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let t = text(&d); + assert_eq!(t.matches("/Ktrue").count(), 1, "{fills}"); + let Some(start) = t.find("/Ktrue") else { panic!("no knockout group") }; + let group = &t[start..]; + let Some(end) = group.find("endstream") else { panic!("no knockout stream") }; + assert_eq!(group[..end].matches("Do").count(), groups, "{fills}: {t}"); + } +} + +/// Elements that don't overlap need no masks of each other. +#[test] +fn knockout_masks_hold_only_the_elements_that_overlap() { + let mut d = Document::new(200.0, 100.0); + let squares = (0..3) + .map(|i| { + let x = f64::from(i) * 60.0; + let mut s = rect(Rect::new(x, 0.0, x + 40.0, 40.0), (0.0, 0.0, 1.0)); + (s.id, s.opacity, s.knockout_shape) = (d.alloc_id(), 0.5, true); + std::sync::Arc::new(s) + }) + .collect(); + let mut g = Node::group(NodeId(0), squares); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let t = text(&d); + assert!(!t.contains("/Luminosity") && !t.contains("/Ktrue"), "{t}"); +} + +/// The copies of symbol instances drawn in knockout masks don't count toward the objects the +/// visible symbol art of an export may hold (`MAX_SYMBOL_NODES`): of two overlapping 50% instances +/// whose art placed through 9-slice guides holds about 6,510,000 objects each (a gradient square +/// in all nine cells, each copy with a disabled opacity mask of 723,351 objects), both are written +/// in their nine cells, although the lower one's mask draws a copy of the upper one first. +#[test] +fn knockout_mask_copies_of_symbol_instances_leave_the_visible_art_its_object_limit() { + let d = panel_instances(850, &[20.0, 40.0]); + // Nine squares of each instance (the mask's copy of the upper one is opacity-mask art). + assert_eq!(painted_paths(&export(&d, &PdfOptions::default()).unwrap()), 18); +} + +/// A knockout group of overlapping 50% instances, at `xs`, of a symbol whose art placed through +/// 9-slice guides holds 9 × (`mask_objects`² + `mask_objects` + 3) objects (a gradient square in +/// all nine cells, each copy with a disabled opacity mask of `mask_objects`² + `mask_objects` + 1 +/// objects). +fn panel_instances(mask_objects: usize, xs: &[f64]) -> Document { + use vectorcraft_color::{Gradient, GradientPaint}; + use vectorcraft_geom::Affine; + let mut d = Document::new(300.0, 200.0); + let leaf = std::sync::Arc::new(rect(Rect::new(0.0, 0.0, 1.0, 1.0), (0.0, 0.0, 0.0))); + let inner = std::sync::Arc::new(Node::group(NodeId(0), vec![leaf; mask_objects])); + let mut mask = vectorcraft_doc::OpacityMask::new(Node::group(NodeId(0), vec![inner; mask_objects]), true); + (mask.linked, mask.disabled) = (false, true); + let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))); + let mut square = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::basic(gradient, Paint::None, 0.0)); + square.extras_mut().mask = Some(Box::new(mask)); + let art = Node::group(NodeId(0), vec![std::sync::Arc::new(square)]); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Panel".into(), art: std::sync::Arc::new(art) }); + d.set_symbol_nine_slice("Panel", Some(vectorcraft_doc::SymbolNineSlice::default())); + let instances = xs + .iter() + .map(|&x| { + let xf = Affine::translate((x, 20.0)) * Affine::scale_non_uniform(2.0, 1.5); + let mut n = Node::new(NodeId(0), vectorcraft_doc::NodeKind::SymbolInstance { symbol: "Panel".into(), xf }); + (n.id, n.opacity, n.knockout_shape) = (d.alloc_id(), 0.5, true); + std::sync::Arc::new(n) + }) + .collect(); + let mut g = Node::group(NodeId(0), instances); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + d +} + +/// The paths of `pdf` read back, the art of opacity masks left out. +fn painted_paths(pdf: &[u8]) -> usize { + let mut paths = 0; + import(pdf).unwrap().walk(|n| paths += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::Path { .. }))); + paths +} + +/// When the copies the knockout masks draw would hold more symbol objects than they may +/// (`MAX_SYMBOL_NODES` of their own), the group is written as a PDF knockout group, with a +/// warning, instead of masks that leave out art their elements cover: four overlapping instances +/// of about 3,870,000 objects each fit the visible art's limit, and the six copies in their masks +/// do not. +#[test] +fn knockout_masks_whose_symbol_copies_run_out_give_way_to_a_pdf_knockout_group() { + let d = panel_instances(655, &[20.0, 40.0, 60.0, 80.0]); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert!(r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_SHAPES), "{:?}", r.warnings); + assert_eq!(painted_paths(&r.bytes), 36); +} + +/// A brushed stroke draws brush art that the objects counted for the knockout masks leave out: +/// a group in which a mask would draw an element with a brushed stroke is written as a PDF +/// knockout group, with a warning. A brushed element that no mask draws (the lowest one) leaves +/// the masks as they are. +#[test] +fn a_knockout_group_whose_masks_would_draw_a_brushed_stroke_is_a_pdf_knockout_group() { + for (brushed, expected) in [(1, true), (0, false)] { + let mut d = Document::new(200.0, 200.0); + let squares = (0..2) + .map(|i| { + let x = f64::from(i) * 40.0; + let mut s = rect(Rect::new(20.0 + x, 50.0, 120.0 + x, 150.0), (0.0, 0.0, 1.0)); + (s.id, s.opacity, s.knockout_shape) = (d.alloc_id(), 0.5, true); + if i == brushed { + let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + s.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(st)); + } + std::sync::Arc::new(s) + }) + .collect(); + let mut g = Node::group(NodeId(0), squares); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert_eq!(r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_BRUSHES), expected, "{brushed}: {:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_SHAPES), "{brushed}: {:?}", r.warnings); + assert_eq!(text(&d).matches("/Ktrue").count(), usize::from(expected), "{brushed}"); + } +} + +/// The knockout group of `upper` at 50%, whose opacity defines its knockout shape when +/// `knockout_shape`, between two 50% squares whose opacity defines theirs (so that the group is +/// drawn through masks): whether the export gives the brushed-stroke warning. +fn warns_of_brushes(mut d: Document, mut upper: Node, knockout_shape: bool) -> bool { + (upper.id, upper.opacity, upper.knockout_shape) = (d.alloc_id(), 0.5, knockout_shape); + let mut lower = rect(Rect::new(20.0, 50.0, 120.0, 150.0), (1.0, 0.0, 0.0)); + (lower.id, lower.opacity, lower.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut top = rect(Rect::new(100.0, 50.0, 180.0, 150.0), (0.0, 0.8, 0.0)); + (top.id, top.opacity, top.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut g = Node::group(NodeId(0), [lower, upper, top].map(std::sync::Arc::new).to_vec()); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_BRUSHES) +} + +/// A square at (60, 50) with a 3 pt brushed stroke. +fn brushed_square() -> Node { + let mut line = rect(Rect::new(60.0, 50.0, 160.0, 150.0), (1.0, 1.0, 1.0)); + let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + line.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(st)); + line +} + +/// Only brushed strokes a mask draws count: a hidden member's is not drawn, nor is one without +/// paint or width, nor the one in the opacity mask of an element whose mask does not define its +/// knockout shape (its copy in a mask leaves the opacity mask out). +#[test] +fn brushed_strokes_that_a_mask_does_not_draw_leave_the_masks_as_they_are() { + let d = Document::new(200.0, 200.0); + let mut hidden = brushed_square(); + hidden.visible = false; + let plain = rect(Rect::new(60.0, 50.0, 160.0, 150.0), (0.0, 0.0, 1.0)); + let group = Node::group(NodeId(0), vec![std::sync::Arc::new(plain.clone()), std::sync::Arc::new(hidden)]); + assert!(!warns_of_brushes(d.clone(), group, true)); + // A brushed stroke without paint or width draws nothing. + for (paint, width) in [(Paint::None, 3.0), (Paint::solid(Color::BLACK), 0.0)] { + let mut idle = brushed_square(); + if let Some(vectorcraft_doc::AppearanceItem::Stroke(st)) = idle.appearance.items.last_mut() { + (st.paint, st.width) = (paint, width); + } + assert!(!warns_of_brushes(d.clone(), idle, true), "width {width}"); + } + assert!(warns_of_brushes(d.clone(), brushed_square(), true)); + // A group's own brushed stroke is drawn too: the export bakes it into a path among the + // group's members. + let mut stroked_group = Node::group(NodeId(0), vec![std::sync::Arc::new(plain.clone())]); + stroked_group.appearance = brushed_square().appearance; + assert!(warns_of_brushes(d.clone(), stroked_group, true)); + for (knockout_shape, expected) in [(false, false), (true, true)] { + let mut masked = plain.clone(); + let mut mask = vectorcraft_doc::OpacityMask::new(brushed_square(), true); + mask.linked = false; + masked.extras_mut().mask = Some(Box::new(mask)); + assert_eq!(warns_of_brushes(d.clone(), masked, knockout_shape), expected, "knockout shape {knockout_shape}"); + } +} + +/// A compound path's members are drawn with its own paint, so a brushed stroke on a member draws +/// nothing; one on the compound path does. +#[test] +fn a_brushed_stroke_counts_on_a_compound_path_and_not_on_its_members() { + let d = Document::new(200.0, 200.0); + let compound = |members: Vec| { + let children = members.into_iter().map(std::sync::Arc::new).collect(); + let mut c = Node::new(NodeId(0), vectorcraft_doc::NodeKind::Compound { children, rule: Default::default() }); + c.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0).into(); + c + }; + assert!(!warns_of_brushes(d.clone(), compound(vec![brushed_square()]), true)); + let mut stroked = compound(vec![rect(Rect::new(60.0, 50.0, 160.0, 150.0), (1.0, 1.0, 1.0))]); + stroked.appearance = brushed_square().appearance; + assert!(warns_of_brushes(d, stroked, true)); +} + +/// A pattern's tiles draw the brushed strokes in its art: an element filled with such a pattern +/// counts, and one filled with a pattern without them does not. +#[test] +fn a_brushed_stroke_in_a_pattern_counts() { + for (brushed_tile, expected) in [(true, true), (false, false)] { + let mut d = Document::new(200.0, 200.0); + let tile = if brushed_tile { brushed_square() } else { rect(Rect::new(60.0, 50.0, 160.0, 150.0), (1.0, 1.0, 1.0)) }; + d.patterns.push(vectorcraft_doc::pattern::PatternDef::new("Tile", vec![std::sync::Arc::new(tile)])); + let pattern = Paint::Pattern { pattern: "Tile".into(), xf: vectorcraft_geom::Affine::IDENTITY }; + let filled = Node::path(NodeId(0), shapes::rectangle(Rect::new(60.0, 50.0, 160.0, 150.0)), Appearance::basic(pattern, Paint::None, 0.0)); + assert_eq!(warns_of_brushes(d, filled, true), expected, "brushed tile {brushed_tile}"); + } +} + +/// A stroke whose brush the export does not know is drawn as a plain stroke, and a stroke without +/// width draws nothing, not even its pattern's brushed strokes. +#[test] +fn strokes_that_draw_no_brush_art_leave_the_masks_as_they_are() { + let mut unknown = brushed_square(); + if let Some(vectorcraft_doc::AppearanceItem::Stroke(st)) = unknown.appearance.items.last_mut() { + st.brush = Some("No Such Brush".into()); + } + assert!(!warns_of_brushes(Document::new(200.0, 200.0), unknown, true)); + let mut d = Document::new(200.0, 200.0); + d.patterns.push(vectorcraft_doc::pattern::PatternDef::new("Tile", vec![std::sync::Arc::new(brushed_square())])); + let pattern = Paint::Pattern { pattern: "Tile".into(), xf: vectorcraft_geom::Affine::IDENTITY }; + let mut thin = rect(Rect::new(60.0, 50.0, 160.0, 150.0), (0.0, 0.0, 1.0)); + thin.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(vectorcraft_doc::StrokeLayer::new(pattern, 0.0))); + assert!(!warns_of_brushes(d, thin, true)); +} + +/// Type draws the brushed strokes in the patterns its characters are filled with and in the art +/// of its inline graphics. +#[test] +fn brushed_strokes_in_type_patterns_and_inline_graphics_count() { + let text = |d: &mut Document, fill: Paint, inline: bool| { + let style = vectorcraft_doc::CharStyle { size: 60.0, fill, ..Default::default() }; + let mut t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(60.0, 120.0), "Type", style.clone()); + if inline { + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Brushed".into(), art: std::sync::Arc::new(brushed_square()) }); + let mut run = vectorcraft_doc::TextRun::new("\u{FFFC}", style); + run.inline = Some(vectorcraft_doc::InlineArt::new("Brushed")); + t.runs.push(run); + } + Node::new(NodeId(0), vectorcraft_doc::NodeKind::Text(Box::new(t))) + }; + let black = Paint::solid(Color::BLACK); + let mut d = Document::new(200.0, 200.0); + let plain = text(&mut d, black.clone(), false); + assert!(!warns_of_brushes(d.clone(), plain, true)); + d.patterns.push(vectorcraft_doc::pattern::PatternDef::new("Tile", vec![std::sync::Arc::new(brushed_square())])); + let patterned = text(&mut d, Paint::Pattern { pattern: "Tile".into(), xf: vectorcraft_geom::Affine::IDENTITY }, false); + assert!(warns_of_brushes(d, patterned, true)); + let mut d = Document::new(200.0, 200.0); + let inline = text(&mut d, black, true); + assert!(warns_of_brushes(d, inline, true)); +} + +/// Type draws the brushed strokes of its appearance as brush art too. +#[test] +fn a_brushed_stroke_on_type_counts() { + let style = vectorcraft_doc::CharStyle { size: 60.0, ..Default::default() }; + let text = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(60.0, 120.0), "Type", style); + let mut n = Node::new(NodeId(0), vectorcraft_doc::NodeKind::Text(Box::new(text))); + n.appearance = brushed_square().appearance; + assert!(warns_of_brushes(Document::new(200.0, 200.0), n, true)); +} + +/// The brushed strokes a mask would draw are found in the opacity-mask art of a symbol's art too, +/// and a disabled opacity mask, which is not drawn, holds none. +#[test] +fn brushed_strokes_in_mask_art_count_only_where_the_mask_is_on() { + for (in_symbol, disabled, expected) in [(true, false, true), (false, true, false), (false, false, true)] { + let mut d = Document::new(200.0, 200.0); + let mut line = rect(Rect::new(0.0, 0.0, 100.0, 100.0), (1.0, 1.0, 1.0)); + let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + line.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(st)); + let mut mask = vectorcraft_doc::OpacityMask::new(line, true); + (mask.linked, mask.disabled) = (false, disabled); + let mut masked = rect(Rect::new(0.0, 0.0, 100.0, 100.0), (0.0, 0.0, 1.0)); + masked.extras_mut().mask = Some(Box::new(mask)); + let mut upper = if in_symbol { + let art = Node::group(NodeId(0), vec![std::sync::Arc::new(masked)]); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Masked".into(), art: std::sync::Arc::new(art) }); + let xf = vectorcraft_geom::Affine::translate((60.0, 50.0)); + Node::new(NodeId(0), vectorcraft_doc::NodeKind::SymbolInstance { symbol: "Masked".into(), xf }) + } else { + masked.transform(vectorcraft_geom::Affine::translate((60.0, 50.0)), false); + masked + }; + (upper.id, upper.opacity, upper.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut lower = rect(Rect::new(20.0, 50.0, 120.0, 150.0), (1.0, 0.0, 0.0)); + (lower.id, lower.opacity, lower.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut g = Node::group(NodeId(0), vec![std::sync::Arc::new(lower), std::sync::Arc::new(upper)]); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + let warned = r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_BRUSHES); + assert_eq!(warned, expected, "symbol {in_symbol}, disabled {disabled}: {:?}", r.warnings); + } +} + +/// PDF/A-2 allows transparency: a knockout group is written as one there too. +#[test] +fn a_pdf_a_2b_file_keeps_a_knockout_group() { + let mut o = PdfOptions::default(); + (o.settings.standard, o.settings.compatibility) = (Standard::PdfA2b, Standard::PdfA2b.version()); + let bytes = export(&knockout_squares(3, false, false), &o).unwrap(); + assert_eq!(knockout_groups(&import(&bytes).unwrap()), [3]); +} diff --git a/crates/pdf/tests/gradient_forms.rs b/crates/pdf/tests/gradient_forms.rs index 9f06c9b844..7373ec8494 100644 --- a/crates/pdf/tests/gradient_forms.rs +++ b/crates/pdf/tests/gradient_forms.rs @@ -91,9 +91,7 @@ fn fixture(form: Form, kind: GradientKind, mode: ColorMode, origin: Point) -> (D /// Round trips preserve gradient placement for the form types the importer supports. #[test] fn gradients_in_forms_keep_their_position_when_read_back() { - // The importer has a pre-existing mismatch on the knockout's synthesized nested masks; - // Quartz below checks its actual PDF output, along with the other forms. - for form in FORMS.into_iter().filter(|f| !matches!(f, Form::Knockout)) { + for form in FORMS { for kind in [GradientKind::Linear, GradientKind::Radial] { for origin in [Point::ORIGIN, Point::new(125.0, -80.0)] { let (d, opts) = fixture(form, kind, ColorMode::Rgb, origin); diff --git a/crates/render/src/fx/bounds.rs b/crates/render/src/fx/bounds.rs index 13a6dad415..ce76713e74 100644 --- a/crates/render/src/fx/bounds.rs +++ b/crates/render/src/fx/bounds.rs @@ -356,7 +356,7 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { }; let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_ref()); let b = match &reshaped { - Some(r) if matches!(n.kind, NodeKind::SymbolInstance { .. }) && effects::has_item_geometry(n) => instance_piece_bounds(symbols, r), + Some(r) if has_instance_row_effects(n) => instance_piece_bounds(symbols, r), Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => { r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, painted_reach(symbols, c))) } @@ -379,9 +379,16 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { } } +/// Is `n` a symbol instance whose active fill or stroke has geometry or raster effects that the +/// fills and strokes of its art take ([`effects::has_item_geometry`], +/// [`effects::has_instance_row_raster`])? +fn has_instance_row_effects(n: &Node) -> bool { + matches!(n.kind, NodeKind::SymbolInstance { .. }) && (effects::has_item_geometry(n) || effects::has_instance_row_raster(n)) +} + /// The bounds of `art`, the art [`effects::reshape`] made of a symbol instance whose active fill or -/// stroke has geometry effects ([`effects::has_item_geometry`]): its paths measured with the -/// geometry effects of their fills and strokes ([`visual_bounds`]), the symbol instances in it that +/// stroke has geometry or raster effects ([`has_instance_row_effects`]): its paths measured with the +/// effects of their fills and strokes, shadows and glows included ([`visual_bounds`]), the symbol instances in it that /// took those effects by their own reshaped art in turn, its groups and layers with their own fills /// and strokes by the art those paint ([`effects::evaluate_container`]), its clip groups by their /// clipping paths and other objects as [`Node::visual_bounds_with`] measures them. Hidden objects are left out. @@ -409,7 +416,7 @@ fn pieces_within(symbols: &SymbolBoxes, art: &Node, depth: usize, budget: &mut u g.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, pieces_within(symbols, c, depth, budget))) } NodeKind::Path { .. } | NodeKind::Compound { .. } if has_fx(art) => visual_bounds(symbols, art), - NodeKind::SymbolInstance { symbol, .. } if depth < vectorcraft_doc::depth::MAX_DEPTH && effects::has_item_geometry(art) => { + NodeKind::SymbolInstance { symbol, .. } if depth < vectorcraft_doc::depth::MAX_DEPTH && has_instance_row_effects(art) => { let doc = symbols.document(); let reshaped = doc.symbols.iter().find(|s| s.name == *symbol).and_then(|s| { let count = s.art.count() as u64; @@ -639,7 +646,7 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool, brush: b }; let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_ref()); let b = match &reshaped { - Some(r) if matches!(n.kind, NodeKind::SymbolInstance { .. }) && effects::has_item_geometry(n) => instance_piece_bounds(symbols, r), + Some(r) if has_instance_row_effects(n) => instance_piece_bounds(symbols, r), Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => { r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c))) } diff --git a/crates/render/src/fx/predicates.rs b/crates/render/src/fx/predicates.rs index 4f14cadfd1..e4e479b2e5 100644 --- a/crates/render/src/fx/predicates.rs +++ b/crates/render/src/fx/predicates.rs @@ -20,12 +20,14 @@ pub(crate) fn has_fx(n: &Node) -> bool { /// Does `n` carry visible object-level effects that only apply through its art (type, images, /// symbol instances, live objects; Crop Marks and colour adjustments on anything), is it type with /// a geometry effect on a fill or stroke of its own or an image with a fill or stroke of its own -/// that paints a shape ([`effects::has_item_geometry`]), is it a path whose Transform effect copies -/// it with a gradient or changes its fills and strokes ([`effects::has_transform_art`]), or is it a -/// group or layer with an appearance of its own (see [`Renderer::draw_object_fx`])? +/// that paints a shape ([`effects::has_item_geometry`]), a symbol instance whose active fill or +/// stroke has a raster effect ([`effects::has_instance_row_raster`]), is it a path whose Transform +/// effect copies it with a gradient or changes its fills and strokes ([`effects::has_transform_art`]), +/// or is it a group or layer with an appearance of its own (see [`Renderer::draw_object_fx`])? pub(crate) fn has_object_fx(n: &Node) -> bool { (effects::needs_outline(n) && (visible(&n.appearance.effects) || n.projection().is_some())) || effects::has_item_geometry(n) + || effects::has_instance_row_raster(n) || effects::has_transform_art(n) || effects::has_container_appearance(n) || effects::has_crop_marks(n) diff --git a/crates/svg/src/export/nodes.rs b/crates/svg/src/export/nodes.rs index a4cdf26271..8716af7367 100644 --- a/crates/svg/src/export/nodes.rs +++ b/crates/svg/src/export/nodes.rs @@ -163,6 +163,18 @@ fn pin_to_page(n: &mut Node, m: Affine) { /// What SVG Tiny 1.2 does without masks. const TINY_MASKS: &str = "SVG Tiny 1.2 has no masks: opacity masks are left out and knockout groups written as plain groups"; + +/// What a knockout group too large to mask is written as. +const KNOCKOUT_SIZE: &str = "knockout groups with too many overlapping elements to mask are written as plain groups"; + +/// The most knockout shapes the masks of one knockout group's elements use between them: each +/// mask uses the shapes of the elements above its own element that overlap it. +const MAX_KNOCKOUT_MASK_SHAPES: usize = 1 << 16; + +/// The most elements of one knockout group drawn through masks: finding the elements that +/// overlap compares every pair. +const MAX_MASKED_ELEMENTS: usize = 2048; + pub(super) const UNDECODED: &str = "an image couldn't be decoded and is missing from the picture"; /// The region of a mask or filter over art that has no box to measure: one covering any artwork. @@ -450,20 +462,39 @@ impl Writer<'_> { region } + /// The box of what each of `nodes` paints, as [`Self::paint_box`] measures it. + fn paint_boxes(&mut self, nodes: &[&Node]) -> Vec>> { + let symbols = self.region_boxes.take().unwrap_or_else(|| self.doc.symbol_boxes()); + let mut art_reach = std::mem::take(&mut self.art_reach); + let boxes = nodes.iter().map(|n| self.paint_box(&symbols, &mut art_reach, n)).collect(); + self.region_boxes = Some(symbols); + self.art_reach = art_reach; + boxes + } + + /// The box of what `n` paints in the current user space, as [`Self::region_attrs`] measures + /// it: `Some(None)` when it has no box, `None` when the box can't be told (type, or nesting + /// deeper than [`MAX_REACH_DEPTH`]). + fn paint_box(&self, symbols: &vectorcraft_doc::SymbolBoxes, art_reach: &mut HashMap, n: &Node) -> Option> { + let Some(v) = n.visual_bounds_with(symbols) else { return Some(None) }; + let r = self.paint_reach(symbols, art_reach, n, v, 0)?; + let g = n.geometric_bounds_with(symbols).unwrap_or(v); + let stroke = [g.x0 - v.x0, g.y0 - v.y0, v.x1 - g.x1, v.y1 - g.y1].into_iter().fold(0.0, f64::max); + let scaled = self.xf.transform_rect_bbox(v.inflate(r, r)); + let unscaled = self.xf.transform_rect_bbox(g).inflate(stroke + r, stroke + r); + Some(Some(scaled.union(unscaled))) + } + /// [`Self::region_attrs`] with the document's symbol boxes and the symbols' art reach so far. fn region_of(&self, symbols: &vectorcraft_doc::SymbolBoxes, art_reach: &mut HashMap, nodes: &[&Node]) -> String { let mut painted: Option = None; for n in nodes { - let Some(v) = n.visual_bounds_with(symbols) else { continue }; - let Some(r) = self.paint_reach(symbols, art_reach, n, v, 0) else { + let Some(b) = self.paint_box(symbols, art_reach, n) else { return BIG.to_string(); }; - let g = n.geometric_bounds_with(symbols).unwrap_or(v); - let stroke = [g.x0 - v.x0, g.y0 - v.y0, v.x1 - g.x1, v.y1 - g.y1].into_iter().fold(0.0, f64::max); - let scaled = self.xf.transform_rect_bbox(v.inflate(r, r)); - let unscaled = self.xf.transform_rect_bbox(g).inflate(stroke + r, stroke + r); - let both = scaled.union(unscaled); - painted = Some(painted.map_or(both, |p| p.union(both))); + if let Some(both) = b { + painted = Some(painted.map_or(both, |p| p.union(both))); + } } let Some(b) = painted.map(|b| b.inflate(2.0, 2.0)).filter(|b| b.is_finite()) else { return BIG.to_string(); @@ -572,8 +603,10 @@ impl Writer<'_> { } /// Children of the group being written. SVG has no knockout groups: in one, each element is - /// drawn through a mask of where the elements above it don't paint (the same look for Normal - /// blending). The masks nest, so element i sits inside the masks of elements i+1…n. + /// drawn through one mask of where the elements above it that overlap it don't paint (the + /// same look for Normal blending). The masks share one copy of each element's knockout shape, + /// and the elements stay siblings however many there are. A group whose masks would hold more + /// than [`MAX_KNOCKOUT_MASK_SHAPES`] shapes is written as a plain group, with a warning. pub(super) fn children(&mut self, children: &[std::sync::Arc]) { if self.knockout && self.tiny() { self.warn(TINY_MASKS); @@ -603,36 +636,124 @@ impl Writer<'_> { } return; } - let Some((first, rest)) = elements.split_first() else { return }; - // The elements are written first, in paint order, so that each mask copies an element + let all: Vec<&Node> = elements.iter().map(|c| &***c).collect(); + let Some(above) = self.knockout_masks(&all) else { + self.warn(KNOCKOUT_SIZE); + for c in elements { + self.node(c); + } + return; + }; + // The elements are written first, in paint order, so that each shape copies an element // with the symbol art count the element itself was written with - // (`Self::symbol_nodes`): an instance left out of an element is left out of its mask. - let (depth, nest) = (self.depth, rest.len()); - let first = self.detached(depth + nest, |w| w.node(first)); - let mut written = Vec::with_capacity(nest); - for (i, c) in rest.iter().enumerate() { + // (`Self::symbol_nodes`): an instance left out of an element is left out of its shape. + let depth = self.depth; + let mut written = Vec::with_capacity(elements.len()); + for (c, above) in elements.iter().zip(&above) { let before = self.symbol_nodes; - written.push((before, self.detached(depth + nest - 1 - i, |w| w.node(c)))); + written.push((before, self.detached(depth + usize::from(!above.is_empty()), |w| w.node(c)))); } let after = self.symbol_nodes; // Each mask covers what the group's elements paint. - let all: Vec<&Node> = elements.iter().map(|c| &***c).collect(); let region = self.region_attrs(&all); - for (c, (before, _)) in rest.iter().zip(&written).rev() { - self.symbol_nodes = *before; - let mid = self.knockout_mask(c, ®ion); - self.line(&format!("")); - self.depth += 1; + let mut shapes: Vec> = vec![None; elements.len()]; + for &j in above.iter().flatten() { + if shapes[j].is_none() { + self.symbol_nodes = written[j].0; + shapes[j] = Some(self.knockout_shape(elements[j])); + } } self.symbol_nodes = after; - self.body.push_str(&first); - for (_, body) in written { - self.depth -= 1; - self.line(""); + for (above, (_, body)) in above.iter().zip(written) { + if above.is_empty() { + self.body.push_str(&body); + continue; + } + let ids: Vec<&str> = above.iter().filter_map(|&j| shapes[j].as_deref()).collect(); + let mid = self.knockout_mask(&ids, ®ion); + self.line(&format!("")); self.body.push_str(&body); + self.line(""); } } + /// For each element of a knockout group, the indices of the elements above it whose painted + /// boxes overlap its own (every one above it when a box can't be told). `None` when there are + /// more than [`MAX_MASKED_ELEMENTS`] elements or the masks would hold more than + /// [`MAX_KNOCKOUT_MASK_SHAPES`] shapes. + fn knockout_masks(&mut self, elements: &[&Node]) -> Option>> { + if elements.len() > MAX_MASKED_ELEMENTS { + return None; + } + let boxes = self.paint_boxes(elements); + let overlap = |a: Option>, b: Option>| match (a, b) { + (Some(None), _) | (_, Some(None)) => false, + (Some(Some(a)), Some(Some(b))) => a.x0 <= b.x1 + 2.0 && b.x0 <= a.x1 + 2.0 && a.y0 <= b.y1 + 2.0 && b.y0 <= a.y1 + 2.0, + _ => true, + }; + let mut shapes = 0usize; + let mut out = Vec::with_capacity(elements.len()); + for (i, &below) in boxes.iter().enumerate() { + let mut above = vec![]; + for (j, &over) in boxes.iter().enumerate().skip(i + 1) { + if overlap(below, over) { + shapes = shapes.checked_add(1).filter(|&s| s <= MAX_KNOCKOUT_MASK_SHAPES)?; + above.push(j); + } + } + out.push(above); + } + Some(out) + } + + /// One copy of the knockout shape of `c` in the definitions: `c` at full object opacity + /// without its own mask, unless those define its shape. Returns its id. + fn knockout_shape(&mut self, c: &Node) -> String { + let shape = if c.knockout_shape { + c.clone() + } else { + let mut s = c.clone().with_mask(None); + s.opacity = 1.0; + s + }; + let anonymous = std::mem::replace(&mut self.anonymous, true); + let art = self.detached(2, |w| w.node(&shape)); + self.anonymous = anonymous; + let id = self.fresh_id("knockout-art"); + self.def(1, &format!("")); + self.defs.push_str(&art); + self.def(1, ""); + id + } + + /// A mask that is 1 − the knockout shapes `ids` ([`Self::knockout_shape`]): white, then the + /// shapes painted black. `region` is the attributes of the mask's region + /// ([`Self::region_attrs`]), which the filter that paints the shapes black shares. Returns + /// the mask id. + fn knockout_mask(&mut self, ids: &[&str], region: &str) -> String { + let filter = match self.knockout_filters.get(region) { + Some(f) => f.clone(), + None => { + let f = self.fresh_id("knockout-shape"); + self.def(1, &format!("")); + self.def(2, ""); + self.def(1, ""); + self.knockout_filters.insert(region.to_string(), f.clone()); + f + } + }; + let mid = self.fresh_id("knockout"); + self.def(1, &format!("")); + self.def(2, &format!("")); + self.def(2, &format!("")); + for id in ids { + self.def(3, &format!("")); + } + self.def(2, ""); + self.def(1, ""); + mid + } + /// The pieces of a partly transparent mesh-warped image (`elements`) as the elements of a /// knockout group ([`vectorcraft_effects::warp_pieces_translucent`]). Each piece is drawn /// through one mask of where the pieces above it that reach it don't cover @@ -683,42 +804,6 @@ impl Writer<'_> { } } - /// A mask that is 1 − the knockout shape of `c`: white, then `c` painted black (at full object - /// opacity without its own mask, unless those define its shape). `region` is the attributes of - /// the mask's region ([`Self::region_attrs`]), which the filter that paints `c` black shares. - /// Returns the mask id. - fn knockout_mask(&mut self, c: &Node, region: &str) -> String { - let filter = match self.knockout_filters.get(region) { - Some(f) => f.clone(), - None => { - let f = self.fresh_id("knockout-shape"); - self.def(1, &format!("")); - self.def(2, ""); - self.def(1, ""); - self.knockout_filters.insert(region.to_string(), f.clone()); - f - } - }; - let shape = if c.knockout_shape { - c.clone() - } else { - let mut s = c.clone().with_mask(None); - s.opacity = 1.0; - s - }; - let anonymous = std::mem::replace(&mut self.anonymous, true); - let art = self.detached(3, |w| w.node(&shape)); - self.anonymous = anonymous; - let mid = self.fresh_id("knockout"); - self.def(1, &format!("")); - self.def(2, &format!("")); - self.def(2, &format!("")); - self.defs.push_str(&art); - self.def(2, ""); - self.def(1, ""); - mid - } - /// An object, inside `` when it links to a URL (Attributes panel). pub(super) fn node(&mut self, n: &Node) { // Ruler guides are not written, not even as a link. diff --git a/crates/svg/src/import/text.rs b/crates/svg/src/import/text.rs index 419e73a385..1a221a69f1 100644 --- a/crates/svg/src/import/text.rs +++ b/crates/svg/src/import/text.rs @@ -110,8 +110,25 @@ struct ElemStyle { word_spacing: f64, /// `paint-order` paints the stroke before the fill. stroke_first: bool, + /// What its characters' Character Direction ([`Ctx::elem`]) leaves out of the file's + /// `unicode-bidi`, for the import's warnings. + bidi_notes: Vec<&'static str>, } +/// An override (`unicode-bidi="bidi-override"`) read as an embedding. +const BIDI_OVERRIDE: &str = "unicode-bidi=\"bidi-override\" imported as an embedding; directional override is not supported"; +/// An isolate (`unicode-bidi="isolate"`) read as an embedding. +const BIDI_ISOLATE: &str = "unicode-bidi=\"isolate\" imported as an embedding; isolation is not supported"; +/// `unicode-bidi="isolate-override"` read as an embedding. +const BIDI_ISOLATE_OVERRIDE: &str = "unicode-bidi=\"isolate-override\" imported as an embedding; isolation and override are not supported"; +/// `unicode-bidi="plaintext"` read as an embedding in the direction of its first strong character. +const BIDI_PLAINTEXT: &str = + "unicode-bidi=\"plaintext\" imported as an embedding in the direction of its first strong character; isolation is not supported"; +/// An embedding inside another, kept as an embedding of its own. +const BIDI_NESTED: &str = "a unicode-bidi embedding inside another imported one level deep; its characters may be ordered differently"; +/// An override on the `` itself, which keeps only its paragraph direction. +const BIDI_TEXT_OVERRIDE: &str = "unicode-bidi override on the text imported as its paragraph direction; directional override is not supported"; + /// One character of a text and the adjustments its position needs. struct Cell { ch: char, @@ -694,6 +711,9 @@ struct Ctx { sizes: RefCell>, weights: RefCell>, elems: RefCell>>, + /// Whether the first strong character of each element read so far is right to left + /// ([`Self::first_strong_rtl`]); `None` where it holds none. + strong: RefCell>>, /// The path each `` read so far follows ([`Self::text_path`]); `None` where it has /// no usable one. The styles of a `` instance share it, so each path is found and parsed /// once however many `` elements show its text. @@ -716,6 +736,7 @@ impl Ctx { sizes: RefCell::default(), weights: RefCell::default(), elems: RefCell::default(), + strong: RefCell::default(), paths: Rc::default(), }; let root = xml.root_element(); @@ -737,6 +758,7 @@ impl Ctx { sizes: RefCell::default(), weights: RefCell::default(), elems: RefCell::default(), + strong: RefCell::default(), paths: self.paths.clone(), } } @@ -915,6 +937,8 @@ impl Ctx { }; let parent = if n.tag_name().name() == "text" { 0.0 } else { n.parent_element().map_or(0.0, |p| self.elem(p).style.baseline_shift) }; st.baseline_shift = parent + own; + let (char_direction, bidi_notes) = self.char_direction(n); + st.char_direction = char_direction; if self.prop(n, "visibility").is_some_and(|v| v == "hidden" || v == "collapse") { (st.fill, st.stroke, fill_url, stroke_url) = (Paint::None, Paint::None, None, None); } @@ -922,11 +946,93 @@ impl Ctx { let order = self.prop(n, "paint-order").and_then(|v| svgtypes::PaintOrder::from_str(&v).ok()).unwrap_or_default().order; let at = |k| order.iter().position(|o| *o == k); let stroke_first = at(svgtypes::PaintOrderKind::Stroke) < at(svgtypes::PaintOrderKind::Fill); - let e = Rc::new(ElemStyle { style: st, fill: fill_url, stroke: stroke_url, word_spacing, stroke_first }); + let e = Rc::new(ElemStyle { style: st, fill: fill_url, stroke: stroke_url, word_spacing, stroke_first, bidi_notes }); self.elems.borrow_mut().insert(n.id(), e.clone()); e } + /// The computed `unicode-bidi` of element `n`, in lowercase. It is not inherited: `normal` + /// unless `n` sets it, and its parent's value for `inherit`. + fn unicode_bidi(&self, n: XNode) -> String { + let mut at = Some(n); + while let Some(a) = at { + match self.css.own(a, "unicode-bidi").map(|v| v.trim().to_ascii_lowercase()) { + Some(v) if v == "inherit" => at = self.parent(a), + Some(v) => return v, + None => break, + } + } + "normal".into() + } + + /// The Character Direction of the characters of element `n` inside a ``, and what it + /// leaves out (SVG 1.1 §10.7.4, + /// https://www.w3.org/TR/SVG11/text.html#RelationshipWithBi-directionality, and CSS Writing + /// Modes 3 §2.2, https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi). A ``, + /// `` or `` whose `unicode-bidi` opens an embedding gives its characters an + /// embedding in its `direction`; one that opens none (`normal`) leaves them in its parent's. + /// The text model holds one level of embedding and no override or isolation: overrides and + /// isolates become embeddings, `plaintext` an embedding in the direction of its first strong + /// character, and an embedding inside another an embedding of its own, each with a note. The + /// `` itself sets the paragraph direction ([`pin_direction`]) and no Character Direction. + fn char_direction(&self, n: XNode) -> (Option, Vec<&'static str>) { + let name = n.tag_name().name(); + let bidi = self.unicode_bidi(n); + if !matches!(name, "tspan" | "textPath" | "a") { + let notes = + if name == "text" && matches!(bidi.as_str(), "bidi-override" | "isolate-override") { vec![BIDI_TEXT_OVERRIDE] } else { vec![] }; + return (None, notes); + } + let parent = n.parent_element().map(|p| self.elem(p)); + let inherited = parent.as_ref().and_then(|p| p.style.char_direction); + let mut notes = parent.map_or_else(Vec::new, |p| p.bidi_notes.clone()); + let rtl = |rtl: bool| Some(if rtl { ParaDirection::RightToLeft } else { ParaDirection::LeftToRight }); + let own = || rtl(self.prop(n, "direction").is_some_and(|d| d.trim() == "rtl")); + let (dir, note) = match bidi.as_str() { + "embed" => (own(), None), + "bidi-override" => (own(), Some(BIDI_OVERRIDE)), + "isolate" => (own(), Some(BIDI_ISOLATE)), + "isolate-override" => (own(), Some(BIDI_ISOLATE_OVERRIDE)), + "plaintext" => (rtl(self.first_strong_rtl(n) == Some(true)), Some(BIDI_PLAINTEXT)), + // `normal`, and a value CSS ignores. + _ => return (inherited, notes), + }; + for note in note.into_iter().chain(inherited.map(|_| BIDI_NESTED)) { + if !notes.contains(¬e) { + notes.push(note); + } + } + (dir, notes) + } + + /// Whether the first strong character (UAX #9 P2) of the characters [`Self::collect`] reads + /// from element `n` is right to left; `None` when they hold none. Each element is read once. + fn first_strong_rtl(&self, n: XNode) -> Option { + if let Some(s) = self.strong.borrow().get(&n.id()) { + return *s; + } + let s = n.children().find_map(|c| { + if c.is_text() { + text_first_strong_rtl(c.text().unwrap_or("")) + } else if self.is_inline_child(c) { + self.first_strong_rtl(c) + } else { + None + } + }); + self.strong.borrow_mut().insert(n.id(), s); + s + } + + /// Whether child `c` of a `` content element holds characters of the text: a shown + /// ``, `` or ``. + fn is_inline_child(&self, c: XNode) -> bool { + c.is_element() + && c.tag_name().namespace() == Some(SVG_NS) + && matches!(c.tag_name().name(), "tspan" | "textPath" | "a") + && self.css.own(c, "display").as_deref() != Some("none") + } + /// Characters of `el` and its ``, `` and `` children, with white space /// handled like `xml:space` (newlines and tabs become spaces; runs of spaces collapse unless /// preserved). @@ -943,11 +1049,7 @@ impl Ctx { } out.chars.push((ch, el)); } - } else if c.is_element() - && c.tag_name().namespace() == Some(SVG_NS) - && matches!(c.tag_name().name(), "tspan" | "textPath" | "a") - && self.css.own(c, "display").as_deref() != Some("none") - { + } else if self.is_inline_child(c) { if c.tag_name().name() == "textPath" { out.path.get_or_insert(c); } @@ -1234,6 +1336,16 @@ impl Ctx { } }) .collect(); + // What the Character Direction leaves out of the file's `unicode-bidi`, once for the text. + let mut bidi_notes: Vec<&str> = vec![]; + for (el, _) in cs.spans.iter().filter(|(_, r)| !r.is_empty() && r.start < n) { + for note in &self.elem(*el).bidi_notes { + if !bidi_notes.contains(note) { + bidi_notes.push(note); + } + } + } + warnings.extend(bidi_notes.into_iter().map(|note| format!("{label}: {note}"))); let under = stroke_under(&mut cells, label, warnings); let path = cs.path.and_then(|tp| match self.text_path(xml, tp) { @@ -1244,7 +1356,16 @@ impl Ctx { } }); let vertical = self.prop(t, "writing-mode").is_some_and(|m| m.starts_with("tb") || m.starts_with("vertical")); - let rtl = self.prop(t, "direction").is_some_and(|d| d.trim() == "rtl"); + // With `unicode-bidi: plaintext`, the text runs in the direction of its first strong + // character, and each paragraph in that of its own ([`pin_first_strong`]). + let plaintext = self.unicode_bidi(t) == "plaintext"; + let rtl = if plaintext { self.first_strong_rtl(t) == Some(true) } else { self.prop(t, "direction").is_some_and(|d| d.trim() == "rtl") }; + let pin = |obj: &mut TextObject| { + pin_direction(obj, rtl); + if plaintext { + pin_first_strong(obj); + } + }; // Where the chunk starting at character `i` sits on its anchor: 0 its left (or top) edge, // 0.5 its middle, 1 its right (or bottom) edge; on a path, its edge toward the path's // start, its middle or its edge toward the path's end. In horizontal type `start` and @@ -1258,7 +1379,15 @@ impl Ctx { Some("end") => 1.0, _ => 0.0, }; - if !vertical && self.prop(el, "direction").is_some_and(|d| d.trim() == "rtl") { 1.0 - a } else { a } + // With `unicode-bidi: plaintext` on the ``, a chunk runs in the direction of its + // first strong character, left to right without one ([`pin_first_strong`]). + let chunk_rtl = || { + let end = (i + 1..n).find(|&j| x[j].is_some() || y[j].is_some()).unwrap_or(n); + let chunk: String = cs.chars[i..end].iter().map(|c| c.0).collect(); + text_first_strong_rtl(&chunk) == Some(true) + }; + let rtl = if plaintext { chunk_rtl() } else { self.prop(el, "direction").is_some_and(|d| d.trim() == "rtl") }; + if !vertical && rtl { 1.0 - a } else { a } }; let anchor = anchor_of(0); self.fit_lengths(&cs.spans, &mut cells, vertical, label, warnings); @@ -1306,7 +1435,7 @@ impl Ctx { obj.xf = Affine::IDENTITY; } else { let origin = Point::new(x[0].unwrap_or(0.0) + dx[0], y[0].unwrap_or(0.0) + dy[0]); - let (justify, left, starts) = lines(&mut cells, &x, &y, &dx, &dy, &anchor_of, rtl, origin.x, &mut breaks); + let (justify, left, starts) = lines(&mut cells, &x, &y, &dx, &dy, &anchor_of, &pin, origin.x, &mut breaks); obj.para.justify = justify; obj.xf = Affine::translate((finite(left), finite(origin.y))); // Lines anchored differently or at different x (paragraphs aligned or indented @@ -1332,7 +1461,7 @@ impl Ctx { if !para_styles.is_empty() { obj.set_paragraph_styles(para_styles); } - pin_direction(&mut obj, rtl); + pin(&mut obj); keep_reader_anchors(&mut obj); // The placeholder covers the text (laid out when a paint server needs its bounding box). let b = if paints.is_empty() { obj.bounds() } else { Some(obj.xf.transform_rect_bbox(measure(&obj).bounds)) }.unwrap_or_default(); @@ -1437,9 +1566,9 @@ fn leading_tolerance(spacing: f64) -> f64 { /// Point type: line breaks, baseline shifts and kerning from the positioning lists. Returns the /// justification, the x of the first line's left (or anchor) edge and each line's first /// character, alignment (as for [`justify_of`]) and anchor x. `anchor_of` gives where the chunk -/// starting at a character sits on its anchor, and `rtl` whether the text's `direction` is `rtl` -/// ([`pin_direction`]). When characters are placed on their own, every line is left-aligned and -/// its anchor x is its left edge. +/// starting at a character sits on its anchor, and `pin` sets the paragraph directions of the +/// text ([`pin_direction`]). When characters are placed on their own, every line is left-aligned +/// and its anchor x is its left edge. #[allow(clippy::too_many_arguments)] fn lines( cells: &mut [Cell], @@ -1448,7 +1577,7 @@ fn lines( dx: &[f64], dy: &[f64], anchor_of: &dyn Fn(usize) -> f64, - rtl: bool, + pin: &dyn Fn(&mut TextObject), x0: f64, breaks: &mut Vec, ) -> (Justify, f64, Vec<(usize, f64, f64)>) { @@ -1519,7 +1648,7 @@ fn lines( for round in 0..4 { let (runs, bytes) = assemble(cells, breaks); let mut text = with_runs(runs.0); - pin_direction(&mut text, rtl); + pin(&mut text); let (pens, ends) = pens(&measure(&text), &bytes); // The edges of each line's first chunk, found once a round. let first: Vec> = lines.iter().map(|&(s, _)| edges(s, &pens, &ends)).collect(); @@ -1621,6 +1750,30 @@ fn pin_direction(obj: &mut TextObject, rtl: bool) { } } +/// `unicode-bidi: plaintext` on a `` (CSS Writing Modes 3 §2.2, +/// https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi): each paragraph of `obj` is pinned to +/// the direction of its first strong character, and left to right without one (UAX #9 P2, P3). +fn pin_first_strong(obj: &mut TextObject) { + let text = obj.plain_text(); + let mut styles = obj.paragraph_styles(); + for (para, style) in text.split('\n').zip(styles.iter_mut()) { + let rtl = text_first_strong_rtl(para) == Some(true); + style.direction = Some(if rtl { ParaDirection::RightToLeft } else { ParaDirection::LeftToRight }); + } + obj.set_paragraph_styles(styles); +} + +/// Whether the first strong character (UAX #9 P2) of `text` is right to left; `None` when it holds +/// none. Paragraph separators count as spaces, so all of `text` is searched. +fn text_first_strong_rtl(text: &str) -> Option { + let line: String = text.chars().map(|c| if matches!(c, '\n' | '\r' | '\u{1C}'..='\u{1E}' | '\u{85}' | '\u{2029}') { ' ' } else { c }).collect(); + if vectorcraft_text::paragraph_is_rtl(&line, None) { + return Some(true); + } + // Without a strong character of its own, a right-to-left letter after it is the first one. + (!vectorcraft_text::paragraph_is_rtl(&format!("{line}\u{5D0}"), None)).then_some(false) +} + /// A left-aligned point text of `runs` at the origin (for measuring). fn with_runs(runs: Vec) -> TextObject { let mut t = TextObject::point(Point::ZERO, "", CharStyle::default()); diff --git a/crates/svg/tests/it/blend_export.rs b/crates/svg/tests/it/blend_export.rs index 3804d30ed2..dd86f318f5 100644 --- a/crates/svg/tests/it/blend_export.rs +++ b/crates/svg/tests/it/blend_export.rs @@ -262,3 +262,93 @@ fn a_mask_region_covers_a_blend_as_written() { let [x, y, w, h] = ["x", "y", "width", "height"].map(|a| mask.attribute(a).and_then(|v| v.parse::().ok()).unwrap_or(f64::NAN)); assert!(x <= 0.0 && y <= 0.0 && x + w >= 95.0 && y + h >= 35.0 && w < 200.0 && h < 200.0, "{x} {y} {w} {h} in {svg}"); } + +/// A knockout group of `n` red, green and blue squares at 50% opacity that all overlap, over an +/// opaque backdrop. `knockout_shape`: their opacity defines their knockout shape. +fn knockout_squares(n: usize, knockout_shape: bool) -> Document { + let mut d = Document::new(200.0, 200.0); + let backdrop = rect(&mut d, Rect::new(0.0, 0.0, 200.0, 200.0), [230, 200, 50]); + add(&mut d, backdrop); + let colors = [[255, 0, 0], [0, 200, 0], [0, 0, 255]]; + let squares = (0..n) + .map(|i| { + let x = (i % 20) as f64 * 2.0; + let mut s = rect(&mut d, Rect::new(20.0 + x, 50.0, 120.0 + x, 150.0), colors[i % 3]); + (s.opacity, s.knockout_shape) = (0.5, knockout_shape); + std::sync::Arc::new(s) + }) + .collect(); + let mut group = Node::group(d.alloc_id(), squares); + group.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, group); + d +} + +/// The deepest an element of `svg` nests, counting the root element as 1. +fn element_depth(svg: &str) -> usize { + let xml = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }).unwrap(); + xml.descendants().filter(|n| n.is_element()).map(|n| n.ancestors().filter(|a| a.is_element()).count()).max().unwrap_or(0) +} + +/// The elements of a knockout group are siblings, each in one mask of the overlapping elements +/// above it, however many there are: 200 translucent elements stay well within the nesting the +/// web build reads (34 levels), and the file reads back with nothing merged. Where the elements' +/// shapes are the area they paint, the file draws as the canvas does. +#[test] +fn a_knockout_group_of_many_translucent_elements_stays_shallow() { + for knockout_shape in [false, true] { + let d = knockout_squares(200, knockout_shape); + let (svg, warnings) = vectorcraft_svg::export_with_report(&d, &ExportOptions::default()); + assert!(warnings.is_empty(), "{warnings:?}"); + assert_eq!(svg.matches("Wave"#, 0.0), (r#"Wave"#, 0.0), (r#"Wave"#, 1.0), + // With `unicode-bidi: plaintext`, the direction of the chunk's first strong character. + (r#"אבג"#, 1.0), + (r#"12 אב"#, 0.0), + (r#"Wave"#, 0.0), + (r#"12"#, 1.0), ] { let d = open(body); let (left, right) = line_edges(only_text(&d))[0]; @@ -464,6 +469,169 @@ fn rtl_lines_use_each_chunk_first_character_direction_and_anchor() { assert!(close((e[3].0 + e[3].1) / 2.0, 200.0, 1e-6), "a middle line is centered on 200: {e:?}"); } +/// The Character Direction of the run holding `part`. +fn char_direction_of(t: &TextObject, part: &str) -> Option { + t.runs.iter().find(|r| r.text.contains(part)).unwrap_or_else(|| panic!("no run holds {part:?}: {:?}", t.runs)).style.char_direction +} + +/// SVG 1.1 §10.7.4: a `` whose `unicode-bidi` is `embed` or `bidi-override` opens a bidi +/// embedding in its `direction`; with `normal`, its `direction` leaves the ordering alone. The +/// embedding becomes the Character Direction of its characters, and the `` keeps its own +/// paragraph direction. https://www.w3.org/TR/SVG11/text.html#RelationshipWithBi-directionality +#[test] +fn a_tspan_bidi_embedding_becomes_its_character_direction() { + use vectorcraft_doc::ParaDirection::{LeftToRight as Ltr, RightToLeft as Rtl}; + let read = |body: &str| { + let (d, w) = import_with_report(&svg(body)).unwrap_or_else(|e| panic!("{e}")); + (only_text(&d).clone(), w) + }; + // (outer direction, inner direction, unicode-bidi, the inner run's Character Direction, a warning) + for (outer, inner, bidi, want, warns) in [ + ("ltr", "rtl", "embed", Some(Rtl), false), + ("rtl", "ltr", "embed", Some(Ltr), false), + ("ltr", "rtl", "bidi-override", Some(Rtl), true), + ("rtl", "ltr", "bidi-override", Some(Ltr), true), + ("ltr", "rtl", "normal", None, false), + ("rtl", "ltr", "normal", None, false), + ("ltr", "rtl", "EMBED", Some(Rtl), false), + ("ltr", "rtl", "sideways", None, false), + ] { + let (t, w) = read(&format!( + r#"ab 1 2 cd"# + )); + let case = format!("{outer} text, {inner} {bidi} tspan"); + assert_eq!(t.plain_text(), "ab 1 2 cd", "{case}: the characters stay in logical order"); + assert_eq!(char_direction_of(&t, "1 2"), want, "{case}: {:?}", t.runs); + assert_eq!(char_direction_of(&t, "ab"), None, "{case}"); + assert_eq!(t.para_at(0).direction == Some(Rtl), outer == "rtl", "{case}: the paragraph keeps the text's direction"); + assert_eq!(w.iter().any(|w| w.contains("bidi-override") && w.contains("not supported")), warns, "{case}: {w:?}"); + if !warns { + assert!(w.is_empty(), "{case}: {w:?}"); + } + } + // In left-to-right text, digits in a right-to-left embedding are drawn right to left. + let order = |bidi: &str| { + let (t, _) = read(&format!(r#"ab 1 2"#)); + let lay = layout(FontDb::global(), &t); + let x = |byte: usize| lay.glyphs.iter().find(|g| g.byte == byte).map(|g| g.origin.x).unwrap_or_else(|| panic!("no glyph at {byte}")); + x(3) < x(5) + }; + assert!(!order("embed"), "an embedding draws 2 before 1"); + assert!(order("normal"), "without one, 1 comes first"); +} + +/// `unicode-bidi` is not inherited: a `` without one stays in its parent's embedding, and +/// `inherit` takes the parent's value. CSS rules and `style` set it as they set any property. +#[test] +fn unicode_bidi_is_not_inherited_and_reads_from_the_cascade() { + use vectorcraft_doc::ParaDirection::{LeftToRight as Ltr, RightToLeft as Rtl}; + let read = |body: &str| { + let (d, w) = import_with_report(&svg(body)).unwrap_or_else(|e| panic!("{e}")); + (only_text(&d).clone(), w) + }; + // An inner `` with only a `direction` stays in the outer embedding. + let (t, w) = read(r#"ab 12 34 cd"#); + assert_eq!((char_direction_of(&t, "12"), char_direction_of(&t, "34"), char_direction_of(&t, "cd")), (Some(Rtl), Some(Rtl), None), "{:?}", t.runs); + assert!(w.is_empty(), "{w:?}"); + // `inherit` opens an embedding of its own, inside the outer one: kept one level deep, with a warning. + let (t, w) = read( + r#"ab 12 34"#, + ); + assert_eq!((char_direction_of(&t, "12"), char_direction_of(&t, "34")), (Some(Rtl), Some(Ltr)), "{:?}", t.runs); + assert!(w.len() == 1 && w[0].contains("inside another"), "{w:?}"); + // A `unicode-bidi` on the `` or a group is not passed on to the ``. + let (t, _) = read(r#"ab 12"#); + assert!(t.runs.iter().all(|r| r.style.char_direction.is_none()), "{:?}", t.runs); + // From a CSS rule and from `style`; `direction` inherits from the ``. + for body in [ + r#"ab 12"#, + r#"ab 12"#, + ] { + let (t, w) = read(body); + assert_eq!(char_direction_of(&t, "12"), Some(Rtl), "{body}: {:?}", t.runs); + assert_eq!(char_direction_of(&t, "ab"), None, "{body}: {:?}", t.runs); + assert!(w.is_empty(), "{body}: {w:?}"); + } + // On a path and in a link too. + let (t, _) = read( + r##"ab 12 34"##, + ); + assert!(matches!(t.kind, TextKind::OnPath { .. })); + assert_eq!((char_direction_of(&t, "12"), char_direction_of(&t, "34")), (Some(Ltr), Some(Rtl)), "{:?}", t.runs); +} + +/// The text model holds embeddings only: isolates and overrides become embeddings in their +/// `direction`, and `plaintext` one in the direction of its first strong character, each with a +/// warning once a text (CSS Writing Modes 3 §2.2, +/// https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi). +#[test] +fn unicode_bidi_values_the_text_model_cannot_hold_are_reported() { + use vectorcraft_doc::ParaDirection::{LeftToRight as Ltr, RightToLeft as Rtl}; + for (bidi, dir, text, want, says) in [ + ("isolate", "rtl", "12", Some(Rtl), "isolation is not supported"), + ("isolate-override", "rtl", "12", Some(Rtl), "isolation and override are not supported"), + ("plaintext", "ltr", "\u{5D0}\u{5D1}", Some(Rtl), "first strong character"), + ("plaintext", "rtl", "12 ab", Some(Ltr), "first strong character"), + ] { + let body = format!( + r#"ab {text} {text}"# + ); + let (d, w) = import_with_report(&svg(&body)).unwrap_or_else(|e| panic!("{e}")); + let t = only_text(&d); + assert_eq!(char_direction_of(t, text), want, "{bidi}: {:?}", t.runs); + assert!(w.len() == 1 && w[0].starts_with("text 't': ") && w[0].contains(&format!("\"{bidi}\"")) && w[0].contains(says), "{bidi}: {w:?}"); + } + // `plaintext` looks for the first strong character among the characters imported, however far in. + let far = format!("{}\u{5D0}\u{5D1}", "1 ".repeat(1500)); + for (body, part, want) in [ + (r#"אבab"#.to_string(), "ab", Ltr), + (format!("{far}"), "\u{5D0}", Rtl), + (format!("\n12 {far}"), "\u{5D0}", Rtl), + ] { + let (d, _) = + import_with_report(&svg(&format!(r#"x {body}"#))).unwrap(); + let t = only_text(&d); + assert_eq!(char_direction_of(t, part), Some(want), "{body}: {:?}", t.runs); + } + // `plaintext` on the ``: each paragraph runs in the direction of its first strong character. + for (body, want) in [ + ("12 \u{5D0}\u{5D1} cd", vec![Some(Rtl)]), + (r#"12 ab"#, vec![Some(Ltr)]), + ( + "ab\u{5D0}\u{5D1} cd", + vec![Some(Ltr), Some(Rtl)], + ), + // A paragraph without a strong character runs left to right. + ( + "\u{5D0}\u{5D1}1-2", + vec![Some(Rtl), Some(Ltr)], + ), + ] { + let (d, w) = import_with_report(&svg(body)).unwrap(); + let t = only_text(&d); + let got: Vec<_> = (0..t.paragraph_styles().len()).map(|i| t.para_at(i).direction).collect(); + assert_eq!(got, want, "{body}"); + assert!(w.is_empty(), "{body}: {w:?}"); + } + // Absolutely placed characters sit on their x in the direction of their own paragraph. + let (d, _) = import_with_report(&svg( + r#"אבab אב cdef"#, + )) + .unwrap(); + let t = only_text(&d); + assert_eq!((t.para_at(0).direction, t.para_at(1).direction), (Some(Rtl), Some(Ltr))); + let lay = layout(FontDb::global(), t); + for (byte, x) in [(5, 100.0), (15, 300.0)] { + let g = lay.glyphs.iter().find(|g| g.byte == byte).unwrap_or_else(|| panic!("no glyph at {byte}")); + let left = (t.xf * g.origin).x; + assert!(close(left, x, 0.05), "the character at {byte} starts at {left}, not {x}"); + } + // An override on the `` keeps its paragraph direction. + let (d, w) = import_with_report(&svg(r#"ab"#)).unwrap(); + assert_eq!(only_text(&d).para_at(0).direction, Some(Rtl)); + assert!(w.len() == 1 && w[0].contains("override on the text"), "{w:?}"); +} + /// A line starts at its own x, also when it is anchored as the line before it is and when another /// line holds absolutely placed characters. #[test] diff --git a/crates/svg/tests/it/text_layout.rs b/crates/svg/tests/it/text_layout.rs index 0678f39f8a..c53318c358 100644 --- a/crates/svg/tests/it/text_layout.rs +++ b/crates/svg/tests/it/text_layout.rs @@ -289,6 +289,40 @@ fn character_direction_is_written_on_its_runs() { } } +/// A run set Left to Right keeps its Character Direction when its SVG is opened again, on a line +/// and on a path. +#[test] +fn character_direction_reads_back() { + use vectorcraft_doc::ParaDirection; + let ltr = CharStyle { char_direction: Some(ParaDirection::LeftToRight), ..style(12.0) }; + let mut on_path = TextObject::point(Point::ZERO, "", style(12.0)); + on_path.runs = vec![run("Due ", style(12.0)), run("21 10 2026", ltr.clone())]; + let mut line = BezPath::new(); + line.move_to((20.0, 100.0)); + line.line_to((280.0, 100.0)); + on_path.kind = TextKind::OnPath { path: PathData::from_bezpath(&line), start: 0.0, end: None }; + let mut path_doc = Document::new(300.0, 200.0); + let n = Node::new(path_doc.alloc_id(), NodeKind::Text(Box::new(on_path))); + let l = path_doc.layers[0].id; + path_doc.insert(Some(l), 0, n).unwrap(); + let area = area_doc(vec![run("Due ", style(12.0)), run("21 10 2026", ltr), run(" ok", style(12.0))], 280.0, Justify::Left); + for (name, d) in [("area", area), ("path", path_doc)] { + let svg = export(&d, &ExportOptions::default()); + let back = vectorcraft_svg::import(&svg).unwrap_or_else(|e| panic!("{name}: {e}\n{svg}")); + let mut texts = vec![]; + back.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + texts.push((**t).clone()); + } + }); + assert_eq!(texts.len(), 1, "{name}: {svg}"); + let t = &texts[0]; + let dir = |part: &str| t.runs.iter().find(|r| r.text.contains(part)).map(|r| r.style.char_direction); + assert_eq!(dir("2026"), Some(Some(ParaDirection::LeftToRight)), "{name}: {:?}\n{svg}", t.runs); + assert_eq!(dir("Due"), Some(None), "{name}: {:?}", t.runs); + } +} + /// Digits are written as drawn: the style's numeral system, not the digits that were typed (as /// All Caps writes capitals). #[test] diff --git a/crates/text/src/colr.rs b/crates/text/src/colr.rs index a3c6bf6e80..66281f70f0 100644 --- a/crates/text/src/colr.rs +++ b/crates/text/src/colr.rs @@ -12,7 +12,7 @@ use skrifa::outline::DrawSettings; use skrifa::raw::TableProvider; use skrifa::raw::types::BoundingBox; use skrifa::{GlyphId, MetadataProvider}; -use vectorcraft_doc::color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint}; +use vectorcraft_doc::color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientSpread, GradientStop, Paint}; use vectorcraft_doc::{Appearance, FillLayer, ImageBlob, Node, NodeId, NodeKind}; use vectorcraft_geom::PathData; use vello_cpu::peniko; @@ -279,8 +279,10 @@ impl<'a> Tree<'a> { }) .collect() }; - let gradient = |kind, stops: Vec, geom| { - let mut paint = GradientPaint::new(Gradient { kind, stops, ..Gradient::default() }); + let gradient = |kind, stops: Vec, spread, geom| { + // COLR repeats the interval from the first stop to the last, and one run of the + // stops spans the gradient's vector. + let mut paint = GradientPaint::new(Gradient { kind, stops, spread, ..Gradient::default() }); paint.geom = Some(geom); Paint::Gradient(Box::new(paint)) }; @@ -290,22 +292,22 @@ impl<'a> Tree<'a> { Some((Paint::solid(c), a)) } Brush::LinearGradient { p0, p1, color_stops, extend } => { - if *extend != Extend::Pad { - self.unsupported.get_or_insert("a repeating or reflecting gradient"); + let spread = spread(*extend); + let (p0, p1) = (point(*p0), point(*p1)); + let stops = stops(color_stops, self); + // Without a stop, the others would repeat over a different interval. + if !spread.is_pad() && stops.len() < color_stops.len() { + self.unsupported.get_or_insert("a repeating or reflecting gradient with a stop missing from the palette"); return None; } - let (p0, p1) = (point(*p0), point(*p1)); - let (stops, first, last) = normalized(stops(color_stops, self))?; + let (stops, first, last) = normalized(stops)?; // Stops past 0 and 1 move the ends instead. let mut geom = GradientGeom { start: p0.lerp(p1, f64::from(first)), end: p0.lerp(p1, f64::from(last)), aspect: 1.0, focal: None }; geom.transform(xf, GradientKind::Linear); - Some((gradient(GradientKind::Linear, stops, geom), 1.0)) + Some((gradient(GradientKind::Linear, stops, spread, geom), 1.0)) } Brush::RadialGradient { c0, r0, c1, r1, color_stops, extend } => { - if *extend != Extend::Pad { - self.unsupported.get_or_insert("a repeating or reflecting gradient"); - return None; - } + let spread = spread(*extend); let (c0, c1, r0, r1) = (point(*c0), point(*c1), f64::from(*r0), f64::from(*r1)); let concentric = (c0 - c1).hypot() < 1e-6; if (r0 > 1e-6 && !concentric) || r1 <= r0 || (c0 - c1).hypot() >= r1 { @@ -317,13 +319,27 @@ impl<'a> Tree<'a> { self.unsupported.get_or_insert("a radial gradient with stops past its circles"); return None; } + if !spread.is_pad() { + if stops.len() < color_stops.len() { + self.unsupported.get_or_insert("a repeating or reflecting gradient with a stop missing from the palette"); + return None; + } + // A run of the stops is the whole way from the center to the circle: one that + // starts at a circle (a first stop past the center comes as one) repeats over + // less of it. The normalized ends can be a rounding error off 0 and 1. + let (lo, hi) = stops.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), s| (lo.min(s.offset), hi.max(s.offset))); + if r0.abs() > 1e-6 || lo.abs() > 1e-4 || (hi - 1.0).abs() > 1e-4 { + self.unsupported.get_or_insert("a repeating or reflecting radial gradient that doesn't start at its center"); + return None; + } + } // A start circle around the centre starts the stops further out. for s in &mut stops { s.offset = ((r0 + f64::from(s.offset) * (r1 - r0)) / r1) as f32; } let mut geom = GradientGeom { start: c1, end: c1 + Vec2::new(r1, 0.0), aspect: 1.0, focal: (!concentric).then_some(c0) }; geom.transform(xf, GradientKind::Radial); - Some((gradient(GradientKind::Radial, stops, geom), 1.0)) + Some((gradient(GradientKind::Radial, stops, spread, geom), 1.0)) } Brush::SweepGradient { .. } => { self.unsupported.get_or_insert("a sweep gradient"); @@ -345,6 +361,16 @@ impl<'a> Tree<'a> { } } +/// A COLR extend mode as the gradient's spread; an unknown one pads, as the COLR specification +/// says. +fn spread(extend: Extend) -> GradientSpread { + match extend { + Extend::Repeat => GradientSpread::Repeat, + Extend::Reflect => GradientSpread::Reflect, + _ => GradientSpread::Pad, + } +} + /// `stops` sorted, from 0 to 1 (as the gradient's ends then move), and where the first and last /// were; `None` without stops. fn normalized(mut stops: Vec) -> Option<(Vec, f32, f32)> { diff --git a/crates/text/src/fontdb/names.rs b/crates/text/src/fontdb/names.rs index 47faeeddae..8b778f7805 100644 --- a/crates/text/src/fontdb/names.rs +++ b/crates/text/src/fontdb/names.rs @@ -53,6 +53,7 @@ pub(super) fn script_families(c: char) -> &'static [&'static str] { "Lohit Devanagari", "Mangal", ], + Script::Gujarati => &["Gujarati Sangam MN", "GujaratiMT", "Nirmala UI", "Shruti", "Noto Sans Gujarati", "Noto Sans Gujarati UI"], _ => &[], } } @@ -164,6 +165,13 @@ mod tests { assert!(script_families('a').is_empty()); } + /// Gujarati takes a font that draws it (macOS Gujarati Sangam MN, Windows Nirmala UI or Shruti). + #[test] + fn gujarati_prefers_fonts_made_for_it() { + assert_eq!(script_families('ગ').first(), Some(&"Gujarati Sangam MN")); + assert!(script_families('ગ').contains(&"Nirmala UI")); + } + #[test] fn a_name_table_of_many_records_is_cut_short() { let many: Vec<(u16, &str)> = (0..2000).map(|_| (1, "Short")).collect(); diff --git a/crates/text/src/layout/shaped.rs b/crates/text/src/layout/shaped.rs index 72f6ed8733..63005364fb 100644 --- a/crates/text/src/layout/shaped.rs +++ b/crates/text/src/layout/shaped.rs @@ -5,6 +5,11 @@ //! line. Each line the composer tries is placed from where the lines above it left the pen, at its //! own tallest metrics, as the line loop in [`super::flow`] places a composed line: the span it //! gets is the room across its whole height. +//! +//! A paragraph the frame cannot hold is still composed whole: a line the frame has no room for, +//! and every line after it, is measured as wide as the span of the last line set in the frame, +//! less its own indents. The line loop sets only the lines that fit, and the rest overflows or +//! goes on in the next threaded frame, which composes it again at its own measures. use std::cell::Cell; use std::collections::HashMap; @@ -63,19 +68,33 @@ struct Start<'r> { /// The paragraph's first line (it takes the first-line indent). first: bool, pen: Pen<'r>, - /// The line placed at each of the metrics it was tried with: its width and the pen after it, - /// or `None` when the frame has no room for it. - placed: Vec<(Metrics, Option<(f64, Pen<'r>)>)>, + /// The width of the span the line above was set in, `None` for the paragraph's first line. + span: Option, + /// The line above was past the frame's end: this line is too, at the width of `span`. + past: bool, + /// The line placed at each of the metrics it was tried with, or `None` when it can't be set. + placed: Vec<(Metrics, Option>)>, /// The metrics of glyphs `pos..to`, measured so far. to: usize, m: Option, } -/// What a line's placement depends on: its first glyph, whether it is the paragraph's first line, -/// and the pen's cell, baselines, pending space, narrowest word without room and further spans. -type Key = (usize, bool, usize, Option, Option, u64, u64, Vec<[u64; 3]>); +/// A line set from a [`Start`]: its width, the pen after it, the width of the span it was set +/// in, and whether it is past the frame's end (measured, but not in the frame). +#[derive(Clone)] +struct Placed<'r> { + width: f64, + pen: Pen<'r>, + span: f64, + past: bool, +} -fn key(pos: usize, first: bool, pen: &Pen<'_>) -> Key { +/// What a line's placement depends on: its first glyph, whether it is the paragraph's first line, +/// the pen's cell, baselines, pending space, narrowest word without room and further spans, the +/// span of the line above and whether the line is past the frame's end. +type Key = (usize, bool, usize, Option, Option, u64, u64, Vec<[u64; 3]>, Option, bool); + +fn key(pos: usize, first: bool, pen: &Pen<'_>, span: Option, past: bool) -> Key { ( pos, first, @@ -85,6 +104,8 @@ fn key(pos: usize, first: bool, pen: &Pen<'_>) -> Key { pen.pending.to_bits(), pen.no_room_for.to_bits(), pen.queued.iter().map(|&(b, x0, x1)| [b.to_bits(), x0.to_bits(), x1.to_bits()]).collect(), + span.map(f64::to_bits), + past, ) } @@ -159,13 +180,14 @@ impl<'a, 'r> Shaped<'a, 'r> { } } - /// The start of a line at glyph `pos` with `pen`: an existing one when its placement is the same. - fn start(&mut self, pos: usize, first: bool, pen: Pen<'r>) -> usize { - let k = key(pos, first, &pen); + /// The start of a line at glyph `pos` with `pen`, after a line set in a span `span` wide (past + /// the frame's end with `past`): an existing one when its placement is the same. + fn start(&mut self, pos: usize, first: bool, pen: Pen<'r>, span: Option, past: bool) -> usize { + let k = key(pos, first, &pen, span, past); if let Some(&id) = self.known.get(&k) { return id; } - self.starts.push(Start { pos, first, pen, placed: vec![], to: pos, m: None }); + self.starts.push(Start { pos, first, pen, span, past, placed: vec![], to: pos, m: None }); let id = self.starts.len() - 1; self.known.insert(k, id); id @@ -177,7 +199,7 @@ impl Measure for Shaped<'_, '_> { fn first(&mut self) -> usize { let pen = self.pen.clone(); - self.start(0, !self.continued, pen) + self.start(0, !self.continued, pen, None, false) } fn line(&mut self, &id: &usize, pos: usize, end: usize) -> Result, Exhausted> { @@ -186,14 +208,14 @@ impl Measure for Shaped<'_, '_> { } self.probes += 1; let Some(start) = self.starts.get_mut(id) else { return Ok(None) }; - let Some((width, pen)) = line(&self.para, start, pos, end)? else { return Ok(None) }; - Ok(Some((width, self.start(end, false, pen)))) + let Some(placed) = line(&self.para, start, pos, end)? else { return Ok(None) }; + Ok(Some((placed.width, self.start(end, false, placed.pen, Some(placed.span), placed.past)))) } /// A longer line is placed as this one is, and gets the same room, unless a glyph after it - /// changes the line's tallest metrics. + /// changes the line's tallest metrics. Past the frame's end every line is as wide. fn may_widen(&mut self, &id: &usize, pos: usize, end: usize) -> bool { - let Some(start) = self.starts.get(id) else { return false }; + let Some(start) = self.starts.get(id).filter(|start| !start.past) else { return false }; let m = if start.pos == pos && start.to == end { start.m } else { self.para.sg.get(pos..end).and_then(Metrics::max) }; match (m, self.rest.get(end).copied().flatten()) { (Some(m), Some(rest)) => m.union(rest) != m, @@ -212,12 +234,14 @@ impl Drop for Shaped<'_, '_> { } } -/// The width of the line of glyphs `pos..end` from `start`, and the pen after it; `None` when -/// the frame has no room for it. -fn line<'r>(p: &Para<'_>, start: &mut Start<'r>, pos: usize, end: usize) -> Result)>, Exhausted> { +/// The line of glyphs `pos..end` from `start`; `None` when it can't be set. +fn line<'r>(p: &Para<'_>, start: &mut Start<'r>, pos: usize, end: usize) -> Result>, Exhausted> { if start.pos != pos || end <= pos { return Ok(None); } + if start.past { + return Ok(past_end(p, start)); + } // The line's tallest metrics, measured on from the glyphs already measured. if end < start.to { start.m = p.sg.get(pos..end).and_then(Metrics::max); @@ -239,9 +263,18 @@ fn line<'r>(p: &Para<'_>, start: &mut Start<'r>, pos: usize, end: usize) -> Resu Ok(placed) } -/// The line from `start` placed with metrics `m`: its width and the pen after it, as the line loop -/// places and settles it; `None` when no frame cell has room for it. -fn place<'r>(p: &Para<'_>, start: &Start<'r>, m: Metrics) -> Result)>, Exhausted> { +/// A line from `start` past the frame's end: as wide as the span of the last line set in the frame, +/// less its indents, and the pen left where the frame ran out. `None` when the paragraph has no line +/// in the frame to measure it by. +fn past_end<'r>(p: &Para<'_>, start: &Start<'r>) -> Option> { + let span = start.span.filter(|span| span.is_finite())?; + let (ind_l, ind_r) = line_indents(p.para, start.first, p.mirror); + Some(Placed { width: span - ind_l - ind_r, pen: start.pen.clone(), span, past: true }) +} + +/// The line from `start` placed with metrics `m`, as the line loop places and settles it; past the +/// frame's end ([`past_end`]) when no frame cell has room for it. +fn place<'r>(p: &Para<'_>, start: &Start<'r>, m: Metrics) -> Result>, Exhausted> { let (ind_l, ind_r) = line_indents(p.para, start.first, p.mirror); let word = word_width(p.sg, start.pos, p.line_end, p.para.kinsoku) - (p.start_credit)(start.pos); let mut pen = start.pen.clone(); @@ -262,12 +295,12 @@ fn place<'r>(p: &Para<'_>, start: &Start<'r>, m: Metrics) -> Result return Ok(None), // Out of steps: the frame may still have room further down. None if p.steps.get() == 0 => return Err(Exhausted), - None => return Ok(None), + None => return Ok(past_end(p, start)), } } } diff --git a/crates/text/src/tests/colr.rs b/crates/text/src/tests/colr.rs index 212882ce7a..c3a0499af6 100644 --- a/crates/text/src/tests/colr.rs +++ b/crates/text/src/tests/colr.rs @@ -326,12 +326,8 @@ fn paints_documents_lack_make_the_glyph_an_image() { let line = |extend| ColorLine::new(extend, 2, vec![ColorStop::new(F2Dot14::from_f32(0.0), 0, one), ColorStop::new(one, 1, one)]); let w = FWord::new; type GlyphPaint<'a> = Box ColrPaint + 'a>; - let cases: [(&str, GlyphPaint<'_>); 3] = [ + let cases: [(&str, GlyphPaint<'_>); 2] = [ ("Colrtest Xor", Box::new(|o| ColrPaint::composite(ColrPaint::glyph(solid(2), o), CompositeMode::Xor, ColrPaint::glyph(solid(0), o)))), - ( - "Colrtest Repeat", - Box::new(move |o| ColrPaint::glyph(ColrPaint::linear_gradient(line(Extend::Repeat), w(0), w(0), w(100), w(0), w(0), w(100)), o)), - ), ( "Colrtest Circles", Box::new(move |o| { @@ -362,6 +358,129 @@ fn a_radial_gradient_from_a_circle_around_its_centre_is_vector_art() { assert_eq!(offsets, [0.25, 1.0], "the stops start a quarter of the way out"); } +/// A COLRv1 color line through `stops` (offset, palette index) that extends by `extend`. +fn color_line(extend: write_fonts::tables::colr::Extend, stops: &[(f32, u16)]) -> write_fonts::tables::colr::ColorLine { + use write_fonts::tables::colr::{ColorLine, ColorStop}; + use write_fonts::types::F2Dot14; + let one = F2Dot14::from_f32(1.0); + let stops: Vec = stops.iter().map(|&(at, index)| ColorStop::new(F2Dot14::from_f32(at), index, one)).collect(); + ColorLine::new(extend, u16::try_from(stops.len()).unwrap(), stops) +} + +/// The gradient filling the glyph's one path. +fn glyph_gradient(font: &[u8]) -> vectorcraft_doc::color::GradientPaint { + let root = art(font, COLOUR_CHAR); + let [only] = children(&root) else { panic!("one path") }; + let Paint::Gradient(g) = fill(only) else { panic!("a gradient") }; + *g +} + +/// A linear gradient that repeats or reflects is vector art. The gradient spreads the same way, +/// and its stops run once from the first stop's place to the last stop's place. +#[test] +fn a_repeating_or_reflecting_linear_gradient_is_vector_art() { + use vectorcraft_doc::color::GradientSpread; + use write_fonts::tables::colr::Extend; + use write_fonts::types::FWord; + let w = FWord::new; + for (family, extend, spread) in + [("Colrtest Linear Repeat", Extend::Repeat, GradientSpread::Repeat), ("Colrtest Linear Reflect", Extend::Reflect, GradientSpread::Reflect)] + { + let line = color_line(extend, &[(0.2, 0), (0.6, 1)]); + let font = + v1_font(family, COLOUR_CHAR, |o| ColrPaint::glyph(ColrPaint::linear_gradient(line.clone(), w(0), w(0), w(100), w(0), w(0), w(100)), o)); + let g = glyph_gradient(&font); + assert_eq!(g.gradient.kind, GradientKind::Linear, "{family}"); + assert_eq!(g.gradient.painted_spread(), (spread, 1), "{family}"); + let offsets: Vec = g.gradient.stops.iter().map(|s| s.offset).collect(); + assert_eq!(offsets, [0.0, 1.0], "{family}"); + let geom = g.geom.unwrap(); + assert!((geom.start.x - 20.0).abs() < 0.01 && (geom.end.x - 60.0).abs() < 0.01, "{family}: {:?} → {:?}", geom.start, geom.end); + } +} + +/// A radial gradient from its center that repeats or reflects is vector art that spreads the same +/// way, also with a focal point. Its circle is at the last stop, and its stops run once from the +/// center out to that circle. +#[test] +fn a_repeating_or_reflecting_radial_gradient_from_its_center_is_vector_art() { + use vectorcraft_doc::color::GradientSpread; + use write_fonts::tables::colr::Extend; + use write_fonts::types::{FWord, UfWord}; + let w = FWord::new; + let cases = [ + ("Colrtest Radial Repeat", Extend::Repeat, GradientSpread::Repeat, 250, 1.0, 400.0), + ("Colrtest Radial Reflect", Extend::Reflect, GradientSpread::Reflect, 300, 1.0, 400.0), + ("Colrtest Radial Half", Extend::Repeat, GradientSpread::Repeat, 300, 0.5, 200.0), + // Normalized, the last stop is a rounding error short of 1. + ("Colrtest Radial Fine", Extend::Repeat, GradientSpread::Repeat, 250, 41.0 / 16384.0, 400.0 * 41.0 / 16384.0), + ]; + for (family, extend, spread, x0, last, radius) in cases { + let line = color_line(extend, &[(0.0, 0), (last / 2.0, 2), (last, 1)]); + let font = v1_font(family, COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(x0), w(300), UfWord::new(0), w(250), w(300), UfWord::new(400)), o) + }); + let g = glyph_gradient(&font); + assert_eq!(g.gradient.kind, GradientKind::Radial, "{family}"); + assert_eq!(g.gradient.painted_spread(), (spread, 1), "{family}"); + let offsets: Vec = g.gradient.stops.iter().map(|s| s.offset).collect(); + assert!(offsets.len() == 3 && offsets.iter().zip([0.0, 0.5, 1.0]).all(|(a, b)| (a - b).abs() < 0.05), "{family}: {offsets:?}"); + let geom = g.geom.unwrap(); + assert!(((geom.end - geom.start).hypot() - radius).abs() < 0.1, "{family}: {geom:?}"); + assert_eq!(geom.focal.is_some(), x0 != 250, "{family}"); + } +} + +/// A radial gradient that repeats from a circle stays an image. The circle is either a start circle +/// around the center or the place a first stop past the center puts the start. +#[test] +fn a_repeating_radial_gradient_from_a_circle_makes_the_glyph_an_image() { + use write_fonts::tables::colr::Extend; + use write_fonts::types::{FWord, UfWord}; + let w = FWord::new; + for (family, r0, first) in [("Colrtest Ring Repeat", 100, 0.0), ("Colrtest Short Repeat", 0, 0.25)] { + let line = color_line(Extend::Repeat, &[(first, 0), (1.0, 1)]); + let font = v1_font(family, COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(250), w(300), UfWord::new(r0), w(250), w(300), UfWord::new(400)), o) + }); + assert!(matches!(content(&font, COLOUR_CHAR), Some(ColourContent::Image(_))), "{family}"); + } +} + +/// A gradient that repeats or reflects with a stop whose palette index is outside the palette stays +/// an image. Without that stop, the other stops would repeat over a different interval. +#[test] +fn a_repeating_gradient_with_a_stop_missing_from_the_palette_makes_the_glyph_an_image() { + use write_fonts::tables::colr::Extend; + use write_fonts::types::{FWord, UfWord}; + let w = FWord::new; + let line = color_line(Extend::Repeat, &[(0.0, 0), (0.5, 1), (1.0, 200)]); + let linear = v1_font("Colrtest Linear Missing", COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::linear_gradient(line.clone(), w(0), w(0), w(100), w(0), w(0), w(100)), o) + }); + let radial = v1_font("Colrtest Radial Missing", COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(250), w(300), UfWord::new(0), w(250), w(300), UfWord::new(400)), o) + }); + for (family, font) in [("linear", linear), ("radial", radial)] { + assert!(matches!(content(&font, COLOUR_CHAR), Some(ColourContent::Image(_))), "{family}"); + } +} + +/// A color line with an extend value the COLR specification doesn't define pads. +#[test] +fn an_unknown_extend_value_pads() { + use vectorcraft_doc::color::GradientSpread; + use write_fonts::tables::colr::Extend; + use write_fonts::types::FWord; + let w = FWord::new; + let line = color_line(Extend::Unknown, &[(0.0, 0), (1.0, 1)]); + let font = v1_font("Colrtest Unknown Extend", COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::linear_gradient(line.clone(), w(0), w(0), w(100), w(0), w(0), w(100)), o) + }); + let g = glyph_gradient(&font); + assert_eq!(g.gradient.painted_spread(), (GradientSpread::Pad, 1)); +} + /// A glyph whose layers don't use the foreground colour is read once, whatever the type's fill. #[test] fn a_glyph_without_the_foreground_colour_is_read_once_for_every_fill() { diff --git a/crates/text/src/tests/combos.rs b/crates/text/src/tests/combos.rs index 0146871265..4f57aa6972 100644 --- a/crates/text/src/tests/combos.rs +++ b/crates/text/src/tests/combos.rs @@ -465,18 +465,72 @@ fn composer_x_mixed_sizes_in_a_frame_that_widens() { #[test] fn composer_x_mixed_sizes_overflow_a_shaped_frame() { - // A stepped frame too short for the paragraph: the lines that fit are set, in order, each once - // and within its room, and the frame overflows. + // A stepped frame too short for the paragraph: the every-line composer still breaks the lines + // that fit as one paragraph, with the lines past the frame's end measured as wide as the last + // line the frame holds. Here that is the narrow part, so the lines that fit break as in a frame + // tall enough for the whole paragraph, and not as the single-line composer breaks them. They + // are set in order, each once and within its room, and the frame overflows. let w = x_width(); - let mut t = stepped(w, w * 0.5, 40.0, 75.0); - t.runs = with_x(sans(36.0)); - t.para.composer = Composer::EveryLine; - let l = layout(db(), &t); + let set = |bottom: f64, composer: Composer| { + let mut t = stepped(w, w * 0.5, 40.0, bottom); + t.runs = with_x(sans(36.0)); + t.para.composer = composer; + let l = layout(db(), &t); + (line_texts(&t, &l), l, t) + }; + let (every, l, t) = set(75.0, Composer::EveryLine); assert!(l.overflow && l.lines.len() >= 2, "{:?}", l.lines); assert_eq!(l.lines[0].start, 0); assert!(l.lines.windows(2).all(|p| p[0].end == p[1].start && p[0].start < p[0].end), "{:?}", l.lines); assert!(past_their_room(&t, &l).is_empty(), "{:?}", l.lines); assert!(l.lines.iter().all(|li| li.baseline + li.descent <= 75.0 + 0.01), "{:?}", l.lines); + assert_eq!(every, ["Destroy all creatures with toughness", "X or less. Then", "draw a card for each"]); + let (whole, ..) = set(300.0, Composer::EveryLine); + assert_eq!(every, whole[..every.len()]); + let (greedy, ..) = set(75.0, Composer::SingleLine); + assert_ne!(every, greedy); + // A frame with no room for a line sets none. + for composer in [Composer::EveryLine, Composer::SingleLine] { + let mut t = stepped(w, w * 0.5, 4.0, 8.0); + t.runs = with_x(sans(36.0)); + t.para.composer = composer; + let l = layout(db(), &t); + assert!(l.lines.is_empty() && l.overflow, "{composer:?}: {:?}", l.lines); + } +} + +#[test] +fn composer_x_mixed_sizes_overflow_around_text_wrap() { + // A rectangle that wraps around an object beside its lower lines, too short for the + // paragraph: the every-line composer breaks the lines that fit as one paragraph, each span + // measured across its line's whole height, and the frame overflows. No glyph runs into the + // object. + let w = x_width(); + let object = Rect::new(w * 0.4, 40.0, w * 0.6, 70.0); + let set = |composer: Composer| { + let mut runs = with_x(sans(36.0)); + runs[0] = run("Destroy all creatures with toughness\u{a0}", sans(12.0)); + let mut t = area(runs, Rect::new(0.0, 0.0, w, 80.0)); + t.wrap = vec![vectorcraft_doc::WrapShape { path: PathData::from_bezpath(&object.to_path(0.1)), wrap: Default::default() }]; + t.para.composer = composer; + let l = layout(db(), &t); + (line_texts(&t, &l), l, t) + }; + let (every, l, t) = set(Composer::EveryLine); + assert!(l.overflow && l.lines.len() >= 3, "{:?}", l.lines); + assert!(l.lines.windows(2).all(|p| p[0].end == p[1].start && p[0].start < p[0].end), "{:?}", l.lines); + assert!(past_their_room(&t, &l).is_empty(), "{:?}", l.lines); + let text = t.plain_text(); + let hits = l.glyphs.iter().filter(|g| { + let li = &l.lines[g.line]; + let across = li.baseline - li.ascent < object.y1 && li.baseline + li.descent > object.y0; + across && !text[g.byte..].starts_with(char::is_whitespace) && g.origin.x < object.x1 && g.origin.x + g.advance > object.x0 + }); + assert_eq!(hits.count(), 0, "{:?}", l.lines); + // The lines that fit break as in `composer_x_mixed_sizes_around_text_wrap`'s taller frame. + assert_eq!(every, ["Destroy all creatures with", "toughness\u{a0}X", "or less. Then", "draw a card for", "each creature"]); + let (greedy, ..) = set(Composer::SingleLine); + assert_ne!(every, greedy); } #[test] diff --git a/crates/text/src/thread/tests.rs b/crates/text/src/thread/tests.rs index 0ac64f5d27..adbf3b2c70 100644 --- a/crates/text/src/thread/tests.rs +++ b/crates/text/src/thread/tests.rs @@ -397,6 +397,80 @@ fn a_continued_paragraph_fills_the_frame_it_continues_into() { assert_eq!(checked, 16); } +/// Area type in a frame `w` wide down to `y` and `narrow` wide below it, down to `bottom`. +fn stepped_frame(w: f64, narrow: f64, y: f64, bottom: f64) -> TextObject { + let mut t = frame(0.0); + let path = kurbo::BezPath::from_vec(vec![ + kurbo::PathEl::MoveTo((0.0, 0.0).into()), + kurbo::PathEl::LineTo((w, 0.0).into()), + kurbo::PathEl::LineTo((w, y).into()), + kurbo::PathEl::LineTo((narrow, y).into()), + kurbo::PathEl::LineTo((narrow, bottom).into()), + kurbo::PathEl::LineTo((0.0, bottom).into()), + kurbo::PathEl::ClosePath, + ]); + t.kind = TextKind::Area { frame: PathData::from_bezpath(&path) }; + t +} + +/// A paragraph of mixed type sizes that a stepped frame cannot hold goes on in the next frame. The +/// every-line composer breaks the lines the first frame holds as one paragraph, as in a frame tall +/// enough for all of it and not as the single-line composer breaks them, and the first frame +/// takes the text of those lines. The next frame sets the rest at its own width, without the +/// first-line indent, and no character is lost or repeated. The frames take the same shares as +/// when each is measured with the whole rest of the story. +#[test] +fn a_mixed_size_paragraph_continued_from_a_shaped_frame_is_composed_in_both() { + use vectorcraft_doc::Composer; + let db = FontDb::global(); + let small = CharStyle { size: 12.0, ..CharStyle::default() }; + let big = CharStyle { size: 36.0, ..CharStyle::default() }; + let probe = layout(db, &TextObject::point(vectorcraft_geom::Point::ZERO, "Destroy all creatures with toughness X or", small.clone())); + let w = probe.lines[0].x1 - probe.lines[0].x0 + 2.0; + let story = vec![ + TextRun { text: "Destroy all creatures with toughness ".into(), style: small.clone(), inline: None }, + TextRun { text: "X".into(), style: big, inline: None }, + TextRun { text: " or less. Then draw a card for each creature destroyed this way.\nSecond paragraph.".into(), style: small, inline: None }, + ]; + let text: String = story.iter().map(|r| r.text.as_str()).collect(); + let (a, tall) = (stepped_frame(w, w * 0.5, 40.0, 85.0), stepped_frame(w, w * 0.5, 40.0, 300.0)); + let b = sized(200.0, w * 0.75, 300.0); + let texts = |lay: &crate::TextLayout, shown: &str| lay.lines.iter().map(|l| shown[l.start..l.end].trim_end().to_string()).collect::>(); + let shares = |composer: Composer, indent: f64| { + let para = ParaStyle { composer, first_line_indent: indent, ..Default::default() }; + let paras = [para.clone(), para]; + let parts = distribute(db, &[&a, &b], &story, ¶s); + assert_eq!(parts.iter().map(|p| p.continues).collect::>(), [false, true], "{composer:?}"); + let joined: String = parts.iter().flat_map(|p| &p.runs).map(|r| r.text.as_str()).collect(); + assert_eq!(joined, text, "{composer:?}: no character lost or repeated"); + same_as_whole_rest(db, &[&a, &b], &story, ¶s, &format!("{composer:?}, indent {indent}")); + let shown = |p: &Slice| p.runs.iter().map(|r| r.text.as_str()).collect::(); + let lays: Vec<_> = [&a, &b].iter().zip(&parts).map(|(f, p)| laid(db, f, p)).collect(); + assert!(lays.iter().all(|l| !l.overflow), "{composer:?}: each frame holds its share"); + let whole = + laid(db, &tall, &Slice { runs: story.clone(), paras: paras.to_vec(), list_count: 0, list_level_counts: vec![], continues: false }); + let first = texts(&lays[0], &shown(&parts[0])); + // The first frame takes the text of the lines it sets. + assert_eq!(shown(&parts[0]).split_whitespace().collect::>(), first.join(" ").split_whitespace().collect::>()); + (first, texts(&whole, &text), lays, parts) + }; + for indent in [0.0, 20.0] { + let (every, whole, lays, parts) = shares(Composer::EveryLine, indent); + assert!(every.len() >= 2, "{every:?}"); + assert_eq!(every, whole[..every.len()], "indent {indent}"); + if indent == 0.0 { + assert_eq!(every, ["Destroy all creatures with toughness", "X or less. Then", "draw a card for each"]); + let (greedy, ..) = shares(Composer::SingleLine, indent); + assert_eq!(greedy, ["Destroy all creatures with toughness X", "or less. Then draw a", "card for each", "creature destroyed"]); + } + // The next frame starts at its left edge, and its lines keep within it. + assert!((line_start(&lays[1], 0) - 200.0).abs() < 1e-6, "{}", line_start(&lays[1], 0)); + assert!(lays[1].lines.iter().all(|l| l.x1 <= 200.0 + w * 0.75 + 1e-6), "{:?}", lays[1].lines); + assert!((line_start(&lays[0], 0) - indent).abs() < 1e-6, "{}", line_start(&lays[0], 0)); + assert!(parts[1].continues && lays[1].lines.len() >= 2, "{:?}", lays[1].lines); + } +} + /// `frames` holding the runs of `parts`, for [`continued`]. fn filled(frames: &[&TextObject], parts: &[Slice]) -> Vec { frames diff --git a/crates/three-d/src/lib.rs b/crates/three-d/src/lib.rs index 309aa77409..7a83c811a1 100644 --- a/crates/three-d/src/lib.rs +++ b/crates/three-d/src/lib.rs @@ -19,7 +19,7 @@ mod lights; pub use lights::{Light, Lights, MAX_LIGHTS}; mod caps; mod mesh; -pub use mesh::{Mesh, extrude_mesh, inflate_meshes, plane_mesh, revolve_mesh}; +pub use mesh::{Mesh, MeshedSolid, extrude_mesh, extrude_meshed, inflate_meshed, inflate_meshes, plane_mesh, revolve_mesh}; mod shadow; pub use shadow::{MAX_SHADOW_FACES, Shadow, ShadowCaster, ShadowPlane, ShadowPosition, TOO_COMPLEX_SHADOW}; diff --git a/crates/three-d/src/mesh.rs b/crates/three-d/src/mesh.rs index b12bd315f5..cd9623191c 100644 --- a/crates/three-d/src/mesh.rs +++ b/crates/three-d/src/mesh.rs @@ -10,8 +10,8 @@ use vectorcraft_geom::{FillRule, PathData, Rect}; -use crate::solid::{Face3, extrude_faces, inflate_faces, plane_faces}; -use crate::{Extrude, Inflate, Revolve, V3, View}; +use crate::solid::{Face3, Keep, extrude_faces, inflate_faces, plane_faces}; +use crate::{Extrude, Facet, Inflate, Revolve, V3, View}; /// Most steps the triangulation of one solid may take: a face's ears and bridges each test the /// face's other corners. @@ -56,12 +56,12 @@ impl Mesh { /// Extrude & Bevel's solid ([`crate::extrude`]) as a mesh. pub fn extrude_mesh(path: &PathData, rule: FillRule, bounds: Rect, options: Extrude, view: View) -> Result { - extrude_faces(path, rule, bounds, options, view, true).and_then(|(_, faces, _)| mesh(&faces)) + extrude_faces(path, rule, bounds, options, view, Keep::Faces).and_then(|(_, faces, _)| mesh(&faces?)) } /// Rotate's plane ([`crate::plane`]) as a mesh. pub fn plane_mesh(path: &PathData, rule: FillRule, bounds: Rect, view: View) -> Result { - plane_faces(path, rule, bounds, view, true).and_then(|(_, faces, _)| mesh(&faces)) + plane_faces(path, rule, bounds, view, Keep::Faces).and_then(|(_, faces, _)| mesh(&faces?)) } /// Inflate's solids for several shapes of one object ([`crate::inflate_together`]) as meshes, one @@ -72,7 +72,49 @@ pub fn inflate_meshes( options: Inflate, view: View, ) -> impl Iterator> + use<> { - inflate_faces(shapes, bounds, options, view, true).map(|r| r.and_then(|(_, faces, _)| mesh(&faces))) + inflate_faces(shapes, bounds, options, view, Keep::Faces).map(|r| r.and_then(|(_, faces, _)| mesh(&faces?))) +} + +/// One solid built once for both its drawing and its mesh: the facets [`crate::extrude`] or +/// [`crate::inflate_each`] gives, and the faces before projection that [`MeshedSolid::mesh`] cuts +/// into the mesh [`extrude_mesh`] or [`inflate_meshes`] gives. +#[derive(Clone, Debug)] +pub struct MeshedSolid { + pub facets: Vec, + /// The faces before projection, or the error the mesh alone gives when there are too many. + faces: Result, String>, +} + +/// A solid with no facets and an empty mesh. +impl Default for MeshedSolid { + fn default() -> Self { + Self { facets: vec![], faces: Ok(vec![]) } + } +} + +impl MeshedSolid { + /// The solid as a mesh. The faces are cut into triangles on each call. + pub fn mesh(&self) -> Result { + mesh(self.faces.as_ref().map_err(String::clone)?) + } +} + +/// Extrude & Bevel's solid ([`crate::extrude`]) and its mesh ([`extrude_mesh`]) from one build. An +/// error where [`crate::extrude`] gives one. +pub fn extrude_meshed(path: &PathData, rule: FillRule, bounds: Rect, options: Extrude, view: View) -> Result { + extrude_faces(path, rule, bounds, options, view, Keep::Both).map(|(facets, faces, _)| MeshedSolid { facets, faces }) +} + +/// Inflate's solids for several shapes of one object ([`crate::inflate_each`]) and their meshes +/// ([`inflate_meshes`]), each solid built once, as the iterator reaches it. An error where +/// [`crate::inflate_each`] gives one. +pub fn inflate_meshed( + shapes: &[(&PathData, FillRule)], + bounds: Rect, + options: Inflate, + view: View, +) -> impl Iterator> + use<> { + inflate_faces(shapes, bounds, options, view, Keep::Both).map(|r| r.map(|(facets, faces, _)| MeshedSolid { facets, faces })) } /// Revolve's surface ([`crate::revolve`]) as a mesh. As the effect draws it, a sweep of less than diff --git a/crates/three-d/src/mesh/tests.rs b/crates/three-d/src/mesh/tests.rs index 2011d6689c..76f9b04ecc 100644 --- a/crates/three-d/src/mesh/tests.rs +++ b/crates/three-d/src/mesh/tests.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use vectorcraft_geom::{BezPath, FillRule, PathData, Rect}; use super::*; -use crate::{Bevel, Edge}; +use crate::{Bevel, BevelShape, Edge}; fn square(b: &mut BezPath, x: f64, y: f64, s: f64, clockwise: bool) { b.move_to((x, y)); @@ -372,3 +372,130 @@ fn join_numbers_each_parts_corners_after_the_last() { assert!(both.triangles.iter().all(|t| t.iter().all(|i| (*i as usize) < both.positions.len()))); assert_eq!(both.triangles[one.triangles.len()][0], one.triangles[0][0] + one.positions.len() as u32); } + +/// A mesh's positions and normals as bits, and its triangles. +type MeshBits = (Vec<[u64; 3]>, Vec<[u64; 3]>, Vec<[u32; 3]>); + +/// The bits of every number in `m`, for comparing two meshes exactly. +fn bits(m: &Mesh) -> MeshBits { + (m.positions.iter().map(|p| p.map(f64::to_bits)).collect(), m.normals.iter().map(|n| n.map(f64::to_bits)).collect(), m.triangles.clone()) +} + +/// A star of `points` points about (100, 100), with a square hole: curved and straight edges both. +fn star(points: usize) -> (PathData, Rect) { + let mut b = BezPath::new(); + for k in 0..2 * points { + let (s, c) = (k as f64 * std::f64::consts::PI / points as f64).sin_cos(); + let r = if k % 2 == 0 { 90.0 } else { 45.0 }; + let p = (100.0 + r * c, 100.0 + r * s); + if k == 0 { b.move_to(p) } else { b.curve_to((p.0 + 3.0, p.1), (p.0, p.1 + 3.0), p) } + } + b.close_path(); + square(&mut b, 90.0, 90.0, 20.0, true); + shape(&b) +} + +/// The views the solids are compared in: straight on, turned, with perspective, and with colored +/// lights (Inflate's smooth triangles). +fn views() -> Vec { + let turned = View { rotation_x: -25.0, rotation_y: 35.0, rotation_z: 10.0, ..View::default() }; + vec![front(), turned, View { perspective: 60.0, ..turned }, View { light_color: [1.0, 0.5, 0.25], ..turned }] +} + +/// Extrude & Bevel built once for its facets and its mesh gives what [`crate::extrude`] and +/// [`extrude_mesh`] give apart, to the bit, errors included. +#[test] +fn an_extruded_solid_and_its_mesh_match_the_two_built_apart() { + let bevel = Bevel { shape: BevelShape::Rounded, width: 6.0, height: 5.0, ..Bevel::default() }; + let options = [ + Extrude::default(), + Extrude { depth: 0.0, ..Extrude::default() }, + Extrude { cap: false, ..Extrude::default() }, + Extrude { hidden: true, ..Extrude::default() }, + Extrude { twist: 90.0, taper: 0.6, ..Extrude::default() }, + Extrude { bevel: Some(bevel), ..Extrude::default() }, + Extrude { bevel: Some(Bevel { both_sides: true, inside: true, ..bevel }), twist: 45.0, ..Extrude::default() }, + ]; + let (p, r) = star(7); + for view in views() { + for o in options { + let meshed = extrude_meshed(&p, FillRule::NonZero, r, o, view).unwrap(); + let facets = crate::extrude(&p, FillRule::NonZero, r, o, view).unwrap(); + assert_eq!(format!("{:?}", meshed.facets), format!("{facets:?}"), "{o:?} {view:?}"); + let apart = extrude_mesh(&p, FillRule::NonZero, r, o, view).unwrap(); + assert!(!apart.is_empty()); + assert_eq!(bits(&meshed.mesh().unwrap()), bits(&apart), "{o:?} {view:?}"); + } + } + // A solid that can't be built gives the same error. + let flat = Rect::new(0.0, 0.0, f64::INFINITY, 10.0); + let o = Extrude::default(); + let error = crate::extrude(&p, FillRule::NonZero, flat, o, front()).err(); + assert!(error.is_some()); + assert_eq!(extrude_meshed(&p, FillRule::NonZero, flat, o, front()).err(), error); +} + +/// Inflate's solids built once for their facets and meshes give what [`crate::inflate_each`] and +/// [`inflate_meshes`] give apart, to the bit: several puffs under one eye, both sides, open +/// backs, and twisted sides cut finer than room is left for. +#[test] +fn inflated_solids_and_their_meshes_match_the_two_built_apart() { + let (star, _) = star(5); + let mut b = BezPath::new(); + square(&mut b, 200.0, 20.0, 60.0, false); + let (box_, _) = shape(&b); + let ring: Vec = (0..600) + .map(|k| { + let (s, c) = (k as f64 * std::f64::consts::TAU / 600.0).sin_cos(); + vectorcraft_geom::Point::new(100.0 + 90.0 * c, 100.0 + 90.0 * s) + }) + .collect(); + let ring = PathData::single(vectorcraft_geom::SubPath::polyline(&ring, true)); + let sets = [vec![(&star, FillRule::NonZero), (&box_, FillRule::NonZero), (&star, FillRule::EvenOdd)], vec![(&ring, FillRule::NonZero)]]; + let bounds = Rect::new(0.0, 0.0, 280.0, 200.0); + let options = [ + Inflate::default(), + Inflate { depth: 30.0, both_sides: true, ..Inflate::default() }, + Inflate { depth: 20.0, cap: false, volume: 80.0, ..Inflate::default() }, + Inflate { depth: 40.0, twist: 360.0, taper: 0.7, both_sides: true, ..Inflate::default() }, + // Thin and barely twisted: the star's sides want more pieces than room is left after its + // front and back, and the mesh, which keeps the faces turned away, has less room left. + Inflate { depth: 0.1, twist: 0.1, both_sides: true, ..Inflate::default() }, + ]; + for shapes in &sets { + for view in views() { + for o in options { + let meshed: Vec<_> = inflate_meshed(shapes, bounds, o, view).collect(); + let facets: Vec<_> = crate::inflate_each(shapes, bounds, o, view).collect(); + let apart: Vec<_> = inflate_meshes(shapes, bounds, o, view).collect(); + assert_eq!(meshed.len(), shapes.len()); + for ((m, f), a) in meshed.iter().zip(&facets).zip(&apart) { + let (m, f, a) = (m.as_ref().unwrap(), f.as_ref().unwrap(), a.as_ref().unwrap()); + assert_eq!(format!("{:?}", m.facets), format!("{f:?}"), "{o:?} {view:?}"); + assert!(!a.is_empty()); + assert_eq!(bits(&m.mesh().unwrap()), bits(a), "{o:?} {view:?}"); + } + } + } + } +} + +/// One build gives both the facets and the mesh: each solid is built once. +#[test] +fn a_meshed_solid_is_built_once() { + let (p, r) = star(5); + let extruded = || crate::solid::EXTRUDED.with(|n| n.get()); + let start = extruded(); + let solid = extrude_meshed(&p, FillRule::NonZero, r, Extrude::default(), front()).unwrap(); + assert!(!solid.facets.is_empty() && solid.mesh().is_ok_and(|m| !m.is_empty())); + assert_eq!(extruded(), start + 1); + let inflated = || crate::solid::INFLATED.with(|n| n.get()); + let start = inflated(); + let mut b = BezPath::new(); + square(&mut b, 200.0, 20.0, 60.0, false); + let (q, _) = shape(&b); + for solid in inflate_meshed(&[(&p, FillRule::NonZero), (&q, FillRule::NonZero)], Rect::new(0.0, 0.0, 280.0, 200.0), Inflate::default(), front()) { + assert!(solid.is_ok_and(|s| !s.facets.is_empty() && s.mesh().is_ok_and(|m| !m.is_empty()))); + } + assert_eq!(inflated(), start + 2); +} diff --git a/crates/three-d/src/shadow.rs b/crates/three-d/src/shadow.rs index d7cd01a869..66842abad2 100644 --- a/crates/three-d/src/shadow.rs +++ b/crates/three-d/src/shadow.rs @@ -11,7 +11,7 @@ use std::ops::Range; use vectorcraft_geom::{BezPath, FillRule, PathData, Point, Rect}; -use crate::solid::{Face3, extrude_faces, inflate_faces, plane_faces}; +use crate::solid::{Face3, Keep, extrude_faces, inflate_faces, plane_faces}; use crate::{Extrude, Inflate, MAX_SOLID_FACES, Revolve, V3, View, finite, perspective_scale}; /// Where the shadow falls. @@ -160,14 +160,14 @@ impl ShadowCaster { /// Add Extrude & Bevel's solid ([`crate::extrude`]). pub fn extrude(&mut self, path: &PathData, rule: FillRule, bounds: Rect, options: Extrude, view: View) -> Result, String> { - let (_, faces, projection) = extrude_faces(path, rule, bounds, options, view, true)?; - self.add(faces, projection) + let (_, faces, projection) = extrude_faces(path, rule, bounds, options, view, Keep::Faces)?; + self.add(faces?, projection) } /// Add Rotate's plane ([`crate::plane`]). pub fn plane(&mut self, path: &PathData, rule: FillRule, bounds: Rect, view: View) -> Result, String> { - let (_, faces, projection) = plane_faces(path, rule, bounds, view, true)?; - self.add(faces, projection) + let (_, faces, projection) = plane_faces(path, rule, bounds, view, Keep::Faces)?; + self.add(faces?, projection) } /// Add Inflate's solids for several shapes of one object ([`crate::inflate_together`]), each @@ -182,11 +182,11 @@ impl ShadowCaster { options: Inflate, view: View, ) -> Vec, String>> { - inflate_faces(shapes, bounds, options, view, true) + inflate_faces(shapes, bounds, options, view, Keep::Faces) .enumerate() .map(|(i, r)| { let (_, faces, projection) = r?; - if casts.get(i).copied().unwrap_or(true) { self.add(faces, projection) } else { Ok(self.faces.len()..self.faces.len()) } + if casts.get(i).copied().unwrap_or(true) { self.add(faces?, projection) } else { Ok(self.faces.len()..self.faces.len()) } }) .collect() } diff --git a/crates/three-d/src/solid.rs b/crates/three-d/src/solid.rs index 49b0738d9e..f67f6c96e8 100644 --- a/crates/three-d/src/solid.rs +++ b/crates/three-d/src/solid.rs @@ -20,9 +20,11 @@ mod membrane; mod outline; mod rings; +#[cfg(test)] +pub(crate) use build::EXTRUDED; #[cfg(test)] use build::{Camera, cuts, edge_distance, newell, section}; -pub(crate) use build::{Face3, extrude_faces, plane_faces}; +pub(crate) use build::{Face3, Keep, extrude_faces, plane_faces}; pub use build::{extrude, plane}; pub use face_map::{FaceMap, extrude_face_map, plane_face_map}; #[cfg(test)] diff --git a/crates/three-d/src/solid/build.rs b/crates/three-d/src/solid/build.rs index 6d21805e74..2aa218e558 100644 --- a/crates/three-d/src/solid/build.rs +++ b/crates/three-d/src/solid/build.rs @@ -64,9 +64,25 @@ impl Camera { /// and its outward normal. pub(crate) type Face3 = (Vec>, V3); -/// What a solid's builder makes: its facets, or with a mesh asked for, its faces before projection, -/// and the projection it used (`None` when nothing was built). -pub(crate) type Built = (Vec, Vec, Option); +/// What a solid's builder makes: its facets, its faces before projection when a mesh is asked for +/// (an error when they pass [`MAX_SOLID_FACES`] while the facets are built too), and the +/// projection it used (`None` when nothing was built). +pub(crate) type Built = (Vec, Result, String>, Option); + +#[cfg(test)] +thread_local! { + /// How many Extrude & Bevel solids this thread has built, for the tests. + pub(crate) static EXTRUDED: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// What a solid's builder keeps: its facets, its faces before projection for a mesh, or both +/// from one build. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Keep { + Facets, + Faces, + Both, +} /// Facets being collected for one solid. pub(super) struct Builder<'a> { @@ -80,6 +96,11 @@ pub(super) struct Builder<'a> { pub(super) facets: Vec, /// With `Some`, every face goes here as it is, none hidden or projected, for a mesh. pub(super) mesh: Option>, + /// The faces are projected into `facets`; without it only `mesh` is kept. + pub(super) draw: bool, + /// The faces kept for the mesh passed [`MAX_SOLID_FACES`] while the facets were built too: the + /// mesh is too complex, and `mesh` keeps no more faces. + pub(super) lost: bool, } /// Where a face goes in the order the facets are drawn, back to front. @@ -98,20 +119,46 @@ pub(super) enum Order { Last, } -impl Builder<'_> { +impl<'a> Builder<'a> { + /// A builder for one solid seen by `camera`, keeping what `keep` asks for. + pub(super) fn new(camera: &'a Camera, closed: bool, hidden: bool, keep: Keep) -> Self { + Self { camera, closed, hidden, facets: vec![], mesh: (keep != Keep::Facets).then(Vec::new), draw: keep != Keep::Faces, lost: false } + } + /// Add a face of view-space contours with outward normal `n` (view space), drawn in `order`. pub(super) fn face(&mut self, contours: Vec>, n: V3, order: Order) -> Result<(), String> { let count: usize = contours.iter().map(Vec::len).sum(); if count < 3 { return Ok(()); } - if let Some(mesh) = &mut self.mesh { - mesh.push((contours, n.unit())); - if mesh.len() > MAX_SOLID_FACES { + if self.draw { + self.facet(&contours, count, n, order)?; + } + let Some(mesh) = &mut self.mesh else { return Ok(()) }; + mesh.push((contours, n.unit())); + if mesh.len() > MAX_SOLID_FACES { + if !self.draw { return Err(TOO_COMPLEX_SURFACE.into()); } - return Ok(()); + self.lose_mesh(); } + Ok(()) + } + + /// Keep no more faces for the mesh, which is too complex. + pub(super) fn lose_mesh(&mut self) { + self.mesh = None; + self.lost = true; + } + + /// How many faces the mesh holds, while the facets are built too and the mesh is kept. + pub(super) fn faces_kept(&self) -> Option { + self.mesh.as_ref().filter(|_| self.draw).map(Vec::len) + } + + /// Project a face of `count` view-space corners with outward normal `n` into a facet, drawn in + /// `order`, unless it is hidden or has no area. + fn facet(&mut self, contours: &[Vec], count: usize, n: V3, order: Order) -> Result<(), String> { let centre = contours.iter().flatten().fold(V3::default(), |s, v| V3(s.0 + v.0, s.1 + v.1, s.2 + v.2)); let centre = V3(centre.0 / count as f64, centre.1 / count as f64, centre.2 / count as f64); let n = n.unit(); @@ -138,7 +185,7 @@ impl Builder<'_> { let flat = !held && contours.iter().flatten().all(|v| (n.dot(*v) - offset).abs() <= 1e-3); let plane = flat.then_some(DepthPlane { normal: [n.0, n.1, n.2], offset, center: self.camera.origin, eye: self.camera.eye }); #[cfg(test)] - let view = contours.clone(); + let view = contours.to_vec(); let contours: Vec> = contours.iter().map(|c| c.iter().map(|v| self.camera.project(*v)).collect()).collect(); if contours.iter().flatten().any(|p| !p.x.is_finite() || !p.y.is_finite()) { return Err("The 3D projection is not finite".into()); @@ -262,7 +309,7 @@ impl Builder<'_> { /// Give the facet that [`Builder::face`] added, when it added one after `before` facets, the /// outline `outline` makes from its projected contours. pub(super) fn outline(&mut self, before: usize, outline: impl FnOnce(&[Vec]) -> Option) { - if self.mesh.is_some() || self.facets.len() <= before { + if !self.draw || self.facets.len() <= before { return; } if let Some(f) = self.facets.last_mut() { @@ -273,7 +320,7 @@ impl Builder<'_> { /// Mark the facet that [`Builder::face`] added, when it added one after `before` facets, as the /// planar end `end` of the solid. pub(super) fn end(&mut self, before: usize, end: PlanarFace) { - if self.mesh.is_some() || self.facets.len() <= before { + if !self.draw || self.facets.len() <= before { return; } if let Some(f) = self.facets.last_mut() { @@ -281,15 +328,19 @@ impl Builder<'_> { } } - /// How many faces the builder holds: facets, or faces kept for a mesh. + /// How many faces the builder holds: facets, or without them the faces kept for a mesh. pub(super) fn len(&self) -> usize { - self.mesh.as_ref().map_or(self.facets.len(), Vec::len) + match &self.mesh { + Some(mesh) if !self.draw => mesh.len(), + _ => self.facets.len(), + } } /// The facets back to front, and the faces kept for a mesh (none without [`Builder::mesh`]). pub(super) fn finish(mut self) -> Built { self.facets.sort_by(|a, b| a.depth.total_cmp(&b.depth)); - (self.facets, self.mesh.unwrap_or_default(), Some(self.camera.projection())) + let faces = if self.lost { Err(TOO_COMPLEX_SURFACE.into()) } else { Ok(self.mesh.unwrap_or_default()) }; + (self.facets, faces, Some(self.camera.projection())) } } @@ -536,16 +587,18 @@ fn steps(o: &Extrude) -> Vec { /// Extrude & Bevel: the shape's outline extruded backwards, with optional twist, taper, bevels /// and caps. pub fn extrude(path: &PathData, rule: FillRule, bounds: Rect, options: Extrude, view: View) -> Result, String> { - extrude_faces(path, rule, bounds, options, view, false).map(|(facets, ..)| facets) + extrude_faces(path, rule, bounds, options, view, Keep::Facets).map(|(facets, ..)| facets) } -/// [`extrude`], or with `mesh` the solid's faces before projection instead of its facets. -pub(crate) fn extrude_faces(path: &PathData, rule: FillRule, bounds: Rect, options: Extrude, view: View, mesh: bool) -> Result { +/// [`extrude`], keeping the solid's facets, its faces before projection, or both, as `keep` asks. +pub(crate) fn extrude_faces(path: &PathData, rule: FillRule, bounds: Rect, options: Extrude, view: View, keep: Keep) -> Result { + #[cfg(test)] + EXTRUDED.with(|n| n.set(n.get() + 1)); check_bounds(bounds)?; let o = options.sanitized(); // No depth: the flat shape, seen from either side. if o.depth <= 0.0 { - return plane_faces(path, rule, bounds, view, mesh); + return plane_faces(path, rule, bounds, view, keep); } let steps = steps(&o); // One face for each edge of the outline in each band between two steps, and the two caps: @@ -561,7 +614,7 @@ pub(crate) fn extrude_faces(path: &PathData, rule: FillRule, bounds: Rect, optio return Err(TOO_COMPLEX_SURFACE.into()); } let camera = Camera::new(view, bounds, o.depth); - let mut b = Builder { camera: &camera, closed: o.cap, hidden: o.hidden, facets: vec![], mesh: mesh.then(Vec::new) }; + let mut b = Builder::new(&camera, o.cap, o.hidden, keep); let z = |t: f64| o.depth / 2.0 - o.depth * t; let ring_at = |ring: &[Point], s: &Step| -> Vec { section(&inset(ring, s.inset), bounds, &o, s.t).iter().map(|p| camera.place(*p, z(s.t))).collect() }; @@ -647,15 +700,15 @@ pub(crate) fn extrude_faces(path: &PathData, rule: FillRule, bounds: Rect, optio /// Rotate: the flat art turned in 3D about the centre of `bounds` (which may be larger than the /// shape's own, so that every part of one object turns about the same point). pub fn plane(path: &PathData, rule: FillRule, bounds: Rect, view: View) -> Result, String> { - plane_faces(path, rule, bounds, view, false).map(|(facets, ..)| facets) + plane_faces(path, rule, bounds, view, Keep::Facets).map(|(facets, ..)| facets) } -/// [`plane`], or with `mesh` its face before projection instead of its facet. -pub(crate) fn plane_faces(path: &PathData, rule: FillRule, bounds: Rect, view: View, mesh: bool) -> Result { +/// [`plane`], keeping its facet, its face before projection, or both, as `keep` asks. +pub(crate) fn plane_faces(path: &PathData, rule: FillRule, bounds: Rect, view: View, keep: Keep) -> Result { check_bounds(bounds)?; let rings = solid_rings_traced(path, rule, bounds.width().max(bounds.height()), MAX_PLANE_POINTS, CURVE_PIECES)?; let camera = Camera::new(view, bounds, 0.0); - let mut b = Builder { camera: &camera, closed: false, hidden: false, facets: vec![], mesh: mesh.then(Vec::new) }; + let mut b = Builder::new(&camera, false, false, keep); let contours: Vec> = rings.iter().map(|r| r.points.iter().map(|p| camera.place(*p, 0.0)).collect()).collect(); // A plane is one flat face, seen from either side. Its normal is turned toward the viewer, so // it is drawn last, as a cap is, and planes drawn in one order keep the order they are given in. diff --git a/crates/three-d/src/solid/inflate.rs b/crates/three-d/src/solid/inflate.rs index d3773a92f9..677e9cc8bc 100644 --- a/crates/three-d/src/solid/inflate.rs +++ b/crates/three-d/src/solid/inflate.rs @@ -1,4 +1,4 @@ -use super::build::{Builder, Built, Camera, Order, Turn, check_bounds, cuts, fit, newell, outward}; +use super::build::{Builder, Built, Camera, Keep, Order, Turn, check_bounds, cuts, fit, newell, outward}; use super::membrane::{Edges, MAX_MATERIAL_SEGMENTS, Membrane, cell_triangles, clip, crossings, filled, material}; use super::rings::{MAX_SOLID_POINTS, TOO_COMPLEX_SURFACE, curve_pieces, signed_area, solid_rings}; use super::{Facet, FillRule, Inflate, MAX_FACES, MAX_SOLID_FACES, PathData, Point, Rect, V3, Vec2, View}; @@ -73,26 +73,26 @@ pub fn inflate_each( options: Inflate, view: View, ) -> impl Iterator, String>> + use<> { - inflate_faces(shapes, bounds, options, view, false).map(|r| r.map(|(facets, ..)| facets)) + inflate_faces(shapes, bounds, options, view, Keep::Facets).map(|r| r.map(|(facets, ..)| facets)) } -/// [`inflate_together`], or with `mesh` each solid's faces before projection instead of its facets. -/// Every shape's membrane is solved at once (the eye depends on the highest puff); each solid is -/// built as the iterator reaches it. +/// [`inflate_together`], keeping each solid's facets, its faces before projection, or both, as +/// `keep` asks. Every shape's membrane is solved at once (the eye depends on the highest puff); +/// each solid is built as the iterator reaches it. pub(crate) fn inflate_faces( shapes: &[(&PathData, FillRule)], bounds: Rect, options: Inflate, view: View, - mesh: bool, + keep: Keep, ) -> impl Iterator> + use<> { let o = options.sanitized(); let puffs: Vec, String>> = shapes.iter().map(|(path, rule)| check_bounds(bounds).and_then(|()| puff(path, *rule, bounds, &o))).collect(); let reach = puffs.iter().flatten().flatten().map(|p| p.height).fold(0.0, f64::max); puffs.into_iter().map(move |p| match p { - Ok(Some(p)) => inflated(p, bounds, o, view, reach, mesh), - Ok(None) => Ok((vec![], vec![], None)), + Ok(Some(p)) => inflated(p, bounds, o, view, reach, keep), + Ok(None) => Ok((vec![], Ok(vec![]), None)), Err(e) => Err(e), }) } @@ -106,9 +106,9 @@ thread_local! { } /// The facets of one inflated shape, for a camera that allows for a puff `reach` high (or the -/// shape's own, if higher): `reach` is the highest puff of the shapes drawn together. With `mesh`, -/// its faces before projection instead. -fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, mesh: bool) -> Result { +/// shape's own, if higher): `reach` is the highest puff of the shapes drawn together. Its faces +/// before projection instead, or both, as `keep` asks. +fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, keep: Keep) -> Result { #[cfg(test)] INFLATED.with(|n| n.set(n.get() + 1)); let Puff { rings, edges, cell, nx, ny, m, height } = p; @@ -149,7 +149,7 @@ fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, mesh: boo let smooth = view.shade && view.has_colored_lights(); let camera = Camera::new(view, bounds, o.depth + 2.0 * height.max(reach)); // Without its back cap the object is a shell open behind, its inside showing. - let mut b = Builder { camera: &camera, closed: o.cap, hidden: false, facets: vec![], mesh: mesh.then(Vec::new) }; + let mut b = Builder::new(&camera, o.cap, false, keep); let half = o.depth / 2.0; // The back: turned and scaled by the twist and taper, its puff too. let back_turn = Turn::new(bounds, o.twist, o.taper, 1.0); @@ -375,15 +375,40 @@ fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, mesh: boo let turn = Turn::new(bounds, o.twist, o.taper, t); ring.iter().map(|p| camera.place(turn.apply(*p), half - o.depth * t)).collect() }; - let mut pieces: Vec> = rings.iter().map(|r| if o.twist != 0.0 { cuts(&at(r, 0), &at(r, 1)) } else { vec![] }).collect(); + let cut: Vec> = rings.iter().map(|r| if o.twist != 0.0 { cuts(&at(r, 0), &at(r, 1)) } else { vec![] }).collect(); + let mut pieces = cut.clone(); fit(&mut pieces, slices, MAX_FACES.saturating_sub(b.len())); let edges = rings.iter().map(Vec::len).fold(0usize, usize::saturating_add); if b.len().saturating_add(edges.saturating_mul(slices)) > MAX_SOLID_FACES { return Err(TOO_COMPLEX_SURFACE.into()); } - for (ring, pieces) in rings.iter().zip(&pieces) { - for k in 0..slices { - b.band(&at(ring, k), &at(ring, k + 1), pieces, |i| outward(ring, i, 0.0))?; + let sides = |b: &mut Builder<'_>, pieces: &[Vec]| -> Result<(), String> { + for (ring, pieces) in rings.iter().zip(pieces) { + for k in 0..slices { + b.band(&at(ring, k), &at(ring, k + 1), pieces, |i| outward(ring, i, 0.0))?; + } + } + Ok(()) + }; + // A mesh kept with the facets has the sides a mesh alone has: limited, and with a twist + // cut, by the faces the mesh holds, which can be more than the facets. + let mut own_sides = None; + if let Some(kept) = b.faces_kept() { + let mut own = cut; + fit(&mut own, slices, MAX_FACES.saturating_sub(kept)); + if kept.saturating_add(edges.saturating_mul(slices)) > MAX_SOLID_FACES { + b.lose_mesh(); + } else if own != pieces { + own_sides = Some((own, b.mesh.take())); + } + } + sides(&mut b, &pieces)?; + if let Some((own, faces)) = own_sides { + let mut m = Builder::new(&camera, o.cap, false, Keep::Faces); + m.mesh = faces; + match sides(&mut m, &own) { + Ok(()) => b.mesh = m.mesh, + Err(_) => b.lose_mesh(), } } } diff --git a/crates/three-d/src/solid/tests.rs b/crates/three-d/src/solid/tests.rs index e89a29afea..335f434419 100644 --- a/crates/three-d/src/solid/tests.rs +++ b/crates/three-d/src/solid/tests.rs @@ -225,12 +225,12 @@ fn a_flat_facet_measures_its_depth_on_its_plane() { /// eye, has no plane: its drawing no longer matches the plane's projection there. #[test] fn a_face_reaching_past_the_eye_has_no_plane() { - use super::build::{Builder, Order}; + use super::build::{Builder, Keep, Order}; let (_, b) = shape(&square(0.0, 0.0, 100.0)); let camera = Camera::new(View { perspective: 50.0, ..front() }, b, 0.0); let eye = camera.center_and_eye().1.unwrap(); for (far, has_plane) in [(eye / 2.0, true), (2.0 * eye, false)] { - let mut builder = Builder { camera: &camera, closed: false, hidden: false, facets: vec![], mesh: None }; + let mut builder = Builder::new(&camera, false, false, Keep::Facets); let corners = vec![V3(-10.0, -10.0, 0.0), V3(10.0, -10.0, 0.0), V3(0.0, 10.0, far)]; builder.face(vec![corners], V3(0.0, -far, 20.0), Order::Depth).unwrap(); assert_eq!(builder.facets.len(), 1, "{far}"); diff --git a/crates/tools/src/guides/tests.rs b/crates/tools/src/guides/tests.rs index 602242d1cd..16b46f6fb9 100644 --- a/crates/tools/src/guides/tests.rs +++ b/crates/tools/src/guides/tests.rs @@ -1028,6 +1028,124 @@ fn a_drawn_point_lands_on_a_corner_of_a_glyphs_bounds() { } } +/// Snap to Glyph: both ends of a gradient vector drawn on the art with the Gradient tool, a click +/// that applies one, and its dragged start and end handles, land on a glyph's anchor point as a +/// Pen click does, with the guides shown while hovering, and not with Snap to Glyph off. A drawn +/// end also lands on a slanted path's segment, which no bounds line passes along, and on a +/// construction guide through its start, and with Shift on an anchor along its 45° step. With nothing +/// selected, hovering off the art shows no guides. A press that snaps still starts a drag only +/// after the pointer moves 3 px from where it was pressed. +#[test] +fn gradient_vector_ends_land_on_a_glyph_anchor() { + use crate::xform::GradientTool; + use crate::{Action, PointerEvent, PointerKind, Tool}; + use vectorcraft_color::{Gradient, GradientGeom, GradientPaint, Paint}; + use vectorcraft_doc::Appearance; + use vectorcraft_geom::shapes; + let (mut d, t) = glyph_doc("H", 0.0); + let lay = layout_text(FontDb::global(), &t); + let g = lay.glyphs.iter().find(|g| !g.outline.elements().is_empty()).expect("a glyph with an outline"); + // An anchor inside the glyph's bounds, which no glyph bounds line passes through. + let bb = g.outline.bounding_box(); + let inside = |p: Point| p.x > bb.x0 + 1.0 && p.x < bb.x1 - 1.0 && p.y > bb.y0 + 1.0 && p.y < bb.y1 - 1.0; + let anchor = t.xf * g.outline.segments().map(|s| s.start()).find(|p| inside(*p)).expect("an anchor inside the glyph"); + let type_id = d.children(Some(d.layers[0].id)).unwrap()[0].id; + // A rectangle away from the type, with a gradient from (300, 100) to (400, 100). + let (l, rect) = (d.layers[0].id, d.alloc_id()); + d.insert(Some(l), 0, Node::path(rect, shapes::rectangle(Rect::new(300.0, 50.0, 400.0, 150.0)), Appearance::default_art())).unwrap(); + let mut gp = GradientPaint::new(Gradient::default()); + gp.geom = Some(GradientGeom { start: Point::new(300.0, 100.0), end: Point::new(400.0, 100.0), aspect: 1.0, focal: None }); + d.node_mut(rect).unwrap().appearance.set_fill(Paint::Gradient(Box::new(gp))); + // A slanted line, its middle far from its bounds' edges. + let (a0, a1) = (Point::new(200.0, 420.0), Point::new(280.0, 480.0)); + let line = d.alloc_id(); + d.insert(Some(l), 0, Node::path(line, shapes::line(a0, a1), Appearance::default_art())).unwrap(); + let mut on_type = Selection::default(); + on_type.add(type_id); + let mut on_rect = Selection::default(); + on_rect.add(rect); + let p = paint(); + let click = anchor + Vec2::new(1.5, -1.0); + let away = anchor + Vec2::new(-83.3, -61.7); + let ev = |kind, q: Point| PointerEvent::new(kind, q.x, q.y); + let preview = |acts: &[Action]| { + acts.iter().find_map(|a| if let Action::Preview(_, v) = a { Some(v.clone()) } else { None }).unwrap_or_else(|| panic!("{acts:?}")) + }; + let at = |v: &serde_json::Value| Point::new(v[0].as_f64().unwrap(), v[1].as_f64().unwrap()); + for glyphs in [true, false] { + let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &on_type, &p) }; + let lands = |q: Point| q.distance(anchor) < 1e-9; + // The pointer hovering at the click shows the guides of the anchor a vector would start on. + let mut tool = GradientTool::default(); + tool.pointer(&c, &ev(PointerKind::Move, click)); + assert_eq!(!tool.overlays(&c).is_empty(), glyphs, "hover, Snap to Glyph {glyphs}"); + // A vector drawn from the click starts on the anchor. + tool.pointer(&c, &ev(PointerKind::Down, click)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click + Vec2::new(80.0, 60.0)))); + assert_eq!(lands(at(&v["start"])), glyphs, "start, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click + Vec2::new(80.0, 60.0))); + // One drawn to beside the slanted line, a quarter of the way along (15 pt or more from its + // bounds' edges, centre and anchors), ends on the line. + let dir = (a1 - a0) / (a1 - a0).hypot(); + let quarter = a0 + (a1 - a0) * 0.25; + tool.pointer(&c, &ev(PointerKind::Down, away)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, quarter + Vec2::new(-dir.y, dir.x) * 2.0))); + assert!((at(&v["end"]) - a0).cross(dir).abs() < 1e-6, "end on the line: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, quarter)); + // One drawn to the click ends on it. + tool.pointer(&c, &ev(PointerKind::Down, away)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click))); + assert_eq!(lands(at(&v["end"])), glyphs, "end, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click)); + // A click at the click applies a gradient that starts on the anchor. + tool.pointer(&c, &ev(PointerKind::Down, click)); + let acts = tool.pointer(&c, &ev(PointerKind::Up, click)); + let v = acts.iter().find_map(|a| if let Action::Exec(_, v) = a { Some(v.clone()) } else { None }).unwrap_or_else(|| panic!("{acts:?}")); + assert_eq!(lands(at(&v["start"])), glyphs, "click, Snap to Glyph {glyphs}: {v}"); + // One drawn to 2 px below the horizontal through its start, far from any target, ends on + // that construction guide, as a Line Segment's end does. + tool.pointer(&c, &ev(PointerKind::Down, away)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, away + Vec2::new(150.0, 2.0)))); + assert!((at(&v["end"]).y - away.y).abs() < 1e-9, "end on the construction guide: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, away + Vec2::new(150.0, 2.0))); + // With Shift, one drawn at 45° toward the line's first anchor slides along that step onto it. + let shift = |kind, q: Point| PointerEvent { mods: crate::Mods { shift: true, ..Default::default() }, ..ev(kind, q) }; + tool.pointer(&c, &shift(PointerKind::Down, a0 + Vec2::new(60.0, -60.0))); + let v = preview(&tool.pointer(&c, &shift(PointerKind::Drag, a0 + Vec2::new(-1.5, 0.5)))); + assert!(at(&v["end"]).distance(a0) < 1e-9, "Shift end on the anchor: {v}"); + tool.pointer(&c, &shift(PointerKind::Up, a0)); + // A press 3 px from the anchor, then a move of 2 px away from it: no drag yet. + let near = anchor + Vec2::new(2.5, -1.5); + tool.pointer(&c, &ev(PointerKind::Down, near)); + let acts = tool.pointer(&c, &ev(PointerKind::Drag, near + Vec2::new(1.5, -1.0))); + assert!(acts.is_empty(), "drag start, Snap to Glyph {glyphs}: {acts:?}"); + tool.pointer(&c, &ev(PointerKind::Up, near + Vec2::new(1.5, -1.0))); + // The end handle of the rectangle's gradient dragged to the click lands on the anchor. + let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &on_rect, &p) }; + let mut tool = GradientTool::default(); + // Hovering over the handle (on the rectangle's edge) shows no guides. + tool.pointer(&c, &ev(PointerKind::Move, Point::new(400.0, 100.0))); + assert!(!tool.overlays(&c).iter().any(|o| matches!(o, Overlay::Label { .. })), "hover over the handle"); + tool.pointer(&c, &ev(PointerKind::Down, Point::new(400.0, 100.0))); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click))); + assert_eq!(lands(at(&v["end"])), glyphs, "end handle, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click)); + // The start handle dragged to the click lands on the anchor. + tool.pointer(&c, &ev(PointerKind::Down, Point::new(300.0, 100.0))); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click))); + assert_eq!(lands(at(&v["start"])), glyphs, "start handle, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click)); + // With nothing selected, hovering beside the rectangle (off the art and its 1 pt stroke, + // where a press draws nothing, but within the snapping tolerance of its edge) shows no + // guides. + let none = Selection::default(); + let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &none, &p) }; + let mut tool = GradientTool::default(); + tool.pointer(&c, &ev(PointerKind::Move, Point::new(296.2, 75.0))); + assert!(tool.overlays(&c).is_empty(), "hover off the art: {:?}", tool.overlays(&c)); + } +} + /// With Smart Guides off, View → Snap to Point still pulls a drawn point onto an anchor, but /// draws no Smart Guides: no "anchor" label, no lines. #[test] diff --git a/crates/tools/src/xform/gradient.rs b/crates/tools/src/xform/gradient.rs index 9c8881b580..9a61c98591 100644 --- a/crates/tools/src/xform/gradient.rs +++ b/crates/tools/src/xform/gradient.rs @@ -7,8 +7,10 @@ //! //! The annotator is a bar from a round start handle to a square end handle. Drag the start handle //! (or the bar) to move the gradient, the end handle to change its length and angle, and just past -//! the end to rotate it. Dragged handles snap to anchors, edges and smart guides; Shift instead -//! constrains the vector to 45° steps from the Constrain Angle preference. Colour stops sit under the bar: click the bar to add +//! the end to rotate it. A vector drawn on the art, a click that applies one, and the dragged +//! handles snap as drawn points do (the anchors and paths of the art, and with Snap to Glyph the +//! guides of live type); Shift keeps the dragged end or handle at 45° steps from the Constrain Angle +//! preference, snapping along that step. Colour stops sit under the bar: click the bar to add //! one, drag one to move it (Alt drags a copy), drag it off the bar to delete it, and drag a //! diamond above the bar to move a midpoint. Double-clicking a stop opens its popover. The //! selected stop (`gradient.selectStop`) is shared with the Gradient and Color panels: Delete or @@ -43,7 +45,7 @@ use vectorcraft_geom::{Affine, Point, Shape, Vec2, constrain_angle_from}; use super::freeform::{self, Annotator as FreeformAnnotator}; use super::paint_owner; -use crate::guides::snap_draw; +use crate::guides::{DrawSnap, Leave}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; const BAR: [u8; 3] = [0x20, 0x20, 0x20]; @@ -239,6 +241,8 @@ struct Gesture { grab: Grab, /// Where the press happened. at: Point, + /// Where a vector drawn on the art starts: the press, snapped. + start: Point, /// Moved past the drag threshold (an interaction is open). began: bool, /// The vector drawn so far (Art drags). @@ -255,8 +259,9 @@ pub struct GradientTool { free: Option, /// The line Lines mode is drawing on a freeform gradient. lines: freeform::Lines, - /// Smart-guide feedback of the handle being dragged. - guides: Vec, + /// Smart Guides for the vector being drawn or the handle being dragged, and for the pointer + /// hovering over the art. + snap: DrawSnap, } /// Overlays of an annotator: the bar, the handles, the midpoint diamonds and a chip per stop; a @@ -378,18 +383,40 @@ impl GradientTool { Some((a, Part::Aspect)) => Grab::Aspect(GradientGeom { aspect: a.geom.aspect.abs(), ..a.geom }), None => { if cx.selection.is_empty() { - let Some(h) = hit_test(cx.doc, p, cx.hit_options()) else { return out }; + let Some(h) = hit_test(cx.doc, p, cx.hit_options()) else { + self.snap.clear(); + return out; + }; out.push(Action::Exec("select.set".into(), json!({ "ids": [paint_owner(cx.doc, h.leaf).0] }))); } Grab::Art } }; - // A vector drawn on the art starts on the snapped point. - let at = if grab == Grab::Art { snap_draw(cx, p, &[]).0 } else { p }; - self.gesture = Some(Gesture { grab, at, began: false, vector: None, stop: None }); + // The targets are gathered for a drag that snaps. A vector drawn on the art starts on the + // snapped point; the drag threshold still counts from the press. + let snaps = matches!(grab, Grab::Art | Grab::Move { .. } | Grab::End(_)); + let snapped = if snaps { self.snap.press(cx, p, &[], None) } else { p }; + let start = if grab == Grab::Art { snapped } else { p }; + if grab != Grab::Art { + self.snap.clear(); + } + self.gesture = Some(Gesture { grab, at: p, start, began: false, vector: None, stop: None }); out } + /// Snap the pointer hovering where a press would draw a vector (anywhere with art selected, + /// over art with nothing selected), to show where the vector starts; no guides over the + /// annotator, elsewhere with nothing selected, or during a press. + fn hover(&mut self, cx: &ToolContext, p: Point) { + let on_annotator = || Annotator::of(cx).is_some_and(|a| a.hit(cx, p).is_some()); + let off_art = || cx.selection.is_empty() && hit_test(cx.doc, p, cx.hit_options()).is_none(); + if self.gesture.is_some() || self.free.is_some() || FreeformAnnotator::of(cx).is_some() || on_annotator() || off_art() { + self.snap.clear(); + } else { + self.snap.hover(cx, p, &[], None); + } + } + fn drag(&mut self, cx: &ToolContext, p: Point, mods: Mods) -> Vec { let Some(g) = &mut self.gesture else { return vec![] }; let mut out = vec![]; @@ -401,23 +428,16 @@ impl GradientTool { out.push(Action::Begin("Gradient".into())); } let constrain = |v: Vec2| constrain_angle_from(v, 45.0, cx.constrain_angle); - // Where a handle dragged to `to` lands: Shift constrains the vector from `from`, else it - // snaps (keeping the guides to show). - let mut guides = vec![]; - let mut place = |from: Point, to: Point| { - if mods.shift { - from + constrain(to - from) - } else { - let (q, o) = snap_draw(cx, to, &[]); - guides = o; - q - } - }; + // Where a handle dragged to `to` lands: it snaps as a drawn segment's end leaving `from` + // does, Shift keeping it at 45° steps from `from` (keeping the guides to show). + let snap = &mut self.snap; + snap.clear(); + let mut place = |from: Point, to: Point| snap.drag(cx, to, Some(&Leave::segment(cx, from, mods.shift))); let (cmd, params) = match &g.grab { Grab::Art => { - let end = place(g.at, p); - g.vector = Some((g.at, end)); - ("paint.setGradientGeom", Self::geom_params(cx, g.at, end)) + let end = place(g.start, p); + g.vector = Some((g.start, end)); + ("paint.setGradientGeom", Self::geom_params(cx, g.start, end)) } Grab::Move { geom, .. } => { // The start handle snaps; Shift constrains the move. @@ -469,13 +489,12 @@ impl GradientTool { ("paint.editGradient", Self::stops_params(cx, &set_midpoint(stops, *index, m))) } }; - self.guides = guides; out.push(Action::Preview(cmd.into(), params)); out } fn release(&mut self, cx: &ToolContext, p: Point) -> Vec { - self.guides.clear(); + self.snap.clear(); let Some(g) = self.gesture.take() else { return vec![] }; if g.began { let mut out = vec![Action::Commit]; @@ -494,12 +513,12 @@ impl GradientTool { let (stops, i) = insert_stop(&a.gradient, t); vec![Action::Exec("paint.editGradient".into(), Self::stops_params(cx, &stops)), Self::select_stop(i)] } - // A click inside selected art applies the gradient from that point. + // A click inside selected art applies the gradient from that point, snapped. Grab::Art => { let Some(h) = hit_test(cx.doc, p, cx.hit_options()) else { return vec![] }; let id = paint_owner(cx.doc, h.leaf); let Some(v) = cx.selection.objects.contains(&id).then(|| click_vector(cx, id)).flatten() else { return vec![] }; - let mut params = Self::geom_params(cx, p, p + v); + let mut params = Self::geom_params(cx, g.start, g.start + v); // Without `ids`, the engine refuses the gradient while an object of a dynamic symbol // instance's art is selected. if cx.selection.symbol_part.is_some() { @@ -550,6 +569,9 @@ impl Tool for GradientTool { } _ => {} } + if matches!(ev.kind, PointerKind::Move) { + self.hover(cx, ev.pos); + } match ev.kind { PointerKind::Down => self.press(cx, ev.pos, ev.mods), PointerKind::Drag => self.drag(cx, ev.pos, ev.mods), @@ -579,14 +601,19 @@ impl Tool for GradientTool { return self.key(cx, ToolKey::Escape, Mods::default()); } (self.gesture, self.free) = (None, None); - self.guides.clear(); + self.snap.clear(); + vec![] + } + + fn deactivate(&mut self, _cx: &ToolContext) -> Vec { + self.snap.clear(); vec![] } fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec { if key == ToolKey::Escape && self.busy() { self.gesture = None; - self.guides.clear(); + self.snap.clear(); return self.free.take().map_or_else(|| vec![Action::Cancel], |g| freeform::cancel(&g)); } if self.busy() || !self.claims_key(cx, key) { @@ -626,7 +653,7 @@ impl Tool for GradientTool { } _ => vec![], }; - o.extend(self.guides.iter().cloned()); + o.extend(self.snap.guides().iter().cloned()); o } diff --git a/crates/trace/tests/quality/centre_lines.rs b/crates/trace/tests/quality/centre_lines.rs index 4cc8eb3321..c32d7dd362 100644 --- a/crates/trace/tests/quality/centre_lines.rs +++ b/crates/trace/tests/quality/centre_lines.rs @@ -425,7 +425,7 @@ pub fn report() -> String { } } let mut md = String::from( - "## Centre lines\n\nCode-drawn scenes only: 23 scenes, each anti-aliased (8×8 coverage samples) and hard (that coverage thresholded at 0.5). Coordinates, widths and scene Stroke values are fixed; junction/letter/crossing coordinates include the 0.27/0.41 px offset. Create Fills and Strokes are on, Stroke is the scene's limit (13 px, mix 6 px, varying 12 px), Black and White threshold 128, Noise 4, Ignore White on, other settings at their defaults. Today's tracer has no minimum-length setting (the dashed scenes specify 0 for future use).\n\nDistances use dense exact centre samples (≤0.25 px), emitted curves sampled at ≤0.5 px control-polygon steps, and nearest-point searches capped at 30 px. Found is the fraction of true samples within 1.5 px; width errors use output samples within 2 px of a true stroke. Samples inside exact fills are excluded. Topology is recovered from emitted subpath endpoints and intersections, merging coincident positions within 0.05 px; an endpoint contributes one arm, an interior two. Junction matching radius is local true width + 3 px; ends use half-width + 4 px. Each match is used once. Free ends exclude true junctions, corners, other strokes and fills; unmatched emitted ends are spurs. Junctions/ends within 2 px of fills are excluded. End error is distance, with signed error along the outward tangent in parentheses. Coverage is a union of painted output at pixel centres (0.02 px flattening tolerance, actual emitted caps), compared with the exact swept shape at pixel centres, for both input variants. Area error is signed relative to exact area. Anchors counts stroke anchors, including all compound subpaths; paths counts stroked + filled paths. Time covers tracing only, one scene at a time. Profile-dependent columns are unavailable (–).\n\n### Centre line and width (px)\n\n| scene | input | paths | out→true mean / p95 | true→out mean / p95 | found % | width err uniform mean / p95 | variable mean / p95 | profile mean |\n|---|---|---|---|---|---|---|---|---|\n", + "## Centre lines\n\nCode-drawn scenes only: 24 scenes, each anti-aliased (8×8 coverage samples) and hard (that coverage thresholded at 0.5). Coordinates, widths and scene Stroke values are fixed; junction/letter/crossing/mixed-width body coordinates include the 0.27/0.41 px offset. Create Fills and Strokes are on, Stroke is the scene's limit (13 px, mix and mixed-width body 6 px, varying 12 px), Black and White threshold 128, Noise 4, Ignore White on, other settings at their defaults. Today's tracer has no minimum-length setting (the dashed scenes specify 0 for future use).\n\nDistances use dense exact centre samples (≤0.25 px), emitted curves sampled at ≤0.5 px control-polygon steps, and nearest-point searches capped at 30 px. Found is the fraction of true samples within 1.5 px; width errors use output samples within 2 px of a true stroke. Samples inside exact fills are excluded. Topology is recovered from emitted subpath endpoints and intersections, merging coincident positions within 0.05 px; an endpoint contributes one arm, an interior two. Junction matching radius is local true width + 3 px; ends use half-width + 4 px. Each match is used once. Free ends exclude true junctions, corners, other strokes and fills; unmatched emitted ends are spurs. Junctions/ends within 2 px of fills are excluded. End error is distance, with signed error along the outward tangent in parentheses. Coverage is a union of painted output at pixel centres (0.02 px flattening tolerance, actual emitted caps), compared with the exact swept shape at pixel centres, for both input variants. Area error is signed relative to exact area. Anchors counts stroke anchors, including all compound subpaths; paths counts stroked + filled paths. Time covers tracing only, one scene at a time. Profile-dependent columns are unavailable (–).\n\n### Centre line and width (px)\n\n| scene | input | paths | out→true mean / p95 | true→out mean / p95 | found % | width err uniform mean / p95 | variable mean / p95 | profile mean |\n|---|---|---|---|---|---|---|---|---|\n", ); for m in &rows { writeln!( @@ -544,7 +544,44 @@ fn known_geometry_measures_junctions_ends_missing_ink_and_caps() { let square = render(&TraceResult { paths: vec![TracedPath { square_caps: true, ..diagonal }], ..Default::default() }, 50, 50, false); assert!(square.iter().filter(|p| **p).count() > round.iter().filter(|p| **p).count()); // Preserve the exact catalogue and special scene limits. - assert_eq!(scenes::all().len(), 23); - assert_eq!((scenes::mix().wmax, scenes::varying().wmax, scenes::dashed(2.0).min_len), (6.0, 12.0, 0.0)); + assert_eq!(scenes::all().len(), 24); + assert_eq!((scenes::mix().wmax, scenes::mixed_width_body().wmax, scenes::varying().wmax, scenes::dashed(2.0).min_len), (6.0, 6.0, 12.0, 0.0)); assert_eq!(scenes::junctions(4.0).junctions[0].0, P::new(60.27, 40.41)); } + +#[test] +fn mixed_width_body_is_one_shape_traced_as_a_fill_and_strokes() { + let sc = scenes::mixed_width_body(); + assert_eq!((sc.fills.len(), sc.strokes.len(), sc.junctions.len(), sc.corners.len()), (1, 3, 0, 0)); + let cov = sc.rasterise(); + let hard: Vec = cov.iter().map(|&c| if c >= 0.5 { 1.0 } else { 0.0 }).collect(); + // The inked pixels form one 8-connected component. + let ink: Vec = hard.iter().map(|&c| c > 0.0).collect(); + let start = ink.iter().position(|&v| v).unwrap(); + let mut seen = vec![false; ink.len()]; + let mut stack = vec![start]; + seen[start] = true; + let mut reached = 0; + while let Some(i) = stack.pop() { + reached += 1; + let (x, y) = ((i % sc.w) as i64, (i / sc.w) as i64); + for (dx, dy) in [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] { + let (nx, ny) = (x + dx, y + dy); + if nx < 0 || ny < 0 || nx >= sc.w as i64 || ny >= sc.h as i64 { + continue; + } + let j = ny as usize * sc.w + nx as usize; + if ink[j] && !seen[j] { + seen[j] = true; + stack.push(j); + } + } + } + assert_eq!(reached, ink.iter().filter(|&&v| v).count()); + // Create Strokes splits the shape: its body is one fill and each arm is a stroke reaching its free end. + for (input, c) in [("aa", &cov), ("hard", &hard)] { + let m = evaluate(&sc, input, c); + assert_eq!((m.strokes, m.fills, m.ends_gt, m.ends_found), (3, 1, 3, 3), "{m:?}"); + assert!(m.complete > 0.95 && m.fill_as_fill > 0.95, "{m:?}"); + } +} diff --git a/crates/trace/tests/quality/centre_scenes.rs b/crates/trace/tests/quality/centre_scenes.rs index 38e4eb3f73..7c1956b2a6 100644 --- a/crates/trace/tests/quality/centre_scenes.rs +++ b/crates/trace/tests/quality/centre_scenes.rs @@ -388,6 +388,19 @@ pub fn mix() -> Scene { s } +/// One connected shape: a wide disc with three thin arms of 2, 3 and 5 px reaching out of it, +/// and nothing else; Stroke width 6. +pub fn mixed_width_body() -> Scene { + let mut s = Scene::new("mixed-width body", 260, 220, 6.0); + s.offset = P::new(0.27, 0.41); + let c = P::new(100.0, 100.0); + s.fills.push(GtFill::Disc(c + s.offset, 24.0)); + s.line(&[c, P::new(220.0, 100.0)], 3.0); + s.line(&[c, P::new(100.0, 20.0)], 5.0); + s.line(&[c, P::new(25.0, 185.0)], 2.0); + s +} + /// Strokes whose width varies: tapers, a lens, a swelling brush curve; Stroke width 12. pub fn varying() -> Scene { let mut s = Scene::new("varying", 480, 300, 12.0); @@ -441,6 +454,7 @@ pub fn all() -> Vec { v.push(letters(w)); } v.push(mix()); + v.push(mixed_width_body()); v.push(varying()); v } diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index d606f630d5..c870110434 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -55,6 +55,42 @@ fn selection_tool_with_selection(app: &VectorcraftApp) -> bool { && app.session.active().is_some_and(|st| !st.selection.is_empty()) } +/// Whether the bar shows its blank canvas actions, Draw (Pencil) and Place Image: a selection tool is +/// in use and the active document has no artwork, only layers and guides. +fn blank_canvas(app: &VectorcraftApp) -> bool { + fn no_art(nodes: &[std::sync::Arc]) -> bool { + nodes.iter().all(|n| n.is_guide() || (n.is_layer() && n.children().is_none_or(|c| no_art(c)))) + } + matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection") + && app.session.active().is_some_and(|st| st.selection.is_empty() && no_art(&st.doc.layers)) +} + +/// Where the bar's top edge centers, on screen: under the lowest corner on screen of the selected +/// artboards or the selection, centered on them (the view may be rotated); on a blank canvas, which +/// has no selection, [`blank_canvas_anchor`]. None with nothing to sit under. +fn bar_anchor(app: &mut VectorcraftApp, xf: &Xf, on_artboards: bool, blank: bool) -> Option { + if blank { + return Some(blank_canvas_anchor(app, xf)); + } + let b = if on_artboards { picked_artboards_bounds(app) } else { app.selection_bounds() }?; + let lowest = xf.quad(b).iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max); + Some(pos2(xf.to_screen(b.center()).x, lowest)) +} + +/// Where the blank canvas bar's top edge centers, on screen: under the bottom of the active artboard +/// when part of it is in view, else the canvas's center. +fn blank_canvas_anchor(app: &VectorcraftApp, xf: &Xf) -> Pos2 { + let board = app.session.active().and_then(|st| { + let last = st.doc.artboards.len().checked_sub(1)?; + st.doc.artboards.get(app.view().map_or(0, |v| v.artboard).min(last)).map(|a| a.rect) + }); + let shown = board.map(|b| (b, xf.quad(b))).filter(|(_, q)| egui::Rect::from_points(q).intersects(xf.rect)); + match shown { + Some((b, q)) => pos2(xf.to_screen(b.center()).x, q.iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max)), + None => xf.rect.center(), + } +} + /// Whether a Type tool edits type, or has type selected: its bar then shows only the text /// formatting controls. fn type_tool_on_type(app: &VectorcraftApp) -> bool { @@ -75,6 +111,8 @@ pub(crate) fn task_bar_shows_type(app: &VectorcraftApp) -> bool { /// While a Type tool edits type, or has type selected, the bar shows only those controls and the /// Fill. When one row would be wider than the canvas, the font controls take a row of their own, /// narrowed to fit it, and the actions wrap onto rows under the handle's ([`bar_layout`]). +/// On a blank canvas, a document with no artwork and a selection tool in use, it sits under the +/// active artboard and offers Draw (Pencil) and Place Image. /// Its handle drags it anywhere on the canvas; unpinned it keeps that offset as it follows the /// selection, pinned it stays put ([`crate::state::TaskBarPlace`]). pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { @@ -83,13 +121,15 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { // Text added with a Type tool shows its formatting controls, as the help page describes // (). let typing = type_tool_on_type(app); - if !(on_artboards || typing || selection_tool_with_selection(app)) { + // A blank canvas offers Draw (Pencil) and Place Image, as the same help page describes. + let blank = !on_artboards && blank_canvas(app); + if !(on_artboards || typing || blank || selection_tool_with_selection(app)) { return; } // Selected type has the font controls too, as the help page describes // (). let fonts = task_bar_shows_type(app); - let Some(b) = (if on_artboards { picked_artboards_bounds(app) } else { app.selection_bounds() }) else { return }; + let Some(anchor) = bar_anchor(app, xf, on_artboards, blank) else { return }; let Some(st) = app.session.active() else { return }; let n = st.selection.len(); let first = st.selection.objects.first().and_then(|id| st.doc.node(*id)); @@ -137,6 +177,10 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { items.push((tl!("Rearrange All"), "dc-rearrange", TASK_BAR_REARRANGE_ARTBOARDS, none())); } items.push((tl!("Export"), "share-2", "file.exportForScreens", none())); + } else if blank { + // Draw (Pencil) switches to the Pencil tool; Place Image picks files for the Place dialog. + items.push((tl!("Draw (Pencil)"), "pencil", "tool.select", json!({"tool": "pencil"}))); + items.push((tl!("Place Image"), "image", "file.place", none())); } else if !typing { // The selection tools' object actions. A Type tool's bar leaves them out, Create Outlines // and the type conversions included. @@ -234,27 +278,20 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { let paints = chip_paints(app, ui.ctx()); let colored = has_colors(&st.doc, &st.selection.objects); let doc = st.uid; - // Under the selection's lowest corner on screen, centered on the selection (the view may be - // rotated). - let lowest = xf.quad(b).iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max); - let anchor = pos2(xf.to_screen(b.center()).x, lowest); let bounds = xf.rect.shrink(8.0); // The canvas's width less the frame's margins and stroke. let room = bounds.width() - BAR_FRAME; // On a canvas too narrow for the type controls at their narrowest, the bar leaves them out. let fonts = fonts && room >= crate::panels::character::task_bar_min_room(); // The selected objects' Fill, Stroke, Recolor Artwork (when they have colours to recolour) and - // Lock (a basic shape's properties instead), left out with the Artboard tool. A Type tool's bar - // has the Fill alone. - let chips: Vec = [ - (!on_artboards, Chip::Fill), - (!on_artboards && !typing, Chip::Stroke), - (!on_artboards && !typing && colored, Chip::Recolor), - (!on_artboards && !typing, Chip::Lock), - ] - .into_iter() - .filter_map(|(shown, chip)| shown.then_some(chip)) - .collect(); + // Lock (a basic shape's properties instead), left out with the Artboard tool and on a blank + // canvas, which have no selected objects. A Type tool's bar has the Fill alone. + let objects = !on_artboards && !blank; + let chips: Vec = + [(objects, Chip::Fill), (objects && !typing, Chip::Stroke), (objects && !typing && colored, Chip::Recolor), (objects && !typing, Chip::Lock)] + .into_iter() + .filter_map(|(shown, chip)| shown.then_some(chip)) + .collect(); let widths: Vec = items.iter().map(|(label, ..)| action_width(ui, label)).collect(); let font_w = fonts.then(|| { let w = crate::panels::character::TASK_BAR_WIDTHS; diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 2a7162f9a1..4d609f2b5e 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -10333,7 +10333,7 @@ fn task_bar_labels(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<(String /// With the Artboard tool, the Contextual Task Bar sits under the selected artboards and offers /// Resize, Duplicate, Rearrange All (with two or more artboards) and Export instead of the /// selected objects' actions. With the Selection tool it offers the objects' actions again, and -/// with nothing selected it doesn't show. +/// with nothing in the document the blank canvas actions. #[test] fn the_task_bar_offers_the_artboard_tools_actions() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); @@ -10341,7 +10341,7 @@ fn the_task_bar_offers_the_artboard_tools_actions() { app.select_tool("selection"); let ctx = egui::Context::default(); let names = |app: &mut VectorcraftApp| task_bar_labels(app, &ctx).into_iter().map(|(t, _)| t).collect::>(); - assert!(names(&mut app).is_empty() && task_bar_rect(&ctx).is_none(), "nothing selected: no bar"); + assert_eq!(names(&mut app), ["Draw (Pencil)", "Place Image"], "a blank canvas"); app.select_tool("artboard"); crate::panels::artboards::select(&mut app, 1); diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index 5ac4025c24..fc06926247 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -30,7 +30,8 @@ pub fn anchor_controls(app: &VectorcraftApp) -> AnchorControls { /// What the Control bar and the Properties panel offer for selected anchors ([`anchor_controls`]): /// "Convert:" corner or smooth, then "Anchors:" remove, connect (Join) and cut. Each group is a -/// row of its own: inline in the Control bar, one under the other in a panel. +/// row of its own: inline in the Control bar, one under the other in a panel. In the Control bar's +/// row, a group shows only while the row has room for its label and all its buttons. pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorControls) { let t = Tokens::get(ui.ctx()); let mut run = None; @@ -56,9 +57,14 @@ pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorCon AnchorControls::Convert => 1, AnchorControls::All => groups.len(), }; + let in_row = ui.layout().main_dir() == egui::Direction::LeftToRight; for (label, buttons) in groups.into_iter().take(shown) { + let text = egui::RichText::new(label).size(12.0).color(t.text); + if in_row && ui.available_width() < row_width(ui, &[widgets::text_width(ui, text.clone())]) + buttons_width(ui, buttons.len()) { + break; + } ui.horizontal(|ui| { - ui.label(egui::RichText::new(label).size(12.0).color(t.text)); + ui.label(text); for &(icon, tip, id, to) in buttons { if widgets::icon_button(ui, icon, tip, false, 24.0).clicked() { run = Some((id, to.map_or_else(|| json!({}), |to| json!({ "to": to })))); @@ -78,6 +84,12 @@ pub(super) const ISOLATION_BUTTON: &str = "cb-isolation"; /// less, the menu is left out so those controls still fit the narrowest window. const ARTBOARD_SNAPPING_WIDTH: f32 = 900.0; +/// The width of [`snapping_menu`]'s button: its label, as a button draws it, and the button's padding. +fn snapping_width(ui: &Ui) -> f32 { + let label = egui::WidgetText::from(tl!("Snapping")).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Button); + label.size().x + 2.0 * ui.spacing().button_padding.x +} + /// The Control bar's snapping quick access; every change uses its existing command, and Pixel /// Snapping Options… opens the dialog of `view.snapToPixel.options`. fn snapping_menu(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -115,9 +127,10 @@ fn snapping_menu(app: &mut VectorcraftApp, ui: &mut Ui) { /// while a symbol's art is the isolated layer. Otherwise, with one object selected that /// [`vectorcraft_tools::select::isolates`], it is Isolate Selected Object, which also isolates a /// path inside a group that Direct Selection selected. An envelope's contents get no button: the -/// envelope's Edit Envelope and Edit Contents buttons show instead. +/// envelope's Edit Envelope and Edit Contents buttons show instead. It is left out when the bar has +/// no room left for it. fn isolation_button(app: &mut VectorcraftApp, ui: &mut Ui) { - let Some(st) = app.session.active() else { return }; + let Some(st) = app.session.active().filter(|_| ui.available_width() >= BUTTON) else { return }; let (icon, tip, id) = if st.isolation.is_some() { if st.doc.pattern_edit.is_some() { return; @@ -151,9 +164,9 @@ const GLOBAL_EDIT_OPTIONS_WIDTH: f32 = 24.0; /// It opens the Properties panel's Global Edit Options, which set the same session options, and is /// off and closed while Global Edit is on, as that panel's dropdown is. It sits after Recolor /// Artwork, ahead of the X/Y/W/H fields that the bar leaves out when it runs short of room, and is -/// left out only when no room is left for it. -fn global_edit_options(app: &mut VectorcraftApp, ui: &mut Ui) { - if !crate::menus::enabled(app, "select.globalEdit") || ui.available_width() < GLOBAL_EDIT_OPTIONS_WIDTH { +/// left out when the room left, less `reserve` ([`transform_reserve`]), cannot hold it. +fn global_edit_options(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) { + if !crate::menus::enabled(app, "select.globalEdit") || ui.available_width() < row_width(ui, &[GLOBAL_EDIT_OPTIONS_WIDTH]) + reserve { return; } let on = app.session.active().is_some_and(|d| d.global_edit.is_some()); @@ -204,8 +217,27 @@ fn buttons_width(ui: &Ui, n: usize) -> f32 { (BUTTON + ui.spacing().item_spacing.x) * n as f32 } +/// The room controls `widths` wide take side by side in the bar's row, with the spacing after each. +fn row_width(ui: &Ui, widths: &[f32]) -> f32 { + widths.iter().map(|w| w + ui.spacing().item_spacing.x).sum() +} + +/// The size of the Control bar's Fill, Stroke and Graphic Style chips. +const PAINT_CHIP: f32 = 22.0; + +/// The width each of those chips takes with the chevron after it. +const CHIP_WIDTH: f32 = PAINT_CHIP + 16.0; + +/// The widths of the Control bar's stroke weight and Opacity fields. +const WEIGHT_FIELD_WIDTH: f32 = 100.0; +const OPACITY_FIELD_WIDTH: f32 = 56.0; + /// The separator that ends a group of the Control bar's controls, as [`group_end_width`] measures it. +/// It is left out when the bar has no room left for it. fn group_end(ui: &mut Ui) { + if ui.available_width() < group_end_width(ui) { + return; + } ui.add_space(GROUP_GAP); boundary(ui, separator); } @@ -256,8 +288,11 @@ fn boundary(ui: &mut Ui, draw: impl FnOnce(&mut Ui)) { } /// The separator that ends a group of the Control bar's controls, drawn as `ui.separator()` draws -/// one, and left out when no control follows it ([`boundary`]). +/// one, and left out when no control follows it ([`boundary`]) or the row has no room left for it. pub(crate) fn group_separator(ui: &mut Ui) { + if ui.layout().main_dir() == egui::Direction::LeftToRight && ui.available_width() < separator_width(ui) { + return; + } boundary(ui, |ui| { ui.separator(); }); @@ -298,20 +333,30 @@ fn end_width(app: &VectorcraftApp, ui: &Ui, nothing_selected: bool) -> f32 { } } +/// The room the Control bar's Fill and Stroke, Opacity, Graphic Style and Character groups leave +/// for the Transform link: the link and the gap and separator ahead of it, while something is +/// `selected` and the panel menu has the Transform group on, or none. Those groups show each control +/// only while this room is left after it, so the bar leaves them out before the link. +fn transform_reserve(app: &VectorcraftApp, ui: &Ui, selected: bool) -> f32 { + if selected && group_on(app, "transform") { group_end_width(ui) + crate::panels::transform::link_width(ui) } else { 0.0 } +} + /// Draws the Control bar's groups between its Character controls and the Transform link, and /// returns whether the Transform link and X/Y/W/H follow (false: nothing is selected, and Document /// Setup and Preferences, drawn here, end the bar, or the Transform group is off). The groups are /// the Paragraph section and area type's vertical alignment (#3759), then the Align buttons, the /// Align To menu and, for two or more objects, the Distribute buttons (a single object keeps the bar /// as it was); each shows only while the end of the bar keeps its room. Groups the panel menu has -/// off are left out, and take no room. -fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize) -> bool { +/// off are left out, and take no room. Without `room` (the Character group had none), all of these +/// groups are left out, so the bar trims them from its right end before the Character group. +fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize, room: bool) -> bool { let nothing = objects == 0; let end = end_width(app, ui, nothing); - if group_on(app, "paragraph") && crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end) { + if room && group_on(app, "paragraph") && crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end) { group_end(ui); } - if group_on(app, "verticalAlignment") + if room + && group_on(app, "verticalAlignment") && crate::panels::properties::vertical_align::shown(app).is_some() && ui.available_width() >= buttons_width(ui, 4) + group_end_width(ui) + end && crate::panels::properties::vertical_align::buttons(app, ui, BUTTON) @@ -319,11 +364,13 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize group_end(ui); } if nothing { + // Each button shows only while the bar has room left for it, so Preferences can show + // when Document Setup did not fit. if group_on(app, "documentOptions") { - if widgets::flat_button(ui, tl!("Document Setup"), DOCUMENT_SETUP_WIDTH).clicked() { + if ui.available_width() >= DOCUMENT_SETUP_WIDTH && widgets::flat_button(ui, tl!("Document Setup"), DOCUMENT_SETUP_WIDTH).clicked() { app.run("file.documentSetup", json!({})).ok(); } - if widgets::flat_button(ui, tl!("Preferences"), PREFERENCES_WIDTH).clicked() { + if ui.available_width() >= PREFERENCES_WIDTH && widgets::flat_button(ui, tl!("Preferences"), PREFERENCES_WIDTH).clicked() { app.run("edit.preferences", json!({})).ok(); } } @@ -331,7 +378,7 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize } let transform = group_on(app, "transform"); let end = if transform { separator_width(ui) + end } else { 0.0 }; - if group_on(app, "alignDistribute") && ui.available_width() >= buttons_width(ui, 7) + end { + if room && group_on(app, "alignDistribute") && ui.available_width() >= buttons_width(ui, 7) + end { crate::panels::align::align_buttons(app, ui, BUTTON); if ui.available_width() >= crate::panels::align::align_to_menu_width(BUTTON) + ui.spacing().item_spacing.x + end { crate::panels::align::align_to_menu(app, ui, BUTTON); @@ -545,55 +592,80 @@ fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { /// The Control bar's Fill and Stroke group: the fill and stroke chips, the Stroke link, the weight /// and the width profile, and the separator after them while a control follows ([`group_separator`]). -fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui) { +/// Short of room, the bar leaves out the width profile first, then the Stroke link with the weight, +/// then the two chips; each shows only while the bar has room for all of it and `reserve` after it +/// ([`transform_reserve`]). +fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) { + if ui.available_width() < row_width(ui, &[CHIP_WIDTH, CHIP_WIDTH]) + reserve { + return; + } let shown_stroke = crate::panels::current_stroke(app); let mixed = crate::panels::stroke_mixed(app, ui.ctx()); let weight = stroke_panel::shown_weight(app, shown_stroke.as_ref(), &mixed); - crate::panels::paint_chip(app, ui, false, 22.0, true); - crate::panels::paint_chip(app, ui, true, 22.0, true); + crate::panels::paint_chip(app, ui, false, PAINT_CHIP, true); + crate::panels::paint_chip(app, ui, true, PAINT_CHIP, true); // The link opens the Stroke panel as a popover under it; then the weight spinner // (with presets) and the width profile. - stroke_panel::link(app, ui, tl!("Stroke:")); - stroke_panel::weight_field(app, ui, "cb-stroke", weight, 100.0); - if let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) { - app.run("stroke.set", json!({"profile": id})).ok(); + let link = widgets::text_width(ui, stroke_panel::link_text(ui, tl!("Stroke:"))); + if ui.available_width() >= row_width(ui, &[link, WEIGHT_FIELD_WIDTH]) + reserve { + stroke_panel::link(app, ui, tl!("Stroke:")); + stroke_panel::weight_field(app, ui, "cb-stroke", weight, WEIGHT_FIELD_WIDTH); + if ui.available_width() >= row_width(ui, &[stroke_panel::PROFILE_DROPDOWN_WIDTH]) + reserve + && let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) + { + app.run("stroke.set", json!({"profile": id})).ok(); + } } - ui.add_space(4.0); - group_separator(ui); + group_end(ui); } /// The Control bar's Opacity group (its link and field, showing `opacity`) and, with something /// `selected`, its Graphic Style group (Style, Recolor Artwork and Global Edit Options), each while -/// the panel menu has it on, and the separator after them while a control follows. -fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: f32, selected: bool) { +/// the panel menu has it on, and the separator after them while a control follows. Short of room, +/// the bar leaves out Global Edit Options first, then Recolor Artwork, then the Style link with its +/// chip. The Opacity link with its field and the Style link with its chip each show only while the +/// bar has room for all of it and `reserve` after it ([`transform_reserve`]), so the narrower Style +/// part can show without the Opacity part. +fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: f32, selected: bool, reserve: f32) { let (opacity_on, style_on) = (group_on(app, "opacity"), group_on(app, "graphicStyle") && selected); + let mut drawn = false; if opacity_on { - // A click opens the Transparency panel; a drag scrubs the field (#4088). - if crate::scrub::link_label(ui, egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline()).clicked() { - app.ui.open_panel = Some("transparency".into()); - } - if let Some(o) = widgets::plain_field(ui, "cb-opacity", opacity as f64 * 100.0, "%", 0, 56.0) - && selected - { - app.run("transparency.set", json!({"opacity": o.clamp(0.0, 100.0)})).ok(); + let text = egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline(); + if ui.available_width() >= row_width(ui, &[widgets::text_width(ui, text.clone()), OPACITY_FIELD_WIDTH]) + reserve { + // A click opens the Transparency panel; a drag scrubs the field (#4088). + if crate::scrub::link_label(ui, text).clicked() { + app.ui.open_panel = Some("transparency".into()); + } + if let Some(o) = widgets::plain_field(ui, "cb-opacity", opacity as f64 * 100.0, "%", 0, OPACITY_FIELD_WIDTH) + && selected + { + app.run("transparency.set", json!({"opacity": o.clamp(0.0, 100.0)})).ok(); + } + drawn = true; } } - if style_on { + let text = egui::RichText::new(tl!("Style:")).size(12.0).color(t.text).underline(); + if style_on && ui.available_width() >= GROUP_GAP + row_width(ui, &[widgets::text_width(ui, text.clone()), CHIP_WIDTH]) + reserve { // Style picker: the selection's graphic style; its menu applies another. - ui.add_space(4.0); - if ui.link(egui::RichText::new(tl!("Style:")).size(12.0).color(t.text).underline()).clicked() { + ui.add_space(GROUP_GAP); + if ui.link(text).clicked() { app.ui.open_panel = Some("graphicStyles".into()); } - let resp = widgets::chip_button_named(ui, 22.0, true, tl!("Graphic Style"), |ui, r| crate::panels::graphic_styles::paint_linked(app, ui, r)); + let resp = + widgets::chip_button_named(ui, PAINT_CHIP, true, tl!("Graphic Style"), |ui, r| crate::panels::graphic_styles::paint_linked(app, ui, r)); egui::Popup::menu(&resp).show(|ui| crate::panels::graphic_styles::picker(app, ui)); // Recolor Artwork, when the selection has colours to recolour. let colored = app.session.active().is_some_and(|st| crate::canvas::has_colors(&st.doc, &st.selection.objects)); - if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 24.0).clicked() { + if colored + && ui.available_width() >= buttons_width(ui, 1) + reserve + && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, BUTTON).clicked() + { crate::menus::invoke(app, "ui.recolorDialog", json!({})); } - global_edit_options(app, ui); + global_edit_options(app, ui, reserve); + drawn = true; } - if opacity_on || style_on { + if drawn { group_separator(ui); } } @@ -709,11 +781,16 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, } gripper(app, ui, t, bar, dock_room); if app.session.active().is_none() { - ui.label(egui::RichText::new(tl!("No Document")).color(t.text_dim)); + let text = egui::RichText::new(tl!("No Document")).color(t.text_dim); + if ui.available_width() >= widgets::text_width(ui, text.clone()) { + ui.label(text); + } return; } // The Artboard tool's controls fill the narrowest window, so they keep it to themselves there. - if group_on(app, "snapping") && (app.session.tool_id() != "artboard" || ui.available_width() >= ARTBOARD_SNAPPING_WIDTH) { + // Any other tool's bar shows the menu while it has room for it. + let snapping_room = if app.session.tool_id() == "artboard" { ARTBOARD_SNAPPING_WIDTH } else { snapping_width(ui) }; + if group_on(app, "snapping") && ui.available_width() >= snapping_room { snapping_menu(app, ui); } let Some(st) = app.session.active() else { return }; @@ -739,7 +816,10 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, _ => tl!(crate::panels::appearance::object_label(app)), }; let has_first = first.is_some(); - ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text)); + let label = egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text); + if ui.available_width() >= widgets::text_width(ui, label.clone()) { + ui.label(label); + } // With the Artboard tool in use, the bar shows the active artboard's controls in place // of the object controls. let (artboard_options, transform) = (group_on(app, "artboardOptions"), group_on(app, "transform")); @@ -755,7 +835,9 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, if object_options { isolation_button(app, ui); } - ui.add_space(6.0); + if ui.available_width() >= 6.0 { + ui.add_space(6.0); + } // An image or an Image Trace object shows its own controls in place of Fill and Stroke, // and leaves Fill and Stroke out while the Image Options group is off too. let image = if group_on(app, "imageOptions") { @@ -771,19 +853,26 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, crate::panels::symbols::control_bar(app, ui); } let opacity = if has_first { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 }; + let reserve = transform_reserve(app, ui, !sel.is_empty()); if !image && group_on(app, "fillStroke") { - fill_and_stroke(app, ui); + fill_and_stroke(app, ui, reserve); } - opacity_and_style(app, ui, t, opacity, !sel.is_empty()); - if group_on(app, "character") { - crate::panels::character::control_bar(app, ui); - } - if !groups_before_transform(app, ui, sel.len()) { + opacity_and_style(app, ui, t, opacity, !sel.is_empty(), reserve); + // The Character group shows whole, with the Transform link's room left after it, or not at + // all; left out, it leaves out the groups between it and the link too. + let room = !group_on(app, "character") || crate::panels::character::control_bar(app, ui, reserve); + if !groups_before_transform(app, ui, sel.len(), room) { erase_trailing(ui); return; } // The Transform link opens the whole Transform panel (reference point, rotate, - // shear, options) in a popover; X/Y/W/H follow inline while the bar has room. + // shear, options) in a popover; X/Y/W/H follow inline while the bar has room. The + // groups from Fill and Stroke on keep its room, but the controls ahead of them can + // still take it. + if ui.available_width() < crate::panels::transform::link_width(ui) { + erase_trailing(ui); + return; + } crate::panels::transform::link(app, ui); // The bounding box, rotated with rotated objects: its centre and its own sides. if let Some(b) = app.selection_box() diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 052c7ec230..be37f10a29 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -3037,6 +3037,127 @@ fn no_separator_is_left_at_the_end_of_the_control_bar() { assert!(stray.is_empty(), "a separator with nothing after it: {stray:#?}"); } +/// The shapes of the Control bar's controls drawn in window `screen` that reach past the left edge +/// of its panel menu button, or past the room a floating bar clips its controls to, or `None` when +/// the bar leaves the button out. +fn control_bar_overflow(app: &mut crate::VectorcraftApp, ctx: &egui::Context, screen: egui::Rect) -> Option> { + control_bar_frame_in(app, ctx, screen, vec![]); + let out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(app, ui)); + let nodes: Vec<_> = out.platform_output.accesskit_update.map(|u| u.nodes.into_iter().map(|(_, n)| n).collect()).unwrap_or_default(); + let menu_left = nodes.iter().find(|n| n.label().is_some_and(|l| l == "Panel menu")).and_then(|n| n.bounds())?.x0 as f32; + // The bar's frame spans the whole bar, the panel menu button included. + let bar_width = if app.ui.control_bar_floating { screen.width().min(800.0) } else { screen.width() }; + let shapes = out.shapes.iter().filter_map(|cs| { + let r = cs.shape.visual_bounding_rect(); + let control = r.is_finite() && cs.clip_rect.intersects(r) && r.width() < bar_width - 4.0 && r.min.x < menu_left - 0.5; + let clipped = cs.clip_rect.right() < menu_left && r.right() > cs.clip_rect.right() + 0.5; + (control && (r.right() > menu_left + 0.5 || clipped)).then_some(r) + }); + Some(shapes.collect()) +} + +/// In a narrow window, the Control bar leaves out a control it has no room for, Fill and Stroke, +/// Opacity and Character included: every control it shows is drawn whole, left of the panel menu button, which +/// shows in every width, with the bar docked to the top or the bottom or floating. +#[test] +fn the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window() { + use crate::tests_removeanchors::{control_frame, has}; + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let mut faults = vec![]; + let mut check = |app: &mut crate::VectorcraftApp, case: &str| { + for (bottom, floating) in [(false, false), (true, false), (false, true)] { + (app.ui.control_bar_bottom, app.ui.control_bar_floating) = (bottom, floating); + for w in (150..=1500).step_by(9) { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w as f32, 300.0)); + match control_bar_overflow(app, &ctx, screen) { + None => faults.push(format!("{case}, bottom {bottom}, floating {floating}, {w} wide: no panel menu")), + Some(over) if !over.is_empty() => faults.push(format!("{case}, bottom {bottom}, floating {floating}, {w} wide: {over:?}")), + Some(_) => continue, + } + break; + } + } + (app.ui.control_bar_bottom, app.ui.control_bar_floating) = (false, false); + }; + app.run("select.set", json!({ "ids": [id] })).unwrap(); + let bar = control_frame(&mut app, &ctx, vec![]); + assert!(has(&bar, "Stroke:") && has(&bar, "Opacity:"), "Fill and Stroke and Opacity show in a wide window: {bar:?}"); + check(&mut app, "a rectangle selected"); + app.select_tool("directSelection"); + app.run("select.anchors", json!({"id": id, "anchors": [[0, 0]]})).unwrap(); + check(&mut app, "an anchor selected"); + app.select_tool("selection"); + let text = app.run("text.create", json!({"x": 20, "y": 120, "text": "Point"})).unwrap()["id"].clone(); + app.run("select.set", json!({ "ids": [text] })).unwrap(); + let bar = control_frame(&mut app, &ctx, vec![]); + assert!(has(&bar, "Character:"), "the Character group shows in a wide window: {bar:?}"); + check(&mut app, "type selected"); + app.run("select.set", json!({ "ids": [] })).unwrap(); + check(&mut app, "nothing selected"); + assert!(faults.is_empty(), "a control drawn past the bar's room: {faults:#?}"); +} + +/// With Opacity and Graphic Style on, a bar too narrow for the Opacity link and its field still +/// shows the narrower Style link and its chip when it has room for them. +#[test] +fn a_narrow_control_bar_shows_the_style_link_without_the_opacity_field() { + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone(); + app.run("select.set", json!({ "ids": [id] })).unwrap(); + app.run("window.control.options", json!({"group": "fillStroke", "visible": false})).unwrap(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let shows = |nodes: &[egui::accesskit::Node], name: &str| nodes.iter().any(|n| n.label().is_some_and(|l| l == name)); + let (mut both, mut style_only) = (false, false); + for w in (150..=900).step_by(2) { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w as f32, 300.0)); + control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let nodes = control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let (opacity, style) = (shows(&nodes, "Opacity:"), shows(&nodes, "Style:")); + both |= opacity && style; + style_only |= style && !opacity; + } + assert!(both, "a wide bar shows the Opacity and Style links"); + assert!(style_only, "some width has room for the Style link and its chip but not the Opacity link and its field"); +} + +/// With nothing selected, a bar too narrow for the Document Setup button still shows the narrower +/// Preferences button when it has room for it. +#[test] +fn a_narrow_control_bar_shows_preferences_without_document_setup() { + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + app.run("select.set", json!({ "ids": [] })).unwrap(); + for group in ["fillStroke", "opacity", "graphicStyle"] { + app.run("window.control.options", json!({"group": group, "visible": false})).unwrap(); + } + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let shows = |nodes: &[egui::accesskit::Node], name: &str| nodes.iter().any(|n| n.label().is_some_and(|l| l == name)); + let (mut both, mut preferences_only) = (false, false); + for w in (150..=900).step_by(2) { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w as f32, 300.0)); + control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let nodes = control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let (setup, preferences) = (shows(&nodes, "Document Setup"), shows(&nodes, "Preferences")); + both |= setup && preferences; + preferences_only |= preferences && !setup; + } + assert!(both, "a wide bar shows Document Setup and Preferences"); + assert!(preferences_only, "some width has room for Preferences but not Document Setup"); +} + /// The Anchor Points group holds Convert and Anchors for direct-selected anchors; off, the bar keeps /// its Anchor Point label. #[test] diff --git a/crates/ui-egui/src/document_drag.rs b/crates/ui-egui/src/document_drag.rs index 2784f29eef..b44d09b5c3 100644 --- a/crates/ui-egui/src/document_drag.rs +++ b/crates/ui-egui/src/document_drag.rs @@ -1,8 +1,10 @@ //! A selection dragged across document tabs remains editable and leaves its source intact. +use std::sync::Arc; + use egui::{Context, Id, Response}; use serde_json::json; -use vectorcraft_doc::NodeId; +use vectorcraft_doc::{Document, NodeId}; use vectorcraft_geom::Point; use crate::{VectorcraftApp, canvas, widgets}; @@ -19,7 +21,11 @@ struct Origin { struct Artwork { origin: Origin, ids: Vec, - revision: u64, + /// The source document when the drag left it. The drag ends when the source's content differs + /// from it ([`vectorcraft_engine::DocState::same_content`]). A redraw that raises its revision + /// (another document's assigned color profiles brought to the front) and a change that is not + /// an edit (its type measured again) keep it. + source: Arc, target: u64, } @@ -75,7 +81,7 @@ pub(crate) fn cleanup(app: &VectorcraftApp, ctx: &Context) { } }); if let Some(art) = egui::DragAndDrop::payload::(ctx) { - let valid = app.session.documents().iter().any(|d| d.uid == art.origin.uid && d.revision == art.revision) + let valid = app.session.documents().iter().any(|d| d.uid == art.origin.uid && d.same_content(&art.source)) && app.session.active().is_some_and(|d| d.uid == art.target); let cancel = ctx.input(|i| { !i.focused @@ -143,13 +149,14 @@ pub(crate) fn hover_tab(app: &mut VectorcraftApp, resp: &Response, win: crate::d return true; } // Alt-drag previews use temporary duplicate ids; only the restored originals travel. - let ids = app.session.active().map(|d| d.selection.objects.clone()).unwrap_or_default(); + let Some((ids, source)) = app.session.active().map(|d| (d.selection.objects.clone(), d.doc.clone())) else { + return true; + }; if ids.is_empty() { return true; } canvas::end_document_drag(ctx); - let revision = app.session.active().map(|d| d.revision).unwrap_or_default(); - egui::DragAndDrop::set_payload(ctx, Artwork { origin, ids, revision, target: uid }); + egui::DragAndDrop::set_payload(ctx, Artwork { origin, ids, source, target: uid }); } if transfer && let Some(art) = egui::DragAndDrop::payload::(ctx) { let mut art = (*art).clone(); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 9d15012784..cad5b53d8e 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -97,7 +97,7 @@ fn plural_polish(n: u64) -> usize { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 28] = [ +pub static LANGUAGES: [LangInfo; 29] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() }, // Hindi shares the integer zero/one rule with French. Desktop UI fonts are loaded lazily // from installed faces by ui_fonts; the web needs a Devanagari font in its font set. @@ -109,6 +109,18 @@ pub static LANGUAGES: [LangInfo; 28] = [ complete_menus: true, catalog: OnceLock::new(), }, + // Gujarati: menus and dialogs, with partial status/error message coverage and loanwords + // transliterated into Gujarati script; zero/one singular plurals. Gujarati text is drawn with an installed system font + // (Gujarati Sangam MN, Nirmala UI, Shruti) or a craft-fonts face tagged `Gujr`. Web selection + // requires a bundled Gujarati interface face; otherwise `gu-*` locales use the fallback. + LangInfo { + code: "gu", + name: "ગુજરાતી", + source: include_str!("i18n/gu.tsv"), + plural: plural_french, + complete_menus: true, + catalog: OnceLock::new(), + }, // Greek: the whole interface and the status and error messages, keeping the same names in // English as Spanish; every `el-*` locale (including `el-GR` and `el-CY`) resolves here. LangInfo { @@ -355,6 +367,16 @@ pub static LANGUAGES: [LangInfo; 28] = [ ]; impl LangInfo { + fn available(&self) -> bool { + self.available_with_fonts(cfg!(target_arch = "wasm32"), vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.scripts.contains(&"Gujr"))) + } + + fn available_with_fonts(&self, web: bool, gujarati_font: bool) -> bool { + // Browsers cannot use the native installed-font fallback. Do not offer a language + // whose text would be missing-glyph boxes, including when restoring a preference. + self.code != "gu" || !web || gujarati_font + } + fn catalog(&self) -> &Catalog { self.catalog.get_or_init(|| Catalog::parse(self.source)) } @@ -387,7 +409,7 @@ impl Lang { /// A language by its exact code. pub fn from_code(code: &str) -> Option { - LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(code)).map(Lang) + LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(code) && l.available()).map(Lang) } /// Resolve the `interfaceLanguage` preference: a language code, or `auto` (and anything @@ -396,12 +418,12 @@ impl Lang { Lang::from_code(pref).unwrap_or_else(system_lang) } - /// Every registered language. + /// Every language available in this build. pub fn all() -> impl Iterator { - LANGUAGES.iter().map(Lang) + LANGUAGES.iter().filter(|l| l.available()).map(Lang) } - /// Every registered language in the order a picker lists them: English first (the source + /// Every available language in the order a picker lists them: English first (the source /// language and the fallback), then the rest by their own name with accents folded, so /// Čeština sits under C rather than after Z. Names in other scripts follow the Latin ones in /// code point order: Greek, Cyrillic, Arabic, then the CJK languages. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 6b03151a00..8cb531e411 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -691,6 +691,8 @@ font-technology Variable Променливи Crop Image Изрязване на изображение Image Tools Инструменти за изображения Remove Background… Премахване на фона… + Draw (Pencil) Рисуване (Молив) + Place Image Вмъкване на изображение Remove Background Премахване на фона Background Фон Edge Color Цвят на ръба diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 948956add6..85db98bcb7 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -201,6 +201,8 @@ Crop Image Izreži sliku Image Tools Alati za slike Remove Background… Ukloni pozadinu… + Draw (Pencil) Crtaj (olovka) + Place Image Postavi sliku Remove Background Ukloni pozadinu Background Pozadina Edge Color Boja ruba diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 2bd7ec113e..ae31e4730c 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1006,6 +1006,8 @@ antiAlias Sharp Nítid Crop Image Retalla la imatge Image Tools Eines d’imatge Remove Background… Elimina el fons… + Draw (Pencil) Dibuixa (llapis) + Place Image Col·loca una imatge Remove Background Elimina el fons Background Fons Edge Color Color de la vora @@ -5293,6 +5295,7 @@ digits Malayalam Malaiàlam @msg `{key}` must be a #rrggbb colour `{key}` ha de ser un color #rrggbb @msg `{key}` must be a number `{key}` ha de ser un nombre @msg `{key}` must be a number not below 0 `{key}` ha de ser un nombre no inferior a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` han de ser quatre nombres no inferiors a 0: [tl, tr, br, bl] @msg `{key}` must be a number, not {v} `{key}` ha de ser un nombre, no {v} @msg `{key}` must be a string `{key}` ha de ser una cadena @msg `{key}` must be a profile name or null `{key}` ha de ser un nom de perfil o null diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index f4da9c9dfe..5f5174dd5c 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -207,6 +207,8 @@ Crop Image Oříznout obraz Image Tools Nástroje obrazu Remove Background… Odstranit pozadí… + Draw (Pencil) Kreslit (tužka) + Place Image Umístit obraz Remove Background Odstranit pozadí Background Pozadí Edge Color Barva okraje @@ -4800,6 +4802,7 @@ imageTrace Outline Obrys @msg `select` must be true or false `select` musí být true nebo false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` a `maxAnchors` musí být celá čísla @msg `{key}` must be a number not below 0 `{key}` musí být číslo nejméně 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` musí být čtyři čísla nejméně 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musí být [x, y, width, height]: čtyři konečná čísla, šířka a výška nesmí být záporné @msg `{min_key}` is greater than `{max_key}` `{min_key}` je větší než `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` zadejte alespoň jeden z parametrů `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -7578,7 +7581,7 @@ breakText Glyphs Glyfy @msg `alignOn` must be a string `alignOn` musí být řetězec @msg `align` is start, center or end `align` je start, center nebo end @msg `align` must be a string `align` musí být řetězec -@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` musí být pole dvojic [dílčí cesta, kotevní bod] +@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` musí být pole dvojic [subpath, anchor] @msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` musí uvádět indexy kotevních bodů (0, 1, 2…), ne {k} @msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` musí uvádět indexy kotevních bodů (0, 1, 2…), ne {v} @msg `answer` for this {kind} dialog must be one of: {_1} `answer` pro tento dialog {kind} musí být jedna z hodnot: {_1} @@ -7643,7 +7646,7 @@ breakText Glyphs Glyfy @msg `fidelity` must be from {lo} to {hi} pt `fidelity` musí být od {lo} do {hi} pt @msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` musí být 1 až 1000 cest k souborům písem, které našlo poslední hledání text.findFontFiles @msg `focal` and `radius` cannot be used together `focal` a `radius` nelze použít společně -@msg `focal` must be an array of [subpath, anchor] pairs `focal` musí být pole dvojic [dílčí cesta, kotevní bod] +@msg `focal` must be an array of [subpath, anchor] pairs `focal` musí být pole dvojic [subpath, anchor] @msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` uvádí [{si}, {ai}], což není kotevní bod cesty @msg `folder` must be an absolute folder path `folder` musí být absolutní cesta ke složce @msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` musí být objekt se značkami os a hodnotami, například {"wght"} @@ -8308,7 +8311,7 @@ breakText Glyphs Glyfy @msg instruction {pc} reached with stack depths {seen} and {d} instrukce {pc} byla dosažena s hloubkami zásobníku {seen} a {d} @msg invalid `color` neplatné `color` @msg invalid anchor index {v}: expected a non-negative integer neplatný index kotevního bodu {v}: očekáváno nezáporné celé číslo -@msg invalid anchor reference {v}: expected [subpath, anchor] neplatný odkaz na kotevní bod {v}: očekáváno [dílčí cesta, kotevní bod] +@msg invalid anchor reference {v}: expected [subpath, anchor] neplatný odkaz na kotevní bod {v}: očekáváno [subpath, anchor] @msg invalid angle neplatný úhel @msg invalid array length neplatná délka pole @msg invalid brush: {e} neplatný štětec: {e} @@ -8417,7 +8420,7 @@ breakText Glyphs Glyfy @msg missing `{k}` chybí `{k}` @msg missing anchor chybí kotevní bod @msg missing anchors chybí kotevní body -@msg missing anchors [[subpath, anchor]…] chybí kotevní body [[dílčí cesta, kotevní bod]…] +@msg missing anchors [[subpath, anchor]…] chybí kotevní body [[subpath, anchor]…] @msg missing array `{key}` chybí pole `{key}` @msg missing case chybí case @msg missing color chybí barva diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 96cbab2fb2..f320896f62 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -194,6 +194,8 @@ Crop Image Beskær billede Image Tools Billedværktøjer Remove Background… Fjern baggrund… + Draw (Pencil) Tegn (blyant) + Place Image Placér billede Remove Background Fjern baggrund Background Baggrund Edge Color Kantfarve diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 87238d47df..2ab7e08cb9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -684,6 +684,8 @@ font-technology Variable Variabel Crop Image Bild beschneiden Image Tools Bildwerkzeuge Remove Background… Hintergrund entfernen… + Draw (Pencil) Zeichnen (Buntstift) + Place Image Bild platzieren Remove Background Hintergrund entfernen Background Hintergrund Edge Color Randfarbe @@ -4413,6 +4415,7 @@ theme Appearance Erscheinungsbild @msg `select` must be true or false `select` muss true oder false sein @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` und `maxAnchors` müssen ganze Zahlen sein @msg `{key}` must be a number not below 0 `{key}` muss eine Zahl nicht kleiner als 0 sein +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` muss aus vier Zahlen nicht kleiner als 0 bestehen: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` muss [x, y, width, height] sein: vier endliche Zahlen, Breite und Höhe nicht negativ @msg `{min_key}` is greater than `{max_key}` `{min_key}` ist größer als `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` geben Sie mindestens einen der Parameter `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` an diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index d07fc114f9..65ae36408d 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -665,6 +665,8 @@ font-technology Variable Μεταβλητές Crop Image Περικοπή εικόνας Image Tools Εργαλεία εικόνας Remove Background… Αφαίρεση φόντου… + Draw (Pencil) Σχεδίαση (μολύβι) + Place Image Τοποθέτηση εικόνας Remove Background Αφαίρεση φόντου Background Φόντο Edge Color Χρώμα άκρης @@ -2559,6 +2561,7 @@ axis Both Και τα δύο @msg `select` must be true or false Το `select` πρέπει να είναι true ή false @msg `minAnchors` and `maxAnchors` must be whole numbers Τα `minAnchors` και `maxAnchors` πρέπει να είναι ακέραιοι αριθμοί @msg `{key}` must be a number not below 0 Το `{key}` πρέπει να είναι αριθμός όχι μικρότερος από 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] Το `{key}` πρέπει να είναι τέσσερις αριθμοί όχι μικρότεροι από 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative Το `{key}` πρέπει να είναι [x, y, width, height]: τέσσερις πεπερασμένοι αριθμοί, με πλάτος και ύψος όχι αρνητικά @msg `{min_key}` is greater than `{max_key}` Το `{min_key}` είναι μεγαλύτερο από το `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` δώστε τουλάχιστον ένα από τα `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8812b6a8fb..acc7bcb18b 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -705,6 +705,8 @@ font-technology Variable Variables Crop Image Recortar imagen Image Tools Herramientas de imagen Remove Background… Quitar fondo… + Draw (Pencil) Dibujar (Lápiz) + Place Image Colocar imagen Remove Background Quitar fondo Background Fondo Edge Color Color del borde @@ -4490,6 +4492,7 @@ axis Both Ambos @msg `select` must be true or false `select` debe ser true o false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` y `maxAnchors` deben ser números enteros @msg `{key}` must be a number not below 0 `{key}` debe ser un número no inferior a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` debe ser cuatro números no inferiores a 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` debe ser [x, y, width, height]: cuatro números finitos, con anchura y altura no negativas @msg `{min_key}` is greater than `{max_key}` `{min_key}` es mayor que `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica al menos uno de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 555669b4b4..558fbbc268 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -214,6 +214,8 @@ Crop Image Rajaa kuva Image Tools Kuvatyökalut Remove Background… Poista tausta… + Draw (Pencil) Piirrä (lyijykynä) + Place Image Sijoita kuva Remove Background Poista tausta Background Tausta Edge Color Reunan väri @@ -4714,6 +4716,7 @@ imageTrace Outline Ääriviiva @msg `select` must be true or false `select` on oltava true tai false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ja `maxAnchors` on oltava kokonaislukuja @msg `{key}` must be a number not below 0 `{key}` on oltava luku, joka on vähintään 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` on oltava neljä lukua, jotka ovat vähintään 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` on oltava [x, y, width, height]: neljä äärellistä lukua, joista leveys ja korkeus eivät ole negatiivisia @msg `{min_key}` is greater than `{max_key}` `{min_key}` on suurempi kuin `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` anna vähintään yksi parametreista `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 2295293e6b..df0ba85bc8 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -707,6 +707,8 @@ font-technology Variable Variables Crop Image Recadrer l’image Image Tools Outils d’image Remove Background… Supprimer l’arrière-plan… + Draw (Pencil) Dessiner (Crayon) + Place Image Importer une image Remove Background Supprimer l’arrière-plan Background Arrière-plan Edge Color Couleur du bord @@ -4479,6 +4481,7 @@ axis Both Les deux @msg `select` must be true or false `select` doit valoir true ou false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` et `maxAnchors` doivent être des nombres entiers @msg `{key}` must be a number not below 0 `{key}` doit être un nombre supérieur ou égal à 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` doit être quatre nombres supérieurs ou égaux à 0 : [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` doit être [x, y, width, height] : quatre nombres finis, largeur et hauteur non négatives @msg `{min_key}` is greater than `{max_key}` `{min_key}` est supérieur à `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indiquez au moins l’un des paramètres `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -7485,6 +7488,1810 @@ breakText Glyphs Glyphes Margin Marge Box Cadre Swatch Nuance +@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions 'caller', 'callee' et 'arguments' ne peuvent pas être utilisés sur les fonctions en mode strict +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. Un profil accepte un seul effet Révolution. Modifiez plutôt les options de l’effet existant. +@msg Add Points at Crossings: a path went missing Ajouter des points aux intersections : un tracé a disparu +@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean.prototype.{name} a été appelé sur une valeur qui n’est pas un booléen +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB Les images CMJN dont les échantillons n’ont pas pu être lus en CMJN ont été importées en RVB +@msg CSS options: {e} Options CSS : {e} +@msg CSV has a header but no data rows Le fichier CSV a un en-tête mais aucune ligne de données +@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark Le fichier CSV contient des octets NUL : enregistrez-le en UTF-8, ou en UTF-16 avec une marque d’ordre des octets +@msg CSV is empty Le fichier CSV est vide +@msg CSV is larger than {MAX_CSV_BYTES} bytes Le fichier CSV dépasse {MAX_CSV_BYTES} octets +@msg CSV is not UTF-16 Le fichier CSV n’est pas en UTF-16 +@msg CSV is not UTF-8 Le fichier CSV n’est pas en UTF-8 +@msg CSV would be larger than {MAX_CSV_BYTES} bytes Le fichier CSV dépasserait {MAX_CSV_BYTES} octets +@msg Cannot write to the location of “{name}”; choose where to save it Impossible d’écrire à l’emplacement de « {name} » ; choisissez où l’enregistrer +@msg Choose original artwork, not a dimension's generated paths Choisissez l’illustration d’origine, pas les tracés générés d’une cote +@msg Contour: select paths or shapes Contour : sélectionnez des tracés ou des formes +@msg Contour: the Offset Path effect is not available Contour : l’effet Décalage n’est pas disponible +@msg Create needs fills, strokes or both La création nécessite des fonds, des contours ou les deux +@msg DCS files can only be placed linked: use File › Place with Link on Les fichiers DCS ne peuvent être importés que liés : utilisez Fichier › Importer avec l’option Lien activée +@msg DXF options: {e} Options DXF : {e} +@msg DXF scale must be a positive number up to {_1}, not {scale} L’échelle DXF doit être un nombre positif d’au plus {_1}, pas {scale} +@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}` : {allowed} +@msg Date.prototype method called on a value that is not a Date object Méthode de Date.prototype appelée sur une valeur qui n’est pas un objet Date +@msg Dimension is locked or hidden La cote est verrouillée ou masquée +@msg Dimension mode does not match its source Le mode de la cote ne correspond pas à sa source +@msg Draw Inside needs exactly one selected path, compound path or text Dessin intérieur nécessite exactement un tracé, un tracé transparent ou un texte sélectionné +@msg EPS Save writes all artwork: use document.export for selected-only EPS L’enregistrement EPS écrit toute l’illustration : utilisez document.export pour un EPS de la sélection seule +@msg EPS flattener: {e} Aplatissement EPS : {e} +@msg EPS options: {e} Options EPS : {e} +@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}` : {allowed} +@msg Error.prototype.toString was called on a value that is not an object Error.prototype.toString a été appelé sur une valeur qui n’est pas un objet +@msg Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft Exporter pour les écrans écrit png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai ou vectorcraft +@msg Flare click coordinates must be finite numbers Les coordonnées du clic de l’halo doivent être des nombres finis +@msg Function.prototype.toString was called on a value that is not a function Function.prototype.toString a été appelé sur une valeur qui n’est pas une fonction +@msg HTML Text applies to object slices of type only Texte HTML s’applique uniquement aux tranches d’objet de texte +@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) Identifier les objets par est réglé sur ID XML : le nom doit être un ID XML valide (commençant par une lettre, un trait de soulignement ou deux-points ; sans espaces) +@msg Image Trace failed: internal error: its thread stopped (please report this bug) Échec de la vectorisation de l’image : erreur interne : son thread s’est arrêté (merci de signaler ce bogue) +@msg Image Trace failed: {e} Échec de la vectorisation de l’image : {e} +@msg Image Trace finished, but its document was closed La vectorisation de l’image est terminée, mais son document a été fermé +@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again La vectorisation de l’image est terminée, mais l’image ou ses réglages ont changé entre-temps : vectorisez-la à nouveau +@msg Import SVG Filter reads SVG files, not SVGZ Importer un filtre SVG lit les fichiers SVG, pas SVGZ +@msg Invalid dimension offset Décalage de cote non valide +@msg Invalid point Point non valide +@msg JSON.parse: {why} (at offset {at}) JSON.parse : {why} (à la position {at}) +@msg JSON.stringify cannot write a structure that contains itself JSON.stringify ne peut pas écrire une structure qui se contient elle-même +@msg Join: a subpath went missing Joindre : un sous-tracé a disparu +@msg Lock Workspace is not available in VectorCraft Verrouiller l’espace de travail n’est pas disponible dans VectorCraft +@msg Move Panel to New Window is not available: floating panels stay in the main window Déplacer le panneau dans une nouvelle fenêtre n’est pas disponible : les panneaux flottants restent dans la fenêtre principale +@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString nécessite un Namespace +@msg New Document profiles need the preferences folder, which this run doesn't have Les profils de nouveau document nécessitent le dossier des préférences, dont cette exécution ne dispose pas +@msg Number.prototype.{name} was called on a value that is not a number Number.prototype.{name} a été appelé sur une valeur qui n’est pas un nombre +@msg Object.create's prototype must be an object or null Le prototype d’Object.create doit être un objet ou null +@msg Object.{function} was given a value that is not an object Object.{function} a reçu une valeur qui n’est pas un objet +@msg Offset Path: the path is too complex to offset Décalage : le tracé est trop complexe pour être décalé +@msg Offset Path: {e} Décalage : {e} +@msg PDF content is incomplete or exceeds the reader's limits; no document was imported Le contenu PDF est incomplet ou dépasse les limites du lecteur ; aucun document n’a été importé +@msg PDF flattener: {e} Aplatissement PDF : {e} +@msg PDF options: {e} Options PDF : {e} +@msg QName.prototype.toString needs a QName QName.prototype.toString nécessite un QName +@msg Recolor Artwork is no longer open Redéfinir les couleurs de l’illustration n’est plus ouvert +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp.prototype.{name} a été appelé sur une valeur qui n’est pas un objet RegExp +@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 Le SVG n’est pas en UTF-8, UTF-16 ou ISO 8859-1 +@msg Save writes one artboard: name one, or export several with document.export L’enregistrement écrit un seul plan de travail : indiquez-en un, ou exportez-en plusieurs avec document.export +@msg Select a dimension first Sélectionnez d’abord une cote +@msg Select dimensions to update Sélectionnez les cotes à mettre à jour +@msg Select only dimensions Sélectionnez uniquement des cotes +@msg Select two or more objects, or a group Sélectionnez au moins deux objets, ou un groupe +@msg Shaper needs 1–64 source paths Shaper nécessite de 1 à 64 tracés source +@msg Stencil Bridges: the selected art is too large or too detailed to bridge Ponts de pochoir : l’illustration sélectionnée est trop grande ou trop détaillée pour être reliée +@msg String.prototype.toString was called on a value that is not a string String.prototype.toString a été appelé sur une valeur qui n’est pas une chaîne +@msg Switch x/y is for scatter graphs Permuter x/y s’applique aux graphes de dispersion +@msg Text options: {e} Options du texte : {e} +@msg The {name} profile could not be read, so the new document has the default settings: {reason} Le profil {name} n’a pas pu être lu, le nouveau document a donc les réglages par défaut : {reason} +@msg Trap: more than {_1} pairs of objects lie close enough to trap, or too many objects share a horizontal range; trap fewer objects at a time Recouvrement : plus de {_1} paires d’objets sont assez proches pour un recouvrement, ou trop d’objets partagent une même plage horizontale ; traitez moins d’objets à la fois +@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off Cadre et lignes d’échenillage : aucune ligne d’échenillage ne tient hors de l’illustration, et le cadre d’échenillage est désactivé +@msg \ at end of pattern \ à la fin du motif +@msg `alignOn` cannot be empty `alignOn` ne peut pas être vide +@msg `alignOn` must be a string `alignOn` doit être une chaîne +@msg `align` is start, center or end `align` vaut start, center ou end +@msg `align` must be a string `align` doit être une chaîne +@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` doit être une liste de paires [sous-tracé, point d’ancrage] +@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` doit lister des indices de points d’ancrage (0, 1, 2…), pas {k} +@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` doit lister des indices de points d’ancrage (0, 1, 2…), pas {v} +@msg `answer` for this {kind} dialog must be one of: {_1} `answer` pour cette boîte de dialogue {kind} doit valoir l’une de ces valeurs : {_1} +@msg `answers` has more than {MAX_ANSWERS} answers `answers` contient plus de {MAX_ANSWERS} réponses +@msg `answers` is larger than 1 MiB `answers` dépasse 1 Mio +@msg `answers` must be a list of answers `answers` doit être une liste de réponses +@msg `artboard` must be the index of an artboard of the document `artboard` doit être l’indice d’un plan de travail du document +@msg `artboardsAt` must be [x, y] `artboardsAt` doit être [x, y] +@msg `artboards` must be a list of artboard indices `artboards` doit être une liste d’indices de plans de travail +@msg `artboards` names an artboard the document does not have `artboards` désigne un plan de travail que le document ne contient pas +@msg `artboards` names an artboard twice `artboards` désigne deux fois le même plan de travail +@msg `asset` is the asset id (see assets.list) `asset` est l’identifiant de la ressource (voir assets.list) +@msg `at` must be [x, y] (got {at}) `at` doit être [x, y] (reçu : {at}) +@msg `at` must be a byte offset `at` doit être une position en octets +@msg `autoLeading` must be a number `autoLeading` doit être un nombre +@msg `background` must be "", "matte" or #rrggbb `background` doit valoir "", "matte" ou #rrggbb +@msg `beside` needs `side` `beside` nécessite `side` +@msg `burasagari` must be "none", "standard" or "forced" `burasagari` doit valoir "none", "standard" ou "forced" +@msg `center` must be [x, y] `center` doit être [x, y] +@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign` doit valoir "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" ou "icfBottom" +@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection` doit valoir "default", "leftToRight" ou "rightToLeft" +@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke` nécessite `stroke: false`, `ids: []` et aucun `stop` +@msg `clear` must be a list of type object ids, not {v} `clear` doit être une liste d’identifiants d’objets texte, pas {v} +@msg `clear` must list type objects; {_1} is not one `clear` doit lister des objets texte ; {_1} n’en est pas un +@msg `clear` must not list `id` `clear` ne doit pas lister `id` +@msg `code` must be a whole number `code` doit être un nombre entier +@msg `colorMode` must be one of rgb, cmyk `colorMode` doit valoir rgb ou cmyk +@msg `color` needs a `rule` to make the theme from `color` nécessite une règle `rule` à partir de laquelle créer le thème +@msg `colors` does not apply to the {name} reduction `colors` ne s’applique pas à la réduction {name} +@msg `colors` is empty `colors` est vide +@msg `colors` must be a whole number from 2 to 256 `colors` doit être un nombre entier compris entre 2 et 256 +@msg `colors` must be at least 9 for the uniform reduction `colors` doit valoir au moins 9 pour la réduction uniforme +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors` doit être compris entre 1 et {MAX_THEME_COLORS} (reçu : {n}) +@msg `columnType` is vertical, uniform, repeating or sliding `columnType` vaut vertical, uniform, repeating ou sliding +@msg `columns` must be a whole number 1..50 `columns` doit être un nombre entier de 1 à 50 +@msg `commands` must not be empty `commands` ne doit pas être vide +@msg `contents` goes with `to` `contents` s’utilise avec `to` +@msg `corners` must be sharp, round or square, not `{other}` `corners` doit valoir sharp, round ou square, pas `{other}` +@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners` doit lister des indices de points d’ancrage (0, 1, 2…), pas {v} +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count` doit être un nombre entier compris entre 0 et {MAX_COUNT} +@msg `count` must be a whole number from 1 to 100 `count` doit être un nombre entier compris entre 1 et 100 +@msg `dataBase64` is larger than the module limit `dataBase64` dépasse la limite des modules +@msg `dataBase64` is not valid base64 `dataBase64` n’est pas du base64 valide +@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly` ne peut pas être combiné avec `ids`, `reverse` ou `append` +@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth` et `childLimit` découpent le résumé : passez `summary: true` +@msg `design` is a design name, or null for plain columns `design` est un nom de motif, ou null pour des colonnes simples +@msg `design` is a design name, or null for the default marker `design` est un nom de motif, ou null pour le repère par défaut +@msg `destinationFolder` is required when destination is `folder` `destinationFolder` est obligatoire quand la destination est `folder` +@msg `destination` must be `none`, `saveAndClose` or `folder` `destination` doit valoir `none`, `saveAndClose` ou `folder` +@msg `diameter` must be from 0.1 to 2500 pt `diameter` doit être compris entre 0.1 et 2500 pt +@msg `digits` must be one of {_1} `digits` doit valoir l’une de ces valeurs : {_1} +@msg `dimImages` is a percentage or false `dimImages` est un pourcentage ou false +@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction` doit valoir "auto", "leftToRight" ou "rightToLeft" +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount` doit être un nombre compris entre 0 et 100 +@msg `each` is the value one design stands for, more than 0 `each` est la valeur que représente un motif, supérieure à 0 +@msg `end` must be "start" or "end" `end` doit valoir "start" ou "end" +@msg `ends` applies to joining exactly two paths `ends` s’applique à la jonction d’exactement deux tracés +@msg `event` must be one of {_1} `event` doit valoir l’une de ces valeurs : {_1} +@msg `every` must be "all", "odd" or "even", not {v} `every` doit valoir "all", "odd" ou "even", pas {v} +@msg `expand` must be true or false `expand` doit valoir true ou false +@msg `fidelity` must be from {lo} to {hi} pt `fidelity` doit être compris entre {lo} et {hi} pt +@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` doit contenir de 1 à 1000 chemins de fichiers de police trouvés par la dernière recherche text.findFontFiles +@msg `focal` and `radius` cannot be used together `focal` et `radius` ne peuvent pas être utilisés ensemble +@msg `focal` must be an array of [subpath, anchor] pairs `focal` doit être une liste de paires [sous-tracé, point d’ancrage] +@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` désigne [{si}, {ai}], qui n’est pas un point d’ancrage du tracé +@msg `folder` must be an absolute folder path `folder` doit être le chemin absolu d’un dossier +@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` doit être un objet de balises d’axe et de valeurs, comme {"wght"} +@msg `format` must be a string `format` doit être une chaîne +@msg `format` must be one of {_1} `format` doit valoir l’une de ces valeurs : {_1} +@msg `fractions` is chop or scale `fractions` vaut chop ou scale +@msg `from` and `to` must differ `from` et `to` doivent être différents +@msg `from` has an empty frame `from` a un cadre vide +@msg `from` must be a non-negative integer `from` doit être un entier positif ou nul +@msg `from` must be area type or type on a path `from` doit être un texte captif ou un texte curviligne +@msg `gid` must be a glyph id, not {v} `gid` doit être un identifiant de glyphe, pas {v} +@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate` concerne un seul caractère, pas un style de caractère (text.setGlyphAlternate le définit) +@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate` n’accepte que null, qui retire un choix de glyphe (text.setGlyphAlternate en crée un) +@msg `group` must be one of {_1} `group` doit valoir l’une de ces valeurs : {_1} +@msg `group` must be the Objects on Path group of `member` and `target` `group` doit être le groupe Objets sur tracé de `member` et `target` +@msg `guide` must be a guide index (guide.list) `guide` doit être un indice de repère (guide.list) +@msg `hAlign` must be default, left, center or right `hAlign` doit valoir default, left, center ou right +@msg `id` is an object id, not {v} `id` est un identifiant d’objet, pas {v} +@msg `id` must be a node id `id` doit être un identifiant de nœud +@msg `id` must be an object id, not {v} `id` doit être un identifiant d’objet, pas {v} +@msg `ids` must name Objects on Path groups or objects in them `ids` doit désigner des groupes Objets sur tracé ou des objets qu’ils contiennent +@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids` accepte au plus {MAX_INSPECT_IDS} identifiants +@msg `imageMap` must be none, rectangle or polygon `imageMap` doit valoir none, rectangle ou polygon +@msg `includeArtboardFill` must be true or false `includeArtboardFill` doit valoir true ou false +@msg `includeSubdirs` must be true or false `includeSubdirs` doit valoir true ou false +@msg `index` must be a point index `index` doit être un indice de point +@msg `index` must be a whole number or null `index` doit être un nombre entier ou null +@msg `indices` must be a non-empty array of item indices `indices` doit être une liste non vide d’indices d’éléments +@msg `item` needs `index` `item` nécessite `index` +@msg `items` is a list of {kind, item} `items` est une liste de {kind, item} +@msg `items` is empty `items` est vide +@msg `items` must be a list of {id, points} `items` doit être une liste de {id, points} +@msg `items` must be a list of {{id, anchors?}}, not {v} `items` doit être une liste de {{id, anchors?}}, pas {v} +@msg `items` must be an array of {id, anchors} `items` doit être une liste de {id, anchors} +@msg `items` must be an array of {{id, corners?}}, not {v} `items` doit être une liste de {{id, corners?}}, pas {v} +@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida` doit valoir "none", "short", "medium", "long" ou "stylistic" +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore` concerne le premier caractère d’une ligne, pas un style de caractère (text.setRangeStyle le définit) +@msg `kind` is one of {_1} `kind` vaut l’une de ces valeurs : {_1} +@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind` doit valoir "round", "invertedRound" ou "chamfer", pas {v} +@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind` doit valoir `text`, `visibility`, `image` ou `graph` +@msg `kind` must be a string `kind` doit être une chaîne +@msg `kind` must be image, noImage or htmlText `kind` doit valoir image, noImage ou htmlText +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType` doit valoir "pushInFirst", "pushOutFirst" ou "pushOutOnly" +@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku` doit valoir "none", "hard" ou "soft" +@msg `label` must be a non-empty name of at most 200 characters `label` doit être un nom non vide d’au plus 200 caractères +@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language` doit être un nom ou une balise de langue, comme "Turkish" ou "tr" +@msg `leader` must be a string `leader` doit être une chaîne +@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel` doit valoir "romanBaseline" ou "emBoxTop" +@msg `leading` must be from 0.1 to 5000, or null for Auto `leading` doit être compris entre 0.1 et 5000, ou null pour Auto +@msg `length` must be a number of at most 1000000 pt either way `length` doit être un nombre d’au plus 1000000 pt dans un sens ou dans l’autre +@msg `line` must be true or false `line` doit valoir true ou false +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max` doit être compris entre 1 et {MAX_SUGGESTIONS} +@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle` ({min_angle}°) ne peut pas dépasser `maxAngle` ({max_angle}°) +@msg `mode` must be "handles", "straight" or "smooth" `mode` doit valoir "handles", "straight" ou "smooth" +@msg `mode` must be "straight", "bezier" or "arc" `mode` doit valoir "straight", "bezier" ou "arc" +@msg `mode` must be one of {_1} `mode` doit valoir l’une de ces valeurs : {_1} +@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi` doit valoir "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" ou "lineEndHalf" +@msg `name` must be a name that is not empty `name` doit être un nom non vide +@msg `name` must not be empty `name` ne doit pas être vide +@msg `names` is a list of preset names `names` est une liste de noms de paramètres prédéfinis +@msg `names` lists preset names `names` liste des noms de paramètres prédéfinis +@msg `names` must be a list of names `names` doit être une liste de noms +@msg `names` must be a list of swatch names `names` doit être une liste de noms de nuances +@msg `names` must be an array of style names `names` doit être une liste de noms de styles +@msg `new` is a design name, or true `new` est un nom de motif, ou true +@msg `origin` must be [x, y] in points `origin` doit être [x, y] en points +@msg `path` must be a folder `path` doit être un dossier +@msg `path` must be a path object `path` doit être un objet tracé +@msg `picks` must be an array (got {v}) `picks` doit être une liste (reçu : {v}) +@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks` doit contenir de 1 à {MAX_PICKS} prélèvements (reçu : {_1}) +@msg `pivot` must be a whole number from 0 to 8 `pivot` doit être un nombre entier compris entre 0 et 8 +@msg `points` must be a list of [x, y] points `points` doit être une liste de points [x, y] +@msg `points` must be a list of point indexes `points` doit être une liste d’indices de points +@msg `points` must be an array of point indices `points` doit être une liste d’indices de points +@msg `points` must be point indexes `points` doit contenir des indices de points +@msg `port` must be "in" or "out" `port` doit valoir "in" ou "out" +@msg `position` is above or below `position` vaut above ou below +@msg `preset` must be a string `preset` doit être une chaîne +@msg `radius` must be 0 or more `radius` doit être supérieur ou égal à 0 +@msg `ratio` must be from 0.1 to 0.5 `ratio` doit être compris entre 0.1 et 0.5 +@msg `rect` must be [x, y, width, height] with a positive size `rect` doit être [x, y, width, height] avec une taille positive +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect` doit être [x0, y0, x1, y1] (reçu : {r}) +@msg `relation` must be overlap, inside or outside `relation` doit valoir overlap, inside ou outside +@msg `replace` must be a string `replace` doit être une chaîne +@msg `reversed` must be true or false `reversed` doit valoir true ou false +@msg `rotate` must be a finite number `rotate` doit être un nombre fini +@msg `rows` must be lists of numbers or nulls, not {r} `rows` doit être des listes de nombres ou de null, pas {r} +@msg `rule` must be nonZero or evenOdd `rule` doit valoir nonZero ou evenOdd +@msg `scale` must be a number from 0.001 to 1000 `scale` doit être un nombre compris entre 0.001 et 1000 +@msg `scale` must be a number in (0, 100] `scale` doit être un nombre dans (0, 100] +@msg `scope.{key}` must be a non-empty array `scope.{key}` doit être une liste non vide +@msg `scope` must be "document", {artboards} or {layers} `scope` doit valoir "document", {artboards} ou {layers} +@msg `scope` must be "selection" or "document", not {other} `scope` doit valoir "selection" ou "document", pas {other} +@msg `script` must be a string `script` doit être une chaîne +@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes` est une liste non vide d’indices de séries +@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes` nécessite un `type` ou un `valueAxis` +@msg `shift` must be a number of points `shift` doit être un nombre de points +@msg `showCenter` must be true or false `showCenter` doit valoir true ou false +@msg `side` must be "left", "right", "top" or "bottom" `side` doit valoir "left", "right", "top" ou "bottom" +@msg `side` places the artboard: leave out x, y, width and height `side` place le plan de travail : omettez x, y, width et height +@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification` doit valoir "fullJustify", "alignLeft", "alignCenter" ou "alignRight" +@msg `size` is a percentage of the base text's size, 1 to 100 `size` est un pourcentage de la taille du texte de base, de 1 à 100 +@msg `source` and `target` are the same object `source` et `target` sont le même objet +@msg `source` is the only path in series group `target` `source` est le seul tracé du groupe de séries `target` +@msg `source` must be `folder` or `datasets` `source` doit valoir `folder` ou `datasets` +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` produit plus de {MAX_COUNT} lignes dans le cadre +@msg `spacing` must be a number `spacing` doit être un nombre +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing` doit être un nombre de points supérieur ou égal à 0, ou "auto" +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing` doit être supérieur à 0 et d’au plus {MAX_DISTANCE} pt +@msg `start` must be a finite number `start` doit être un nombre fini +@msg `starts` must be an array of anchor indices (or nulls) `starts` doit être une liste d’indices de points d’ancrage (ou de null) +@msg `status` must be linked, embedded, missing or modified, or a list of them `status` doit valoir linked, embedded, missing ou modified, ou une liste de ces valeurs +@msg `stop` must be a stop index `stop` doit être un indice d’arrêt +@msg `story` styles the whole story: give no `start` or `end` `story` met en forme tout l’article : n’indiquez ni `start` ni `end` +@msg `story` takes the whole story: give no `start` or `end` `story` prend tout l’article : n’indiquez ni `start` ni `end` +@msg `strokeMode` applies to strokes (stroke: true) `strokeMode` s’applique aux contours (stroke: true) +@msg `style` must be roman or japanese `style` doit valoir roman ou japanese +@msg `subpaths` must be two integer indices `subpaths` doit être deux indices entiers +@msg `symbol` must be a symbol name `symbol` doit être un nom de symbole +@msg `target` must be "object" or "contents", not {v} `target` doit valoir "object" ou "contents", pas {v} +@msg `target` must be an object of the same Objects on Path group as `member` `target` doit être un objet du même groupe Objets sur tracé que `member` +@msg `tint` applies to global and spot colours `tint` s’applique aux couleurs globales et aux tons directs +@msg `tint` goes with a global or spot `swatch` `tint` s’utilise avec une nuance `swatch` globale ou en ton direct +@msg `tint` must be a number (%) `tint` doit être un nombre (%) +@msg `tint` needs a global or spot colour `tint` nécessite une couleur globale ou un ton direct +@msg `to` is already threaded `to` est déjà chaîné +@msg `to` is the design's new name `to` est le nouveau nom du motif +@msg `to` must be [x, y] `to` doit être [x, y] +@msg `to` must be a closed path, area type or type on a path other than `from` `to` doit être un tracé fermé, un texte captif ou un texte curviligne autre que `from` +@msg `to` must be a whole number `to` doit être un nombre entier +@msg `to` takes the rows of one object's stack, not aligned rows `to` prend les lignes de la pile d’un seul objet, pas des lignes alignées +@msg `useLab` must be true or false `useLab` doit valoir true ou false +@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign` doit valoir default, top, middle, baseline ou bottom +@msg `values` must be a list `values` doit être une liste +@msg `values` must be an object `values` doit être un objet +@msg `values` must name hex, rgb, cmyk, lab or gray `values` doit indiquer hex, rgb, cmyk, lab ou gray +@msg `values` names a kind twice `values` indique deux fois le même type +@msg `version` must be 0 or {_1} `version` doit valoir 0 ou {_1} +@msg `version` must be 0 or {newest} `version` doit valoir 0 ou {newest} +@msg `weight` must be from 0.01 to 1000 pt `weight` doit être compris entre 0.01 et 1000 pt +@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights` doit être une liste de {_1} nombres, un par ligne (reçu : {v}) +@msg `xml` must be a string `xml` doit être une chaîne +@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it l’entrée {n} de `{KEY}` donne au paragraphe à l’octet {start} de {_1} d’autres retraits qu’une entrée précédente +@msg `{KEY}` entry {n} must be an object l’entrée {n} de `{KEY}` doit être un objet +@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` l’entrée {n} de `{KEY}` ne définit ni `leftIndent` ni `firstLineIndent` +@msg `{KEY}` entry {n}: `id` must be an object id entrée {n} de `{KEY}` : `id` doit être un identifiant d’objet +@msg `{KEY}` entry {n}: `start` must be a byte offset entrée {n} de `{KEY}` : `start` doit être une position en octets +@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt entrée {n} de `{KEY}` : `{k}` doit être un nombre compris entre -{MAX_PARA} et {MAX_PARA} pt +@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} entrée {n} de `{KEY}` : aucun paragraphe de {_1} ne commence à l’octet {start} +@msg `{KEY}` entry {n}: unknown key `{k}` entrée {n} de `{KEY}` : clé inconnue `{k}` +@msg `{KEY}` entry {n}: {_1} is not a type object entrée {n} de `{KEY}` : {_1} n’est pas un objet texte +@msg `{KEY}` is empty `{KEY}` est vide +@msg `{KEY}` must be a list `{KEY}` doit être une liste +@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}` accepte au plus {MAX_PARAGRAPH_INDENTS} entrées (reçu : {_1}) +@msg `{KEY}` takes no `{other}` beside it `{KEY}` n’accepte pas `{other}` à côté de lui +@msg `{_1}` can't be edited or removed `{_1}` ne peut être ni modifié ni supprimé +@msg `{_1}` is built in and always listed `{_1}` est intégré et toujours listé +@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}` n’est pas un fichier de police trouvé par la dernière recherche text.findFontFiles +@msg `{_1}` would be overwritten by the package: choose a different folder or name `{_1}` serait écrasé par l’assemblage : choisissez un autre dossier ou un autre nom +@msg `{base}` is larger than {_1} MB `{base}` dépasse {_1} Mo +@msg `{base}` is not in the DCS file's folder `{base}` n’est pas dans le dossier du fichier DCS +@msg `{file}` has no art to place `{file}` ne contient aucune illustration à importer +@msg `{file}` is over {_1} MB `{file}` dépasse {_1} Mo +@msg `{f}` belongs to a VectorCraft that is running `{f}` appartient à une instance de VectorCraft en cours d’exécution +@msg `{f}` is the copy of a document open now `{f}` est la copie d’un document actuellement ouvert +@msg `{id}` is no writable format (see document.formats) `{id}` n’est pas un format inscriptible (voir document.formats) +@msg `{id}` isn't a loaded library `{id}` n’est pas une bibliothèque chargée +@msg `{key}` has no colours `{key}` ne contient aucune couleur +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` contient plus de {MAX_EDIT_WORDS} mots +@msg `{key}` is a design name `{key}` est un nom de motif +@msg `{key}` is a design name or a list of them `{key}` est un nom de motif ou une liste de noms de motifs +@msg `{key}` is longer than {MAX_NAME} characters `{key}` dépasse {MAX_NAME} caractères +@msg `{key}` is out of range `{key}` est hors limites +@msg `{key}` must be 0 or more `{key}` doit être supérieur ou égal à 0 +@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` doit être [min, desired, max] ou {{min?, desired?, max?}}, en pourcentage +@msg `{key}` must be [x, y] numbers `{key}` doit être des nombres [x, y] +@msg `{key}` must be a list of words `{key}` doit être une liste de mots +@msg `{key}` must be a list, not {v} `{key}` doit être une liste, pas {v} +@msg `{key}` must be a non-negative integer `{key}` doit être un entier positif ou nul +@msg `{key}` must be a number or null, not {v} `{key}` doit être un nombre ou null, pas {v} +@msg `{key}` must be a number {_1}..{_2} `{key}` doit être un nombre de {_1} à {_2} +@msg `{key}` must be a whole number of 0 or more `{key}` doit être un nombre entier supérieur ou égal à 0 +@msg `{key}` must be a whole number of at least 0, not {n} `{key}` doit être un nombre entier d’au moins 0, pas {n} +@msg `{key}` must be an appearance item index (paint order) or null `{key}` doit être un indice d’élément d’aspect (ordre de peinture) ou null +@msg `{key}` must be an array of asset ids (see assets.list) `{key}` doit être une liste d’identifiants de ressources (voir assets.list) +@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}` doit être compris entre 0 et 180 degrés, pas {a} +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}` doit être compris entre 0 et {MAX_DISTANCE} pt +@msg `{key}` must be from {_1} to {shift_max} `{key}` doit être compris entre {_1} et {shift_max} +@msg `{key}` must be from {lo} to {hi}, not {v} `{key}` doit être compris entre {lo} et {hi}, pas {v} +@msg `{key}` must be one of auto, always, never `{key}` doit valoir auto, always ou never +@msg `{key}` must be one of miter, round, bevel `{key}` doit valoir miter, round ou bevel +@msg `{key}` must be one word of at most {_1} characters `{key}` doit être un seul mot d’au plus {_1} caractères +@msg `{key}` must hold single words of at most {_1} characters `{key}` doit contenir des mots isolés d’au plus {_1} caractères +@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}` doit désigner un objet attaché au tracé d’un groupe Objets sur tracé +@msg `{key}` names an object that is hidden or locked `{key}` désigne un objet masqué ou verrouillé +@msg `{key}`: not a colour: {v} `{key}` : ce n’est pas une couleur : {v} +@msg `{k}.family` is required `{k}.family` est obligatoire +@msg `{k}` is a number 0..1 `{k}` est un nombre de 0 à 1 +@msg `{k}` is true or false `{k}` vaut true ou false +@msg `{k}` isn't an export setting: {_1} `{k}` n’est pas un réglage d’export : {_1} +@msg `{k}` must be a byte offset `{k}` doit être une position en octets +@msg `{k}` must be a number `{k}` doit être un nombre +@msg `{k}` must be a number of points `{k}` doit être un nombre de points +@msg `{k}` must be a string `{k}` doit être une chaîne +@msg `{k}` must be a whole number `{k}` doit être un nombre entier +@msg `{k}` must be from {lo} to {hi} for {key} `{k}` doit être compris entre {lo} et {hi} pour {key} +@msg `{k}` must be {what}, not {v} `{k}` doit être {what}, pas {v} +@msg `{l}` isn't a global or spot colour `{l}` n’est ni une couleur globale ni un ton direct +@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}` n’est pas une image bitmap : utilisez clipboard.importSvg, importPdf ou importText +@msg `{name}` can't be deleted `{name}` ne peut pas être supprimé +@msg `{name}` can't be duplicated `{name}` ne peut pas être dupliqué +@msg `{name}` can't be edited `{name}` ne peut pas être modifié +@msg `{name}` can't be edited this way `{name}` ne peut pas être modifié de cette manière +@msg `{name}` can't be renamed `{name}` ne peut pas être renommé +@msg `{name}` can't name a folder `{name}` ne peut pas nommer un dossier +@msg `{name}` has no art to place `{name}` ne contient aucune illustration à importer +@msg `{name}` has no text to place `{name}` ne contient aucun texte à importer +@msg `{name}` in the Word document is not UTF-8 or UTF-16 text `{name}` dans le document Word n’est pas du texte UTF-8 ou UTF-16 +@msg `{name}` in the Word document is not well-formed XML: {e} `{name}` dans le document Word n’est pas du XML bien formé : {e} +@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep `{name}` dans le document Word imbrique des éléments sur plus de {MAX_NESTING} niveaux +@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}` est un paramètre prédéfini intégré en lecture seule : enregistrez-le sous un autre nom +@msg `{name}` is a built-in preset: save under another name `{name}` est un paramètre prédéfini intégré : enregistrez sous un autre nom +@msg `{name}` is a built-in profile and stays `{name}` est un profil intégré et ne peut pas être retiré +@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` est un fichier distinct, qui ne peut être trouvé qu’à côté du chemin du fichier DCS +@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}` est une teinte de `{base}` : modifiez `{base}` (une nuance de teinte n’accepte qu’un nouveau nom) +@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}` est un espace de travail colorimétrique : choisissez-en d’abord un autre avec edit.colorSettings +@msg `{name}` is built in and stays `{name}` est intégré et ne peut pas être retiré +@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}` accepte une peinture de son propre type (couleur, dégradé ou motif) +@msg `{name}`: {_1}: {e} `{name}` : {_1} : {e} +@msg `{n}` can't be deleted `{n}` ne peut pas être supprimé +@msg `{n}` can't be merged `{n}` ne peut pas être fusionné +@msg `{n}` can't be moved `{n}` ne peut pas être déplacé +@msg `{n}` is not a colour swatch `{n}` n’est pas une nuance de couleur +@msg `{n}` isn't a solid colour `{n}` n’est pas une couleur unie +@msg `{n}`: colour groups hold solid colours only `{n}` : les groupes de couleurs ne contiennent que des couleurs unies +@msg `{other}` doesn't trace an image `{other}` ne vectorise pas une image +@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}` : les références de l’étape copient plus de {_1} Mo de valeurs +@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}` n’est pas une balise d’axe : une balise compte quatre caractères, comme "wght" +@msg `{to}` is empty or already used `{to}` est vide ou déjà utilisé +@msg `{u}` is no Unicode character `{u}` n’est pas un caractère Unicode +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` dépasse {MAX_TEXT} octets +@msg `{what}` must be text `{what}` doit être du texte +@msg `{word}` isn't in the custom dictionary `{word}` n’est pas dans le dictionnaire personnalisé +@msg a CMap has too many mappings une CMap contient trop de correspondances +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard un dessin DXF a une seule origine : passez useArtboards: true pour un dessin par plan de travail +@msg a PDF presets file with over {_1} presets un fichier de paramètres prédéfinis PDF de plus de {_1} paramètres +@msg a \u escape needs four hexadecimal digits un échappement \u nécessite quatre chiffres hexadécimaux +@msg a batch preview cannot switch documents un aperçu de traitement par lots ne peut pas changer de document +@msg a brush named `{nn}` exists une forme nommée `{nn}` existe déjà +@msg a comma in an XPath expression must separate function arguments une virgule dans une expression XPath doit séparer des arguments de fonction +@msg a container of the listed objects is gone un conteneur des objets listés a disparu +@msg a content stream can't be read un flux de contenu ne peut pas être lu +@msg a content stream isn't a stream un flux de contenu n’est pas un flux +@msg a design holds at most {_1} objects un motif contient au plus {_1} objets +@msg a design needs a name un motif nécessite un nom +@msg a dictionary grew too large un dictionnaire est devenu trop grand +@msg a document holds at most {_1} SVG filters un document contient au plus {_1} filtres SVG +@msg a document keeps at most {_1} graph designs un document conserve au plus {_1} motifs de graphe +@msg a document keeps at most {_1} saved selections un document conserve au plus {_1} sélections enregistrées +@msg a document keeps at most {_1} views un document conserve au plus {_1} vues +@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files un document lie au plus {MAX_SVG_SCRIPTS} fichiers JavaScript +@msg a document with unsaved changes has no recovery copy un document comportant des modifications non enregistrées n’a pas de copie de récupération +@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes l’ordre d’un filtre feConvolveMatrix est {x}×{y}, ce qui dépasse la capacité de la multiplication de taille de noyau effectuée par usvg +@msg a font's data is too large les données d’une police sont trop volumineuses +@msg a font's subroutines are too large les sous-programmes d’une police sont trop volumineux +@msg a function declaration without a name une déclaration de fonction sans nom +@msg a gradient preset is already called {n} un paramètre prédéfini de dégradé s’appelle déjà {n} +@msg a graph has at most {_1} series, so it transposes with at most that many categories un graphe a au plus {_1} séries, il se transpose donc avec au plus autant de catégories +@msg a host value has too many entries une valeur de l’hôte a trop d’entrées +@msg a host value is nested too deep une valeur de l’hôte est imbriquée trop profondément +@msg a layer can't be merged into its own sublayer un calque ne peut pas être fusionné dans son propre sous-calque +@msg a layer's form box isn't an array le cadre du formulaire d’un calque n’est pas un tableau +@msg a layer's form can't be read le formulaire d’un calque ne peut pas être lu +@msg a layer's form has no box le formulaire d’un calque n’a pas de cadre +@msg a layer's form has no dictionary le formulaire d’un calque n’a pas de dictionnaire +@msg a legend draws at most {MAX_CHIPS} swatches une légende dessine au plus {MAX_CHIPS} nuances +@msg a name names one asset: give multiple: false, or one object un nom désigne une seule ressource : indiquez multiple: false, ou un seul objet +@msg a namespace declaration uses a reserved prefix or URI une déclaration d’espace de noms utilise un préfixe ou un URI réservé +@msg a namespace qualifier is undefined un qualificateur d’espace de noms n’est pas défini +@msg a namespace with an empty URI cannot have a prefix un espace de noms dont l’URI est vide ne peut pas avoir de préfixe +@msg a number has no digits un nombre n’a aucun chiffre +@msg a number's exponent has no digits l’exposant d’un nombre n’a aucun chiffre +@msg a number's fraction has no digits la partie décimale d’un nombre n’a aucun chiffre +@msg a page can't be read une page ne peut pas être lue +@msg a page object is used by something else un objet de page est utilisé par autre chose +@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) une vignette de page doit être en pixels RVB, de 1 à {THUMBNAIL_SIZE} de côté (reçu : {w} × {h}) +@msg a pattern layer doesn't end what it starts un calque de motif ne termine pas ce qu’il commence +@msg a pattern layer's colors hold something else les couleurs d’un calque de motif contiennent autre chose +@msg a pattern named `{n}` exists un motif nommé `{n}` existe déjà +@msg a plug-in with id {_1} is already installed un plug-in d’identifiant {_1} est déjà installé +@msg a preset named `{n}` exists un paramètre prédéfini nommé `{n}` existe déjà +@msg a print presets file with over {_1} presets un fichier de paramètres prédéfinis d’impression de plus de {_1} paramètres +@msg a profile is already called {n} un profil s’appelle déjà {n} +@msg a property descriptor cannot have both a value or writable and a get or set un descripteur de propriété ne peut pas avoir à la fois value ou writable et get ou set +@msg a property descriptor must be an object un descripteur de propriété doit être un objet +@msg a property's {name} must be a function or undefined le {name} d’une propriété doit être une fonction ou undefined +@msg a row can't go into itself une ligne ne peut pas être placée dans elle-même +@msg a series goes on the left or the right value axis une série se place sur l’axe des valeurs gauche ou droit +@msg a start must be an anchor index ≥ 0 or null un début doit être un indice de point d’ancrage ≥ 0 ou null +@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied un de ses flux se décoderait en plus de 256 Mo, ou ses paramètres de décodage ne peuvent pas être appliqués +@msg a string holds a control character une chaîne contient un caractère de contrôle +@msg a string holds an escape sequence JSON does not have une chaîne contient une séquence d’échappement qui n’existe pas en JSON +@msg a string is not closed une chaîne n’est pas fermée +@msg a style named `{name}` already exists un style nommé `{name}` existe déjà +@msg a style named `{to}` already exists un style nommé `{to}` existe déjà +@msg a swatch needs a colour, gradient or pattern une nuance nécessite une couleur, un dégradé ou un motif +@msg a symbol named `{n}` already exists un symbole nommé `{n}` existe déjà +@msg a theme has 1 to {THEME_SIZE} colours un thème compte de 1 à {THEME_SIZE} couleurs +@msg a theme is already called {name} (replace: "{name}" overwrites it) un thème s’appelle déjà {name} (replace: "{name}" l’écrase) +@msg a toolbar name has at most {MAX_NAME} characters un nom de barre d’outils compte au plus {MAX_NAME} caractères +@msg a view named `{_1}` exists une vue nommée `{_1}` existe déjà +@msg a view named `{name}` exists une vue nommée `{name}` existe déjà +@msg a workspace keeps at most {MAX} toolbars un espace de travail conserve au plus {MAX} barres d’outils +@msg a {_1} series can't go in a {_2} graph une série {_1} ne peut pas aller dans un graphe {_2} +@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v} : "pdf" (compatible PDF, par défaut) ou "7" (.ai ancien, version 7) +@msg already editing an opacity mask un masque d’opacité est déjà en cours de modification +@msg already editing another object's opacity mask le masque d’opacité d’un autre objet est déjà en cours de modification +@msg already in pattern editing mode déjà en mode d’édition de motif +@msg an SVG filter is named by its id: give `xml` with `name` un filtre SVG est nommé par son identifiant : indiquez `xml` avec `name` +@msg an XML element has two attributes with the same name un élément XML a deux attributs portant le même nom +@msg an XML value cannot be inserted into itself or its descendants une valeur XML ne peut pas être insérée dans elle-même ou dans ses descendants +@msg an XML value's children cannot be assigned by position les enfants d’une valeur XML ne peuvent pas être affectés par position +@msg an XML value's children cannot be deleted by position les enfants d’une valeur XML ne peuvent pas être supprimés par position +@msg an XMLList converts to XML only when it has one item un XMLList ne se convertit en XML que s’il contient un seul élément +@msg an XPath node test is missing un test de nœud XPath est manquant +@msg an XPath operand is missing un opérande XPath est manquant +@msg an XPath operator is missing un opérateur XPath est manquant +@msg an XPath predicate or step cannot follow here un prédicat ou une étape XPath ne peut pas suivre ici +@msg an XPath predicate or step follows a value that is not a node-set un prédicat ou une étape XPath suit une valeur qui n’est pas un ensemble de nœuds +@msg an XPath step is missing une étape XPath est manquante +@msg an artboard is listed twice un plan de travail est listé deux fois +@msg an empty artboard has no width or height to scale to un plan de travail vide n’a ni largeur ni hauteur à laquelle mettre à l’échelle +@msg an image couldn't be decoded and is missing from the picture une image n’a pas pu être décodée et manque dans l’illustration +@msg an image is too large for DXF and was left out une image est trop grande pour le DXF et a été omise +@msg an image of {_1} MB, at most {_2} MB une image de {_1} Mo, au plus {_2} Mo +@msg an image of {declared_w} × {declared_h} pixels is too large to draw une image de {declared_w} × {declared_h} pixels est trop grande pour être dessinée +@msg an image of {width} × {height} pixels is too large to draw une image de {width} × {height} pixels est trop grande pour être dessinée +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) une image de {w} × {h} pixels est trop grande pour être convertie (au plus {_1} pixels) +@msg an index must be a whole number un indice doit être un nombre entier +@msg an inline graphic has no glyphs un graphique incorporé n’a aucun glyphe +@msg an invisible text form can't be read un formulaire de texte invisible ne peut pas être lu +@msg an object is missing un objet est manquant +@msg an object's header is missing l’en-tête d’un objet est manquant +@msg an overprinting form can't be read un formulaire en surimpression ne peut pas être lu +@msg an unread object has no readable bounds un objet non lu n’a pas de limites lisibles +@msg an unread object holds a frame of threaded type un objet non lu contient un cadre de texte chaîné +@msg an unread object is missing from its layer un objet non lu manque dans son calque +@msg anchor `smooth` must be true or false `smooth` d’un point d’ancrage doit valoir true ou false +@msg anchor `{key}` must be [x, y] `{key}` d’un point d’ancrage doit être [x, y] +@msg anchor `{key}` must be a number `{key}` d’un point d’ancrage doit être un nombre +@msg angle and roundness must be finite l’angle et l’arrondi doivent être finis +@msg angles must be a list as long as pins (degrees or null) angles doit être une liste aussi longue que pins (degrés ou null) +@msg appearance must be flat|center|edge appearance doit valoir flat|center|edge +@msg apply is "fill" or "stroke", not `{other}` apply vaut "fill" ou "stroke", pas `{other}` +@msg apply was called on a value that is not a function apply a été appelé sur une valeur qui n’est pas une fonction +@msg area must be a closed polygon of 3 to 100000 [x, y] points area doit être un polygone fermé de 3 à 100000 points [x, y] +@msg area type in a thread cannot use Dynamic Text: {frames} un texte captif chaîné ne peut pas utiliser le texte dynamique : {frames} +@msg area type needs a path that encloses an area le texte captif nécessite un tracé qui délimite une zone +@msg area type {_1} is named twice le texte captif {_1} est désigné deux fois +@msg array literal too long littéral de tableau trop long +@msg arrowAlign must be extend|tip, got {o} arrowAlign doit valoir extend|tip, reçu : {o} +@msg arrowScale must be [start, end] arrowScale doit être [start, end] +@msg artboard is a 0-based index artboard est un indice commençant à 0 +@msg artboard must be a 0-based index artboard doit être un indice commençant à 0 +@msg artboard {i} doesn't print le plan de travail {i} ne s’imprime pas +@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout doit être {layout?, columns?, spacing?, rightToLeft?} +@msg artboardLayout.columns must be 1 or more artboardLayout.columns doit être supérieur ou égal à 1 +@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout doit valoir gridByRow, gridByColumn, row ou column +@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft doit valoir true ou false +@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing doit être compris entre 0 et {MAX_SPACING} pt +@msg artboards must be a non-empty list artboards doit être une liste non vide +@msg artboards must be a number artboards doit être un nombre +@msg asset `{_1}` has no art to export la ressource `{_1}` ne contient aucune illustration à exporter +@msg assignment to a constant binding affectation à une liaison constante +@msg at least one row that isn't excluded needs a weight above 0 au moins une ligne non exclue doit avoir un poids supérieur à 0 +@msg at most {MAX_KEYWORDS} keywords au plus {MAX_KEYWORDS} mots-clés +@msg at most {MAX_MATCHES} matches are kept au plus {MAX_MATCHES} correspondances sont conservées +@msg at most {MAX_POINTS} points au plus {MAX_POINTS} points +@msg at most {MAX_QUEUE} files au plus {MAX_QUEUE} fichiers +@msg at must be [x, y] at doit être [x, y] +@msg at/rect lie outside the canvas at/rect se trouvent en dehors de la zone de travail +@msg attribute must be size|leading|tracking|kerning|baselineShift attribute doit valoir size|leading|tracking|kerning|baselineShift +@msg attrs must be an object attrs doit être un objet +@msg automation path rejected: the folder's name isn't valid text chemin d’automatisation refusé : le nom du dossier n’est pas un texte valide +@msg automation path rejected: {_1} isn't valid text chemin d’automatisation refusé : {_1} n’est pas un texte valide +@msg automation path rejected: {why}: {path} chemin d’automatisation refusé : {why} : {path} +@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis doit valoir "vertical", "horizontal" ou un angle en degrés, pas `{given}` +@msg backgroundContents must be transparent or white backgroundContents doit valoir transparent ou white +@msg bad AES key clé AES incorrecte +@msg bad `runs`: {e} `runs` incorrect : {e} +@msg bad arrowScale arrowScale incorrect +@msg bad base64 base64 incorrect +@msg bad color {c} couleur incorrecte {c} +@msg bad colour couleur incorrecte +@msg bad colour in `colors` couleur incorrecte dans `colors` +@msg bad colour {v} couleur incorrecte {v} +@msg bad colour {v} in groupColors couleur incorrecte {v} dans groupColors +@msg bad cross-reference entry entrée de table de références croisées incorrecte +@msg bad cross-reference table table de références croisées incorrecte +@msg bad current colour {v} couleur actuelle incorrecte {v} +@msg bad dataBase64 dataBase64 incorrect +@msg bad edit modification incorrecte +@msg bad fill {v} fond incorrect {v} +@msg bad fit: {e} ajustement incorrect : {e} +@msg bad model {other} (keep, new or document) modèle incorrect {other} (keep, new ou document) +@msg bad new colour {v} nouvelle couleur incorrecte {v} +@msg bad object objet incorrect +@msg bad object number numéro d’objet incorrect +@msg bad offset position incorrecte +@msg bad path data: {e} données de tracé incorrectes : {e} +@msg bad point {v}: expected [x, y] or [x, y, pressure] point incorrect {v} : [x, y] ou [x, y, pressure] attendu +@msg bad reference référence incorrecte +@msg bad trailer size taille de trailer incorrecte +@msg bad {k} colour couleur {k} incorrecte +@msg batches cannot nest les traitements par lots ne peuvent pas être imbriqués +@msg bind something to a variable first liez d’abord quelque chose à une variable +@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed doit être compris entre 0 et {MAX_BLEED} pt : un nombre, [top, bottom, left, right] ou {{top, bottom, left, right}} +@msg blend must be a mode name blend doit être un nom de mode +@msg both sides of an XPath | must select nodes les deux côtés d’un | XPath doivent sélectionner des nœuds +@msg brush must be a name or null brush doit être un nom ou null +@msg by must be a number by doit être un nombre +@msg can't add page thumbnails: {why} impossible d’ajouter les vignettes de page : {why} +@msg can't add the editing data for other apps: {why} impossible d’ajouter les données d’édition destinées à d’autres applications : {why} +@msg can't edit the written PDF: {why} impossible de modifier le PDF écrit : {why} +@msg can't encrypt the PDF: {why} impossible de chiffrer le PDF : {why} +@msg can't linearise the PDF: {why} impossible de linéariser le PDF : {why} +@msg can't open `{_1}`: {_2} impossible d’ouvrir `{_1}` : {_2} +@msg can't open `{_1}`: {e} impossible d’ouvrir `{_1}` : {e} +@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) impossible d’ouvrir `{name}` : ce n’est pas un format que VectorCraft lit (voir document.formats) +@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) impossible d’importer `{name}` : ce n’est pas un format que VectorCraft lit (voir document.formats) +@msg can't read back the written PDF: {e} impossible de relire le PDF écrit : {e} +@msg can't read this Word document: {why} impossible de lire ce document Word : {why} +@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp impossible d’écrire des images .{e} : utilisez .png, .jpg, .gif, .webp, .tif ou .bmp +@msg can't write PDF layers, overprint or invisible text: {why} impossible d’écrire les calques PDF, la surimpression ou le texte invisible : {why} +@msg can't write the file's copyright: {why} impossible d’écrire le copyright du fichier : {why} +@msg can't write the file's modification date: {why} impossible d’écrire la date de modification du fichier : {why} +@msg can't write the pages' user units: {why} impossible d’écrire les unités utilisateur des pages : {why} +@msg canceled annulé +@msg cannot convert the object to a primitive value impossible de convertir l’objet en valeur primitive +@msg cannot convert undefined or null to an object impossible de convertir undefined ou null en objet +@msg cannot distort a zero-size or non-finite bounding box impossible de déformer un cadre de sélection de taille nulle ou non fini +@msg cannot move an object into itself impossible de déplacer un objet dans lui-même +@msg case must be upper|lower|title|sentence case doit valoir upper|lower|title|sentence +@msg category `{c}`: one of {_1} catégorie `{c}` : l’une de ces valeurs : {_1} +@msg center must be [x, y] center doit être [x, y] +@msg character dimensions cannot be represented with the source text transform les dimensions des caractères ne peuvent pas être représentées avec la transformation du texte source +@msg character dimensions cannot be represented with the target text transform les dimensions des caractères ne peuvent pas être représentées avec la transformation du texte cible +@msg choose at least one artboard choisissez au moins un plan de travail +@msg choose at least one status choisissez au moins un état +@msg clip must be true or false clip doit valoir true ou false +@msg clippingMask must be true or false clippingMask doit valoir true ou false +@msg closed must be a boolean closed doit être un booléen +@msg collapsed must be true or false collapsed doit valoir true ou false +@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile : aucun profil RVB ou CMJN ne s’appelle « {profile} » +@msg colorMode must be rgb or cmyk, not `{m}` colorMode doit valoir rgb ou cmyk, pas `{m}` +@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel doit valoir {own} (le mode du document), grayscale ou bitmap +@msg colour groups can't go into colour groups un groupe de couleurs ne peut pas aller dans un groupe de couleurs +@msg colour, mode and spot apply to solid-colour swatches only couleur, mode et ton direct s’appliquent uniquement aux nuances de couleur unie +@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read colonne {column} : `{cell}` est une colonne `%` des fichiers de données de graphe, qui ne sont pas lus +@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable colonne {column} : `{cell}` désigne `{name}`, qui est une variable {_1} +@msg column {column}: `{cell}` names no variable colonne {column} : `{cell}` ne désigne aucune variable +@msg column {column}: the variable name is longer than {MAX_NAME} characters colonne {column} : le nom de variable dépasse {MAX_NAME} caractères +@msg column: true floats the whole column and takes no onto, above or below column: true rend flottante toute la colonne et n’accepte ni onto, ni above, ni below +@msg command must be a shape.* command command doit être une commande shape.* +@msg commands[{i}]: missing `command` commands[{i}] : `command` manquant +@msg composer must be "singleLine" or "everyLine" composer doit valoir "singleLine" ou "everyLine" +@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes les formes composées acceptent les tracés, tracés transparents, groupes, formes composées, textes, dégradés de formes et enveloppes +@msg compound {_1} must contain exactly one open path to join le tracé transparent {_1} doit contenir exactement un tracé ouvert à joindre +@msg connect end must be "start" or "end" l’extrémité de connect doit valoir "start" ou "end" +@msg connect needs a path other than the one extended connect nécessite un tracé autre que celui qui est prolongé +@msg connect needs id connect nécessite id +@msg connect path has no open end le tracé de connect n’a pas d’extrémité ouverte +@msg continuation must be an array of path chunks continuation doit être une liste de fragments de tracé +@msg continuation requires points continuation nécessite des points +@msg copy needs path: the file to copy into the document's folder copy nécessite path : le fichier à copier dans le dossier du document +@msg copy takes images only: relink placed documents where their files are copy n’accepte que des images : reliez les documents importés là où se trouvent leurs fichiers +@msg copying a file isn't available on the web la copie d’un fichier n’est pas disponible sur le Web +@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus doit valoir unknown, copyrighted ou publicDomain +@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl doit être une URL (sans espaces, au plus 2048 caractères) +@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners doit être 4 points [x, y] (TL, TR, BR, BL) +@msg counterPer `{c}`: item or file counterPer `{c}` : item ou file +@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}` : seules les pages PDF ont des zones art, trim, bleed et media ; utilisez "crop" ou "bounding" +@msg crop rectangle doesn't overlap the image le rectangle de recadrage ne chevauche pas l’image +@msg cross-reference and object streams aren't supported les flux de références croisées et d’objets ne sont pas pris en charge +@msg damaged compressed data: {e} données compressées endommagées : {e} +@msg dash lengths must be numbers, got {v} les longueurs de tiret doivent être des nombres, reçu : {v} +@msg dash must be a list of lengths or null, got {v} dash doit être une liste de longueurs ou null, reçu : {v} +@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) clé de données `{k}` : lettres, chiffres, -, _ et . (ni name ni vc-…) +@msg data must be an object of key: value, not {v} data doit être un objet clé: valeur, pas {v} +@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare le jeu de données `{name}` contient une valeur pour `{var}`, que la bibliothèque ne déclare pas +@msg data set `{name}` holds two values for `{var}` le jeu de données `{name}` contient deux valeurs pour `{var}` +@msg data set `{name}` is declared twice le jeu de données `{name}` est déclaré deux fois +@msg data takes at most {MAX_DATA} keys at once data accepte au plus {MAX_DATA} clés à la fois +@msg dataset `{name}` already exists le jeu de données `{name}` existe déjà +@msg dataset `{to}` already exists le jeu de données `{to}` existe déjà +@msg date `{d}`: YYYY-MM-DD date `{d}` : AAAA-MM-JJ +@msg decoded data is too large les données décodées sont trop volumineuses +@msg define a variable first définissez d’abord une variable +@msg detect must be true or false detect doit valoir true ou false +@msg direct-select anchor points of a path sélectionnez des points d’ancrage d’un tracé avec l’outil Sélection directe +@msg distance must be > 0 distance doit être > 0 +@msg dx and dy give the arrow's direction dx et dy donnent la direction de la flèche +@msg dx and dy must be numbers dx et dy doivent être des nombres +@msg dxf fitTo must be [width, height], positive dxf fitTo doit être [width, height], positifs +@msg dxf must be an object of DXF import options dxf doit être un objet d’options d’import DXF +@msg dxf scale must be a positive number up to {_1}, not {s} l’échelle dxf doit être un nombre positif d’au plus {_1}, pas {s} +@msg dxf unit `{u}`: a unit such as mm, cm, in or pt unité dxf `{u}` : une unité comme mm, cm, in ou pt +@msg dxf: {e} dxf : {e} +@msg each format is an object {format, scale?, suffix?} chaque format est un objet {format, scale?, suffix?} +@msg each item needs an id chaque élément nécessite un identifiant +@msg each match needs `kind` chaque correspondance nécessite `kind` +@msg each match needs a `name` chaque correspondance nécessite un `name` +@msg each of `fonts` is {family, style?}; 1 to 1000 of them chaque élément de `fonts` est {family, style?} ; de 1 à 1000 éléments +@msg each of `items` is {kind, item} chaque élément de `items` est {kind, item} +@msg each of `items` is {{id, corners?}} with an object id, not {it} chaque élément de `items` est {{id, corners?}} avec un identifiant d’objet, pas {it} +@msg each path chunk must contain 1 to 4096 points chaque fragment de tracé doit contenir de 1 à 4096 points +@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) chaque prélèvement est {{at: [x, y]}} ou {{rect: [x0, y0, x1, y1]}} (reçu : {v}) +@msg each row needs `from` chaque ligne nécessite `from` +@msg each stop needs a position chaque taquet nécessite une position +@msg each subpath needs an anchors array chaque sous-tracé nécessite une liste anchors +@msg each weight must be a number of 0 or more (got {v}) chaque poids doit être un nombre supérieur ou égal à 0 (reçu : {v}) +@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType doit valoir easeIn, easeOut, easeInOut ou easeOutIn +@msg editingData must be true or false editingData doit valoir true ou false +@msg editingData must be true, false or "always" editingData doit valoir true, false ou "always" +@msg edits overlap des modifications se chevauchent +@msg effects of objects whose appearances differ can't be moved together les effets d’objets dont l’aspect diffère ne peuvent pas être déplacés ensemble +@msg empty header in column {_1} en-tête vide dans la colonne {_1} +@msg encoding `{_1}`: utf8 or utf16 encodage `{_1}` : utf8 ou utf16 +@msg ends must be two of "first" and "last" ends doit être deux valeurs parmi "first" et "last" +@msg every numbered name for the result is taken tous les noms numérotés pour le résultat sont pris +@msg expanded SVG styles exceed the import size limit les styles SVG développés dépassent la taille limite d’import +@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes le fichier de paramètres prédéfinis d’export dépasse {MAX_PRESET_FILE_BYTES} octets +@msg exportText must be editable or appearance exportText doit valoir editable ou appearance +@msg exportType must be movieClip or graphic, not `{s}` exportType doit valoir movieClip ou graphic, pas `{s}` +@msg extend needs id extend nécessite id +@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges liste au plus {MAX_FEATURE_CHANGES} balises +@msg featureChanges must be a list of tags featureChanges doit être une liste de balises +@msg features must be a list of tags features doit être une liste de balises +@msg fidelity must be 0..100 fidelity doit être compris entre 0 et 100 +@msg files must be {name, dataBase64} files doit être {name, dataBase64} +@msg finally table {t} out of range table finally {t} hors limites +@msg fit must be none|autoHeight|shrinkText|block, got {id} fit doit valoir none|autoHeight|shrinkText|block, reçu : {id} +@msg flags cannot be given with a RegExp object les indicateurs ne peuvent pas être fournis avec un objet RegExp +@msg flattener: {e} aplatissement : {e} +@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}` : high, medium, low ou un paramètre enregistré (voir flattener.presets.list) +@msg flipProfile must be along|across, got {o} flipProfile doit valoir along|across, reçu : {o} +@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}` : none, rows, format, item ou itemFormat +@msg folders deeper than {MAX_BATCH_DEPTH} levels dossiers de plus de {MAX_BATCH_DEPTH} niveaux de profondeur +@msg folders deeper than {MAX_DEPTH} levels dossiers de plus de {MAX_DEPTH} niveaux de profondeur +@msg font family `{family}` is not available la famille de polices `{family}` n’est pas disponible +@msg font family `{tf}` is not available la famille de polices `{tf}` n’est pas disponible +@msg format `{f}`: pdf or postscript format `{f}` : pdf ou postscript +@msg format `{f}`: text or json format `{f}` : text ou json +@msg format is `{format}` le format est `{format}` +@msg formats must be an array of rows, not {v} formats doit être une liste de lignes, pas {v} +@msg frequency must be between 1 and 300 lines per inch la linéature doit être comprise entre 1 et 300 lignes par pouce +@msg from and to must be finite from et to doivent être finis +@msg from must be [x0, y0, x1, y1] from doit être [x0, y0, x1, y1] +@msg function declaration index out of range indice de déclaration de fonction hors limites +@msg give `before` or `after`, not both indiquez `before` ou `after`, pas les deux +@msg give `char`, `text`, `code` or `unicode` indiquez `char`, `text`, `code` ou `unicode` +@msg give `colors`, or `color` and `rule` indiquez `colors`, ou `color` et `rule` +@msg give `count` or `spacing`, not both indiquez `count` ou `spacing`, pas les deux +@msg give `event`, such as onclick indiquez `event`, par exemple onclick +@msg give `file`, the JavaScript file's path or URL indiquez `file`, le chemin ou l’URL du fichier JavaScript +@msg give `fill` and/or `stroke` indiquez `fill` et/ou `stroke` +@msg give `fill`, `stroke` or both indiquez `fill`, `stroke` ou les deux +@msg give `folder` indiquez `folder` +@msg give `fonts`, or open a document indiquez `fonts`, ou ouvrez un document +@msg give `from` or `indices`, not both indiquez `from` ou `indices`, pas les deux +@msg give `id` (no text is being edited) indiquez `id` (aucun texte n’est en cours de modification) +@msg give `id` and `anchors`, or `items`, not both indiquez `id` et `anchors`, ou `items`, pas les deux +@msg give `id` or `guide`, not both indiquez `id` ou `guide`, pas les deux +@msg give `length` or `to` indiquez `length` ou `to` +@msg give `matches`: [{kind, name, target}] indiquez `matches` : [{kind, name, target}] +@msg give `name` (or edit a pattern) indiquez `name` (ou modifiez un motif) +@msg give `name` or `names` indiquez `name` ou `names` +@msg give `name`, an SVG filter's name indiquez `name`, le nom d’un filtre SVG +@msg give `path` (a .wasm file) or `dataBase64` (the module) indiquez `path` (un fichier .wasm) ou `dataBase64` (le module) +@msg give `path`, `data` or `dataBase64` indiquez `path`, `data` ou `dataBase64` +@msg give `path`, `data`, `dataBase64` or `csv` indiquez `path`, `data`, `dataBase64` ou `csv` +@msg give `path`, `xml` or `dataBase64` indiquez `path`, `xml` ou `dataBase64` +@msg give `path`, an SVG file, or `svg`, SVG text indiquez `path`, un fichier SVG, ou `svg`, du texte SVG +@msg give `script`, the JavaScript the event runs ("" removes the event) indiquez `script`, le code JavaScript exécuté par l’événement ("" supprime l’événement) +@msg give `text` (the reading) or `remove` indiquez `text` (la lecture) ou `remove` +@msg give `x` and `y` (and a `tolerance` of 0 or more) indiquez `x` et `y` (et une `tolerance` supérieure ou égale à 0) +@msg give `xml`, one SVG element indiquez `xml`, un seul élément SVG +@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments indiquez un mode ou des réglages r/g/b, c/m/y/k, gray ou tint +@msg give a name, or paint the active proxy with a saved preset indiquez un nom, ou peignez l’échantillon actif avec un paramètre prédéfini enregistré +@msg give a name, or select a stroke with a saved profile indiquez un nom, ou sélectionnez un contour doté d’un profil enregistré +@msg give a pattern name or select an object painted with a pattern indiquez un nom de motif ou sélectionnez un objet peint avec un motif +@msg give at least one of clip, invert, disabled, linked indiquez au moins l’un des paramètres clip, invert, disabled, linked +@msg give color, none, swatch or gradient indiquez color, none, swatch ou gradient +@msg give dataBase64 indiquez dataBase64 +@msg give either at (a click) or area (a path drawn around the overlaps) indiquez soit at (un clic), soit area (un tracé dessiné autour des chevauchements) +@msg give exactly one of `path`, `data` and `dataBase64` indiquez exactement un paramètre parmi `path`, `data` et `dataBase64` +@msg give formats or a preset, not both indiquez formats ou un paramètre prédéfini, pas les deux +@msg give index or indexes, not both indiquez index ou indexes, pas les deux +@msg give newName or delete indiquez newName ou delete +@msg give offset or by indiquez offset ou by +@msg give one element indiquez un seul élément +@msg give one of `to`, `rect` or `at` indiquez l’un des paramètres `to`, `rect` ou `at` +@msg give one of assets, layers, selection or fullDocument indiquez l’un des paramètres assets, layers, selection ou fullDocument +@msg give only one of onto, above, below, icon and a column place indiquez un seul paramètre parmi onto, above, below, icon et un emplacement de colonne +@msg give opacity or blend indiquez opacity ou blend +@msg give origin: [x, y], or reset: true indiquez origin: [x, y], ou reset: true +@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note indiquez overprintFill, overprintStroke, showCenter, imageMap, url ou note +@msg give path (a file) or folder indiquez path (un fichier) ou folder +@msg give path, or name and dataBase64 indiquez path, ou name et dataBase64 +@msg give paths or files indiquez paths ou files +@msg give rows, rowHeight, columns or columnWidth indiquez rows, rowHeight, columns ou columnWidth +@msg give svg, or dataBase64 of an SVG or SVGZ file indiquez svg, ou le dataBase64 d’un fichier SVG ou SVGZ +@msg give text indiquez text +@msg give the asset a name donnez un nom à la ressource +@msg give the preset a `name` donnez un `name` au paramètre prédéfini +@msg give the preset a name donnez un nom au paramètre prédéfini +@msg give the preset a name (1–64 characters) donnez un nom au paramètre prédéfini (de 1 à 64 caractères) +@msg give the settings to change, e.g. {"formats"}]} indiquez les réglages à modifier, par exemple {"formats"}]} +@msg give the style a name donnez un nom au style +@msg give two or more style `names` indiquez au moins deux noms de styles `names` +@msg give two or more swatches indiquez au moins deux nuances +@msg give x, y, or a handle's length or angle indiquez x, y, ou la longueur ou l’angle d’une poignée +@msg give {key, value} or {values} indiquez {key, value} ou {values} +@msg global must be true or false global doit valoir true ou false +@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) le glyphe {_1} n’est pas dessiné pour {ch} avec le style du caractère (comme Tout en capitales, Petites capitales ou un système de chiffres) +@msg glyph {g} is not an alternate of {ch} in {_1} {_2} le glyphe {g} n’est pas une variante de {ch} dans {_1} {_2} +@msg gradient must be "objects" or "mesh" gradient doit valoir "objects" ou "mesh" +@msg grays must be "profile" or "black", not `{g}` grays doit valoir "profile" ou "black", pas `{g}` +@msg gridColors must be a preset name or two colours gridColors doit être un nom de paramètre prédéfini ou deux couleurs +@msg gridSize must be small, medium or large gridSize doit valoir small, medium ou large +@msg guide {g} is not selected le repère {g} n’est pas sélectionné +@msg handle must be 0..3 handle doit être compris entre 0 et 3 +@msg handler at {_1} expects {_2} values but its range starts with {d} le gestionnaire à {_1} attend {_2} valeurs, mais sa plage commence avec {d} +@msg handler depth too large profondeur de gestionnaire trop grande +@msg handler range {_1}..{_2} out of range plage de gestionnaire {_1}..{_2} hors limites +@msg hiddenLayers must be true or false hiddenLayers doit valoir true ou false +@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v} : "layer" (par défaut), "omit" ou "visible" +@msg highlight a layer to merge into mettez en surbrillance un calque dans lequel fusionner +@msg highlight two or more rows in one layer or group mettez en surbrillance au moins deux lignes d’un même calque ou groupe +@msg highlight two or more rows to merge mettez en surbrillance au moins deux lignes à fusionner +@msg hvcC hvcC +@msg icon docks a panel in the icon column: use window.panel.dock icon ancre un panneau dans la colonne d’icônes : utilisez window.panel.dock +@msg id must be a dimension id id doit être un identifiant de cote +@msg image has no bounds l’image n’a pas de limites +@msg image {_1} is linked already l’image {_1} est déjà liée +@msg imageMap `{m}`: none, client or server imageMap `{m}` : none, client ou server +@msg images in a spot ink whose samples couldn't be read were imported in RGB les images en encre de ton direct dont les échantillons n’ont pas pu être lus ont été importées en RVB +@msg images past the most pixels one file may hold were left out les images dépassant le nombre maximal de pixels qu’un fichier peut contenir ont été omises +@msg images too large to read were left out les images trop grandes pour être lues ont été omises +@msg index out of range indice hors limites +@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes peut contenir au plus {MAX_GUIDE_INDEXES} entrées +@msg indexes must be an array of guide indexes indexes doit être une liste d’indices de repères +@msg instances must be 1..1000 instances doit être compris entre 1 et 1000 +@msg instruction out of range instruction hors limites +@msg instruction {pc} ({op}) pops below the stack l’instruction {pc} ({op}) dépile sous le bas de la pile +@msg instruction {pc} reached with stack depths {seen} and {d} instruction {pc} atteinte avec des profondeurs de pile {seen} et {d} +@msg invalid `color` `color` non valide +@msg invalid anchor index {v}: expected a non-negative integer indice de point d’ancrage non valide {v} : entier positif ou nul attendu +@msg invalid anchor reference {v}: expected [subpath, anchor] référence de point d’ancrage non valide {v} : [subpath, anchor] attendu +@msg invalid angle angle non valide +@msg invalid array length longueur de tableau non valide +@msg invalid brush: {e} forme non valide : {e} +@msg invalid color couleur non valide +@msg invalid colour {c} couleur non valide {c} +@msg invalid delta delta non valide +@msg invalid group groupe non valide +@msg invalid object id {v}: expected a non-negative integer identifiant d’objet non valide {v} : entier positif ou nul attendu +@msg invalid point point non valide +@msg invalid regular expression flag '{_1}' indicateur d’expression régulière non valide '{_1}' +@msg invalid regular expression: {m} expression régulière non valide : {m} +@msg invalid size taille non valide +@msg invalid t t non valide +@msg invalid time value valeur de temps non valide +@msg invalid tolerance tolérance non valide +@msg is a confirm's answer, whose `value` must be true or false est la réponse à une confirmation, dont `value` doit valoir true ou false +@msg is a prompt's answer, whose `value` must be a string or null est la réponse à une invite, dont `value` doit être une chaîne ou null +@msg is an alert's answer, whose `value` must be "ok" est la réponse à une alerte, dont `value` doit valoir "ok" +@msg is longer than 16 KiB dépasse 16 Kio +@msg it draws its image through a clipping path il dessine son image à travers un tracé de détourage +@msg it draws no image il ne dessine aucune image +@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes il dépasse {MAX_DICTIONARY_FILE_BYTES} octets +@msg it isn't a file ce n’est pas un fichier +@msg it names {_1} plates, and at most {MAX_PLATES} are read il désigne {_1} plaques, et au plus {MAX_PLATES} sont lues +@msg its art is too long or too detailed for a head son illustration est trop longue ou trop détaillée pour une pointe +@msg its art paints nothing son illustration ne peint rien +@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) tous ses plans de travail se trouvent au-delà de la zone de travail (à plus de {_1} pt de l’origine) +@msg its clipping masks are nested too deeply ses masques d’écrêtage sont imbriqués trop profondément +@msg its image is too large to read son image est trop grande pour être lue +@msg its metadata has no description ses métadonnées n’ont pas de description +@msg its metadata has no direct length ses métadonnées n’ont pas de longueur directe +@msg its plates are too large: more than {MAX_SAMPLES} pixels together ses plaques sont trop grandes : plus de {MAX_SAMPLES} pixels au total +@msg its plates are too large: {_1} plates of {w} × {h} pixels ses plaques sont trop grandes : {_1} plaques de {w} × {h} pixels +@msg its plates can't be kept together ses plaques ne peuvent pas être conservées ensemble +@msg its shapes could not be combined into one ses formes n’ont pas pu être combinées en une seule +@msg join two different paths joignez deux tracés différents +@msg jump to {pc}, past the end saut vers {pc}, au-delà de la fin +@msg justify must be auto|left|center|right|justifyAll justify doit valoir auto|left|center|right|justifyAll +@msg kbps must be a positive number, not {kbps} kbps doit être un nombre positif, pas {kbps} +@msg keywords must be a list of words or a comma-separated string keywords doit être une liste de mots ou une chaîne séparée par des virgules +@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords doit compter au plus {MAX_KEYWORD} caractères, sans caractères de contrôle +@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind doit valoir 1, 2 ou 3, ou indiquez un `name` de paramètre prédéfini (voir perspective.presets.list) +@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind doit valoir graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle ou paraStyle +@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout doit valoir "on", "off", "neutral" ou un booléen, pas {v} +@msg layer `{name}` has no art to export le calque `{name}` ne contient aucune illustration à exporter +@msg layer must name a layer or sublayer layer doit désigner un calque ou un sous-calque +@msg layers can't be intertwined: select the objects in them les calques ne peuvent pas être entrelacés : sélectionnez les objets qu’ils contiennent +@msg layers go only at the top level or in layers, objects in layers and groups les calques vont uniquement au niveau supérieur ou dans des calques, les objets dans des calques et des groupes +@msg layers must be true or false layers doit valoir true ou false +@msg layout must be gridByRow, gridByColumn, row or column layout doit valoir gridByRow, gridByColumn, row ou column +@msg leading must be a number or "auto" leading doit être un nombre ou "auto" +@msg left/right must be non-negative widths left/right doivent être des largeurs positives ou nulles +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding doit valoir l’une de ces valeurs : {_1} +@msg level `{t}`: 2 or 3 niveau `{t}` : 2 ou 3 +@msg line {line}: more than {MAX_CSV_COLS} columns ligne {line} : plus de {MAX_CSV_COLS} colonnes +@msg line {line}: unclosed quote ligne {line} : guillemet non fermé +@msg line {line}: value for `{var}` must match its variable kind ligne {line} : la valeur de `{var}` doit correspondre au type de sa variable +@msg lineEndings `{other}`: lf or crlf lineEndings `{other}` : lf ou crlf +@msg loading profiles from disk isn't available in the browser le chargement de profils depuis le disque n’est pas disponible dans le navigateur +@msg localize: the first argument must be a localization object or a string localize : le premier argument doit être un objet de localisation ou une chaîne +@msg margin: a {m} pt margin leaves no imageable area on the paper marge : une marge de {m} pt ne laisse aucune zone imprimable sur le papier +@msg matrix must be finite and invertible, depth finite matrix doit être finie et inversible, et depth finie +@msg matte: {e} cache : {e} +@msg member is for order front, forward, backward or back member s’utilise avec order front, forward, backward ou back +@msg missing `artboards` `artboards` manquant +@msg missing `at` [x, y] `at` [x, y] manquant +@msg missing `attrs` `attrs` manquant +@msg missing `color` or `opacity` `color` ou `opacity` manquant +@msg missing `colors` `colors` manquant +@msg missing `commands` `commands` manquant +@msg missing `destinationFolder` `destinationFolder` manquant +@msg missing `effect` id identifiant `effect` manquant +@msg missing `family` `family` manquant +@msg missing `find` `find` manquant +@msg missing `gid` (a glyph id, or null for the default glyph) `gid` manquant (un identifiant de glyphe, ou null pour le glyphe par défaut) +@msg missing `group` `group` manquant +@msg missing `id` `id` manquant +@msg missing `id`: give one, or select one type object `id` manquant : indiquez-en un, ou sélectionnez un objet texte +@msg missing `index` (a point index, or null to clear) `index` manquant (un indice de point, ou null pour effacer) +@msg missing `index` (a stop index, or null to clear) `index` manquant (un indice d’arrêt, ou null pour effacer) +@msg missing `index` (an item index or null) `index` manquant (un indice d’élément ou null) +@msg missing `item` `item` manquant +@msg missing `library` `library` manquant +@msg missing `library` (see graphicStyle.libraries) `library` manquant (voir graphicStyle.libraries) +@msg missing `library` (see swatch.library.list) `library` manquant (voir swatch.library.list) +@msg missing `map` (rows [{from, to}] or {current}) `map` manquant (lignes [{from, to}] ou {current}) +@msg missing `matrix` [a,b,c,d,e,f] `matrix` [a,b,c,d,e,f] manquant +@msg missing `name` (see gradient.presets.list) `name` manquant (voir gradient.presets.list) +@msg missing `offset` (a distance in points) `offset` manquant (une distance en points) +@msg missing `path` `path` manquant +@msg missing `path` (a path object id) `path` manquant (un identifiant d’objet tracé) +@msg missing `picks` `picks` manquant +@msg missing `rule` `rule` manquant +@msg missing `source` id identifiant `source` manquant +@msg missing `start` (the byte offset of a character) `start` manquant (la position en octets d’un caractère) +@msg missing `status`: linked, embedded, missing or modified `status` manquant : linked, embedded, missing ou modified +@msg missing `target` id identifiant `target` manquant +@msg missing `text` `text` manquant +@msg missing `to` `to` manquant +@msg missing `to` (an index) `to` manquant (un indice) +@msg missing `to`: a non-negative integer `to` manquant : un entier positif ou nul +@msg missing `type` `type` manquant +@msg missing `value` `value` manquant +@msg missing `word` `word` manquant +@msg missing `{key}` `{key}` manquant +@msg missing `{k}` `{k}` manquant +@msg missing anchor point d’ancrage manquant +@msg missing anchors points d’ancrage manquants +@msg missing anchors [[subpath, anchor]…] anchors [[subpath, anchor]…] manquant +@msg missing array `{key}` liste `{key}` manquante +@msg missing case case manquant +@msg missing color couleur manquante +@msg missing commands commandes manquantes +@msg missing destination folder dossier de destination manquant +@msg missing from from manquant +@msg missing group groupe manquant +@msg missing id or ids id ou ids manquant +@msg missing ids ids manquant +@msg missing indexes indexes manquant +@msg missing matrix [a,b,c,d,e,f] matrix [a,b,c,d,e,f] manquant +@msg missing name nom manquant +@msg missing op op manquant +@msg missing or invalid `color` `color` manquant ou non valide +@msg missing or unknown `tool` `tool` manquant ou inconnu +@msg missing point point manquant +@msg missing point [x,y] point [x,y] manquant +@msg missing point `{key}` ([x, y]) point `{key}` manquant ([x, y]) +@msg missing points points manquants +@msg missing points [[x,y]…] points [[x,y]…] manquants +@msg missing segment segment manquant +@msg missing stops taquets manquants +@msg missing subpaths sous-tracés manquants +@msg missing target cible manquante +@msg missing to to manquant +@msg missing x, y x, y manquants +@msg mode is set, add or toggle, not `{other}` mode vaut set, add ou toggle, pas `{other}` +@msg mode must be "cmyk" or "rgb" mode doit valoir "cmyk" ou "rgb" +@msg mode must be area|onPath, got {o} mode doit valoir area|onPath, reçu : {o} +@msg more than 10000 linked files: package dependencies in smaller parts plus de 10000 fichiers liés : assemblez les dépendances en plusieurs parties plus petites +@msg more than 256 commands plus de 256 commandes +@msg more than {MAX_BATCH_FILES} data sets plus de {MAX_BATCH_FILES} jeux de données +@msg more than {MAX_BATCH_FILES} files plus de {MAX_BATCH_FILES} fichiers +@msg more than {MAX_CSV_ROWS} data rows plus de {MAX_CSV_ROWS} lignes de données +@msg more than {MAX_FILES} images plus de {MAX_FILES} images +@msg more than {MAX_IMPORT} presets plus de {MAX_IMPORT} paramètres prédéfinis +@msg moved must be [[x,y]…] moved doit être [[x,y]…] +@msg must be an object with `kind` and `value` doit être un objet avec `kind` et `value` +@msg must have `kind` alert, confirm or prompt doit avoir `kind` alert, confirm ou prompt +@msg must have exactly `kind` and `value` doit avoir exactement `kind` et `value` +@msg name (a toolbar's name) is required name (le nom d’une barre d’outils) est obligatoire +@msg name (a toolbar's name) must be a string name (le nom d’une barre d’outils) doit être une chaîne +@msg name must be a toolbar's name name doit être le nom d’une barre d’outils +@msg names is empty names est vide +@msg names must be an array of symbol names names doit être une liste de noms de symboles +@msg need `anchor` or `segment` + `t` `anchor` ou `segment` + `t` nécessaire +@msg need at least one anchor au moins un point d’ancrage est nécessaire +@msg need at least one point au moins un point est nécessaire +@msg need at least two points au moins deux points sont nécessaires +@msg needs a closing quote after the name nécessite un guillemet fermant après le nom +@msg needs a name nécessite un nom +@msg needs a name of at most 64 bytes nécessite un nom d’au plus 64 octets +@msg needs a name of letters, digits and _, or a quoted name nécessite un nom composé de lettres, de chiffres et de _, ou un nom entre guillemets +@msg needs a name without control characters nécessite un nom sans caractères de contrôle +@msg needs points nécessite des points +@msg nesting too deep imbrication trop profonde +@msg nineSlice must be true or false nineSlice doit valoir true ou false +@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides doit être [left, top, right, bottom], chacun entre -16 et 16, avec left + right < 1 et top + bottom < 1 +@msg no HP-GL format aucun format HP-GL +@msg no New Document profile called `{shown}` ({_1}) aucun profil de nouveau document nommé `{shown}` ({_1}) +@msg no PDF preset named `{n}` aucun paramètre prédéfini PDF nommé `{n}` +@msg no SVG filter at index {k} aucun filtre SVG à l’indice {k} +@msg no action set is current aucun ensemble d’actions n’est actif +@msg no active data set aucun jeu de données actif +@msg no appearance item {index} aucun élément d’aspect {index} +@msg no arrowheads file here aucun fichier de pointes de flèche ici +@msg no artboard {_1} aucun plan de travail {_1} +@msg no artboard {artboard} (0-based; the document has {_1}) aucun plan de travail {artboard} (à partir de 0 ; le document en compte {_1}) +@msg no asset {id} aucune ressource {id} +@msg no asset {id} (see assets.list) aucune ressource {id} (voir assets.list) +@msg no assets to export aucune ressource à exporter +@msg no assets yet: collect art for export first (assets.add) aucune ressource pour l’instant : rassemblez d’abord des illustrations à exporter (assets.add) +@msg no brush named `{n}` aucune forme nommée `{n}` +@msg no character at byte {start} aucun caractère à l’octet {start} +@msg no character has code {code} aucun caractère n’a le code {code} +@msg no colour aucune couleur +@msg no colour group `{group}` aucun groupe de couleurs `{group}` +@msg no colour group `{name}` aucun groupe de couleurs `{name}` +@msg no custom arrowhead {name} (see stroke.arrowheads.list) aucune pointe de flèche personnalisée {name} (voir stroke.arrowheads.list) +@msg no data sets aucun jeu de données +@msg no dataset named `{from}` aucun jeu de données nommé `{from}` +@msg no dataset named `{name}` aucun jeu de données nommé `{name}` +@msg no datasets aucun jeu de données +@msg no design named `{name}` aucun motif nommé `{name}` +@msg no design named `{n}` aucun motif nommé `{n}` +@msg no design named `{old}` aucun motif nommé `{old}` +@msg no encoder for {_1} yet pas encore d’encodeur pour {_1} +@msg no export preset `{name}` (see document.exportPresets.list) aucun paramètre prédéfini d’export `{name}` (voir document.exportPresets.list) +@msg no file to open aucun fichier à ouvrir +@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder aucun dossier : passez `path` ou définissez Préférences › Plug-ins et disques de travail › Dossier de plug-ins supplémentaire +@msg no fonts to look for: they are all available aucune police à rechercher : elles sont toutes disponibles +@msg no formats to export aucun format à exporter +@msg no gradient preset `{name}` (see gradient.presets.list) aucun paramètre prédéfini de dégradé `{name}` (voir gradient.presets.list) +@msg no graphic style `{n}` in `{_1}` aucun style graphique `{n}` dans `{_1}` +@msg no graphic style library `{key}` (see graphicStyle.libraries) aucune bibliothèque de styles graphiques `{key}` (voir graphicStyle.libraries) +@msg no guide selected aucun repère sélectionné +@msg no guide selected (or give an index) aucun repère sélectionné (ou indiquez un indice) +@msg no guide {i} aucun repère {i} +@msg no images in the folder aucune image dans le dossier +@msg no instructions aucune instruction +@msg no item at index {_1} aucun élément à l’indice {_1} +@msg no item at index {from} aucun élément à l’indice {from} +@msg no item at one of the indices {idx} aucun élément à l’un des indices {idx} +@msg no layer aucun calque +@msg no layer to rename aucun calque à renommer +@msg no layer {id} (see layer.list) aucun calque {id} (voir layer.list) +@msg no layers to export aucun calque à exporter +@msg no library `{key}` aucune bibliothèque `{key}` +@msg no metafile format aucun format de métafichier +@msg no object {_1} aucun objet {_1} +@msg no objects aucun objet +@msg no parent layer aucun calque parent +@msg no perspective grid preset named `{b}` (see perspective.presets.list) aucun paramètre prédéfini de grille de perspective nommé `{b}` (voir perspective.presets.list) +@msg no perspective grid preset named `{n}` aucun paramètre prédéfini de grille de perspective nommé `{n}` +@msg no point selected (give `index`) aucun point sélectionné (indiquez `index`) +@msg no preset called `{from}` (see file.newPresets) aucun paramètre prédéfini nommé `{from}` (voir file.newPresets) +@msg no preset called `{name}` (see file.newPresets) aucun paramètre prédéfini nommé `{name}` (voir file.newPresets) +@msg no preset named `{n}` aucun paramètre prédéfini nommé `{n}` +@msg no print preset named `{b}` (see print.presets.list) aucun paramètre prédéfini d’impression nommé `{b}` (voir print.presets.list) +@msg no print preset named `{name}` (presets: {_1}) aucun paramètre prédéfini d’impression nommé `{name}` (paramètres prédéfinis : {_1}) +@msg no print preset named `{n}` aucun paramètre prédéfini d’impression nommé `{n}` +@msg no profile named `{name}` aucun profil nommé `{name}` +@msg no random numbers ({e}) aucun nombre aléatoire ({e}) +@msg no readable files in the folder aucun fichier lisible dans le dossier +@msg no recovery copy `{f}` (see file.recovery.list) aucune copie de récupération `{f}` (voir file.recovery.list) +@msg no rows aucune ligne +@msg no saved PDF preset named `{name}` aucun paramètre prédéfini PDF enregistré nommé `{name}` +@msg no saved export preset `{name}` aucun paramètre prédéfini d’export enregistré `{name}` +@msg no saved gradient preset called {name} aucun paramètre prédéfini de dégradé enregistré nommé {name} +@msg no saved gradient presets aucun paramètre prédéfini de dégradé enregistré +@msg no saved preset called `{name}` aucun paramètre prédéfini enregistré nommé `{name}` +@msg no saved presets to export (name [Default] in `names` to export it) aucun paramètre prédéfini enregistré à exporter (indiquez [Default] dans `names` pour l’exporter) +@msg no saved presets to export (name built-in ones in `names`) aucun paramètre prédéfini enregistré à exporter (indiquez les paramètres intégrés dans `names`) +@msg no saved print preset named `{name}` aucun paramètre prédéfini d’impression enregistré nommé `{name}` +@msg no saved profile called {name} aucun profil enregistré nommé {name} +@msg no saved selection `{name}` aucune sélection enregistrée `{name}` +@msg no saved theme called {name} (see colorTheme.list) aucun thème enregistré nommé {name} (voir colorTheme.list) +@msg no saved views aucune vue enregistrée +@msg no selected or given object has a visible 3D effect aucun objet sélectionné ou indiqué n’a d’effet 3D visible +@msg no series {i} aucune série {i} +@msg no slice {n} in the image (see slice.list) aucune tranche {n} dans l’image (voir slice.list) +@msg no source art aucune illustration source +@msg no stop {i} (the gradient has {n}) aucun arrêt {i} (le dégradé en compte {n}) +@msg no style `{name}` aucun style `{name}` +@msg no such artboard {v} plan de travail {v} introuvable +@msg no such assets (see assets.list) ressources introuvables (voir assets.list) +@msg no such frame anchor, or the frame would reach past the canvas point d’ancrage de cadre introuvable, ou le cadre dépasserait la zone de travail +@msg no such objects objets introuvables +@msg no such path anchor, or the path would reach past the canvas point d’ancrage de tracé introuvable, ou le tracé dépasserait la zone de travail +@msg no such rows lignes introuvables +@msg no swatch `{keep}` aucune nuance `{keep}` +@msg no swatch `{n}` aucune nuance `{n}` +@msg no swatch `{n}` (swatches and colour groups move separately) aucune nuance `{n}` (les nuances et les groupes de couleurs se déplacent séparément) +@msg no swatch library `{key}` (see swatch.library.list) aucune bibliothèque de nuances `{key}` (voir swatch.library.list) +@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) aucune bibliothèque de nuances `{key}` (voir swatch.library.list ; "document" : les nuances du document) +@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) aucune bibliothèque de nuances `{key}` (voir swatch.library.list ; "{DOCUMENT_SWATCHES}" : les nuances du document) +@msg no swatch named `{n}` aucune nuance nommée `{n}` +@msg no swatch or colour group `{n}` aucune nuance ni aucun groupe de couleurs `{n}` +@msg no swatch or colour group `{n}` in `{_1}` aucune nuance ni aucun groupe de couleurs `{n}` dans `{_1}` +@msg no symbol named `{_1}` aucun symbole nommé `{_1}` +@msg no symbol named `{name}` aucun symbole nommé `{name}` +@msg no text objects selected aucun objet texte sélectionné +@msg no toolbar named `{_1}` aucune barre d’outils nommée `{_1}` +@msg no toolbar named `{name}` aucune barre d’outils nommée `{name}` +@msg no tracing preset `{preset}` (see imageTrace.presets) aucun paramètre prédéfini de vectorisation `{preset}` (voir imageTrace.presets) +@msg no user library folder here (save with a path, or without one to get the data) aucun dossier de bibliothèques utilisateur ici (enregistrez avec un chemin, ou sans chemin pour obtenir les données) +@msg no variable named `{from}` aucune variable nommée `{from}` +@msg no variable named `{name}` aucune variable nommée `{name}` +@msg no variable named `{var}` aucune variable nommée `{var}` +@msg no variable named `{v}` aucune variable nommée `{v}` +@msg no view named `{name}` aucune vue nommée `{name}` +@msg node {_1} already has the text variable `{_2}` le nœud {_1} a déjà la variable de texte `{_2}` +@msg node {_1} is not a graph le nœud {_1} n’est pas un graphe +@msg node {_1} is not an image le nœud {_1} n’est pas une image +@msg node {_1} is not type le nœud {_1} n’est pas du texte +@msg not a DCS file ce n’est pas un fichier DCS +@msg not a PDF presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis PDF (format `{_1}`) +@msg not a PDF presets file: {e} ce n’est pas un fichier de paramètres prédéfinis PDF : {e} +@msg not a RegExp object ce n’est pas un objet RegExp +@msg not a Word document: its WordDocument stream doesn't start with a file information block ce n’est pas un document Word : son flux WordDocument ne commence pas par un bloc d’informations de fichier +@msg not a Word document: its compound file has no WordDocument stream ce n’est pas un document Word : son fichier composé n’a pas de flux WordDocument +@msg not a Word document: its main part is not a WordprocessingML document ce n’est pas un document Word : sa partie principale n’est pas un document WordprocessingML +@msg not a Word document: its package has no main document part ce n’est pas un document Word : son package n’a pas de partie de document principale +@msg not a closed path ce n’est pas un tracé fermé +@msg not a flattener presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis d’aplatissement (format `{_1}`) +@msg not a flattener presets file: {e} ce n’est pas un fichier de paramètres prédéfinis d’aplatissement : {e} +@msg not a layer or group ce n’est ni un calque ni un groupe +@msg not a library file: {e} ce n’est pas un fichier de bibliothèque : {e} +@msg not a mesh envelope point ce n’est pas un point d’enveloppe de maillage +@msg not a perspective grid presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis de grille de perspective (format `{_1}`) +@msg not a perspective grid presets file: {e} ce n’est pas un fichier de paramètres prédéfinis de grille de perspective : {e} +@msg not a print presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis d’impression (format `{_1}`) +@msg not a print presets file: {e} ce n’est pas un fichier de paramètres prédéfinis d’impression : {e} +@msg not a regular expression ce n’est pas une expression régulière +@msg not a stored or deflated central record ce n’est pas un enregistrement central stocké ou compressé +@msg not a valid SVGZ file: {e} ce n’est pas un fichier SVGZ valide : {e} +@msg not an RTF file: it doesn't start with {\rtf ce n’est pas un fichier RTF : il ne commence pas par {\rtf +@msg not an action set: the text doesn't start with /version and a number ce n’est pas un ensemble d’actions : le texte ne commence pas par /version et un nombre +@msg not an image VectorCraft reads (see document.formats) ce n’est pas une image que VectorCraft lit (voir document.formats) +@msg not area type (text in a frame) outside perspective ce n’est pas un texte captif (texte dans un cadre) hors perspective +@msg not editing an opacity mask aucun masque d’opacité en cours de modification +@msg not type on a path outside perspective ce n’est pas un texte curviligne hors perspective +@msg nothing prints rien ne s’imprime +@msg nothing to bind rien à lier +@msg nothing to change rien à modifier +@msg nothing to change: give dx, dy, hScale, vScale or rotation rien à modifier : indiquez dx, dy, hScale, vScale ou rotation +@msg nothing to crop rien à recadrer +@msg nothing to export rien à exporter +@msg nothing to export: the document has no visible art rien à exporter : le document n’a aucune illustration visible +@msg nothing to mark rien à marquer +@msg nothing to print: every artboard picked is blank rien à imprimer : chaque plan de travail choisi est vide +@msg nothing to print: the document has no art that prints rien à imprimer : le document n’a aucune illustration imprimable +@msg nothing to repeat rien à répéter +@msg numbers out of order in {_1} quantifier nombres dans le désordre dans le quantificateur {_1} +@msg object is not a path l’objet n’est pas un tracé +@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object la transparence d’un objet ne peut pas être prélevée sur des caractères en surbrillance ; désactivez Transparence dans les options de la pipette ou sélectionnez l’objet entier +@msg object {_1} can't be set as point type l’objet {_1} ne peut pas être converti en texte de point +@msg object {_1} has lost its file's contents: relink it l’objet {_1} a perdu le contenu de son fichier : liez-le à nouveau +@msg object {_1} has no path l’objet {_1} n’a pas de tracé +@msg object {_1} has no size l’objet {_1} n’a pas de taille +@msg object {_1} is hidden or locked, or on a locked artboard l’objet {_1} est masqué ou verrouillé, ou sur un plan de travail verrouillé +@msg object {_1} is not a placed document l’objet {_1} n’est pas un document importé +@msg object {_1} is not an Intertwine group l’objet {_1} n’est pas un groupe Entrelacer +@msg object {_1} is not an image l’objet {_1} n’est pas une image +@msg object {_1} is not an image or a linked file l’objet {_1} n’est ni une image ni un fichier lié +@msg object {_1} is not an image or a placed document l’objet {_1} n’est ni une image ni un document importé +@msg object {_1} is not in this Intertwine group l’objet {_1} n’est pas dans ce groupe Entrelacer +@msg object {_1} needs a 3D effect l’objet {_1} nécessite un effet 3D +@msg object {_1} needs a 3D effect at index {index} l’objet {_1} nécessite un effet 3D à l’indice {index} +@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic objet {_1} : une extrusion et biseautage creuse n’a pas de fermetures : activez Fermeture pour placer un graphique +@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front objet {_1} : avec une profondeur de 0, l’objet n’a qu’une face : placez ses graphiques sur l’avant +@msg object {_1}: {_2} objet {_1} : {_2} +@msg object {_1}: {e} objet {_1} : {e} +@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams objet {_1} : {name} à l’indice {index} n’a pas de paramètres prédéfinis d’éclairage ; définissez ses éclairages avec effect.setParams +@msg object {id} has anchors 0–{_1} for `corners`, not {past} l’objet {id} a les points d’ancrage 0–{_1} pour `corners`, pas {past} +@msg object {id} has {n} anchors, so no anchor {past} l’objet {id} a {n} points d’ancrage, donc pas de point d’ancrage {past} +@msg objects must be none, move or copy objects doit valoir none, move ou copy +@msg objects were left out des objets ont été omis +@msg offset out of range décalage hors limites +@msg on must be true or false on doit valoir true ou false +@msg only groups, layers, type and images have a Contents (Characters) row seuls les groupes, calques, textes et images ont une ligne Contenu (Caractères) +@msg only interior mesh lines can be deleted seules les lignes de filet intérieures peuvent être supprimées +@msg only solid colours can be added (not gradients or patterns) seules les couleurs unies peuvent être ajoutées (pas les dégradés ni les motifs) +@msg only solid-colour swatches can be spot colours seules les nuances de couleur unie peuvent être des tons directs +@msg only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first seul l’aperçu incorporé de ce fichier Affinity a pu être lu : utilisez Fichier › Ouvrir pour l’afficher avec son avertissement, ou exportez d’abord en SVG ou PDF depuis Affinity +@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning seul l’aperçu incorporé de ce fichier Affinity a pu être lu ; utilisez Fichier › Ouvrir pour l’afficher avec son avertissement +@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon et un emplacement de colonne s’utilisent avec window.panel.float et window.panel.dock +@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon et un emplacement de colonne n’acceptent ni anchor, ni side, ni zone, ni before +@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon et un emplacement de colonne n’acceptent ni x, ni y, ni width, ni height +@msg opacity must be a number opacity doit être un nombre +@msg open a document to look for its linked files ouvrez un document pour rechercher ses fichiers liés +@msg operand too large opérande trop grand +@msg options must be an object (see file.formatOptions) options doit être un objet (voir file.formatOptions) +@msg order is a whole number from 0 to {others}, not `{v}` order est un nombre entier de 0 à {others}, pas `{v}` +@msg order must be leftToRight or rightToLeft order doit valoir leftToRight ou rightToLeft +@msg orientation must be portrait or landscape, not `{o}` orientation doit valoir portrait ou landscape, pas `{o}` +@msg outlineText {v}: true or false outlineText {v} : true ou false +@msg output intent profile: {e} profil de mode de sortie : {e} +@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination : aucun profil RVB ou CMJN ne s’appelle « {name} » +@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination : les fichiers {_1} sont en CMJN, et « {name} » est un profil RVB +@msg output.destination: {e} output.destination : {e} +@msg output.inks: no ink is set to print output.inks : aucune encre n’est réglée pour l’impression +@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent : aucun profil ne s’appelle « {name} », et un mode de sortie {std} incorpore son profil +@msg output.outputIntent: no profile is called “{name}”: a {std} output intent embeds a profile, or names a registered printing condition (outputConditionId and registry) output.outputIntent : aucun profil ne s’appelle « {name} » : un mode de sortie {std} incorpore un profil, ou désigne une condition d’impression enregistrée (outputConditionId et registry) +@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent : les fichiers {std} s’impriment en CMJN, et « {name} » est un profil RVB +@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent : « {name} » est un profil de niveaux de gris, qui ne peut pas être un mode de sortie {std} +@msg overlap: {o} pt is half a tile or more chevauchement : {o} pt représente la moitié d’une mosaïque ou plus +@msg overlapping Shaper paths must be in the same layer or group les tracés Shaper qui se chevauchent doivent être dans le même calque ou groupe +@msg overlapping edits modifications qui se chevauchent +@msg page art conflicts with image data for {key} l’illustration de la page est en conflit avec les données d’image de {key} +@msg page art conflicts with pattern {name} l’illustration de la page est en conflit avec le motif {name} +@msg page art conflicts with swatch {name} l’illustration de la page est en conflit avec la nuance {name} +@msg page art is missing image data for {key} il manque à l’illustration de la page les données d’image de {key} +@msg page art is missing pattern {name} il manque à l’illustration de la page le motif {name} +@msg page art is missing swatch {name} il manque à l’illustration de la page la nuance {name} +@msg page must be a whole number from 1 page doit être un nombre entier à partir de 1 +@msg page thumbnail: {e} vignette de page : {e} +@msg page {page} of the PDF has no objects la page {page} du PDF n’a aucun objet +@msg page {page}: `{file}` has {n} {artboards} page {page} : `{file}` contient {n} {artboards} +@msg page {page}: `{name}` has {n} {pages} page {page} : `{name}` contient {n} {pages} +@msg pages must be a range such as "2-3" or a page number, not {v} pages doit être une plage comme "2-3" ou un numéro de page, pas {v} +@msg pages: {e} pages : {e} +@msg panel position must be finite la position du panneau doit être finie +@msg parameters must be an object parameters doit être un objet +@msg params must be an object params doit être un objet +@msg passwords can have up to {MAX_PASSWORD} bytes les mots de passe peuvent compter jusqu’à {MAX_PASSWORD} octets +@msg path chunks must share their boundary point les fragments de tracé doivent partager leur point de jonction +@msg path has no open end le tracé n’a pas d’extrémité ouverte +@msg paths must be strings paths doit contenir des chaînes +@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false nécessite que chaque plan de travail conserve l’illustration modifiable +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false nécessite que les plans de travail conservent l’illustration modifiable quand useArtboards vaut false +@msg pdfs `{m}`: single or multiple pdfs `{m}` : single ou multiple +@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom vaut "artwork" ou "swatches", pas {v} +@msg pins and moved must be non-empty lists of the same length pins et moved doivent être des listes non vides de même longueur +@msg pins must be [[x,y]…] pins doit être [[x,y]…] +@msg pins must be finite pins doit être fini +@msg place is above, below or inside, not `{other}` place vaut above, below ou inside, pas `{other}` +@msg plane must be left|right|ground plane doit valoir left|right|ground +@msg plane must be left|right|ground|none plane doit valoir left|right|ground|none +@msg point is outside the mesh le point est en dehors du filet +@msg point x must be a number le x du point doit être un nombre +@msg point y must be a number le y du point doit être un nombre +@msg points must be [t, left, right] with non-negative widths points doit être [t, left, right] avec des largeurs positives ou nulles +@msg points must be [x,y] pairs points doit être des paires [x,y] +@msg points must be a non-empty [[x,y,pressure?]…] list points doit être une liste [[x,y,pressure?]…] non vide +@msg points must be a non-empty list of finite [x,y] points doit être une liste non vide de [x,y] finis +@msg points must be an array or null points doit être une liste ou null +@msg points must be finite points doit être fini +@msg points must be finite [x,y] pairs points doit être des paires [x,y] finies +@msg points must be finite canvas points points doit être des points finis de la zone de travail +@msg points must contain 1–4096 coordinates points doit contenir de 1 à 4096 coordonnées +@msg position must be normal, superscript or subscript position doit valoir normal, superscript ou subscript +@msg position must be topLeft, topRight, bottomLeft or bottomRight position doit valoir topLeft, topRight, bottomLeft ou bottomRight +@msg positions must be between 0 and 100000 pt les positions doivent être comprises entre 0 et 100000 pt +@msg ppi must be a positive number, not {ppi} ppi doit être un nombre positif, pas {ppi} +@msg ppi must be between 1 and 2400 ppi doit être compris entre 1 et 2400 +@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}` n’est pas transforms|bounds|fileDimensions|fit|fill +@msg preserveShape must be clippingMask|transparency preserveShape doit valoir clippingMask|transparency +@msg preset `{id}`: mobile or density preset `{id}` : mobile ou density +@msg preset must be standard, diffuse, topLeft or right preset doit valoir standard, diffuse, topLeft ou right +@msg preset must be the name of a print preset preset doit être le nom d’un paramètre prédéfini d’impression +@msg previewMode must be default, pixel or overprint previewMode doit valoir default, pixel ou overprint +@msg print settings: {e} réglages d’impression : {e} +@msg procedures nest too deeply les procédures sont imbriquées trop profondément +@msg procedures nested too deeply procédures imbriquées trop profondément +@msg quotes must be {double?, single?} quotes doit être {double?, single?} +@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k} doit être un guillemet ouvrant et un guillemet fermant, par exemple "“”" +@msg range out of order in character class plage dans le désordre dans une classe de caractères +@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi doit être compris entre 1 et 2400 (72 écran, 150 moyenne, 300 haute) +@msg rating must be a whole number from 0 to {MAX_RATING} rating doit être un nombre entier compris entre 0 et {MAX_RATING} +@msg rect and {k} contradict each other: give the area or the artboards rect et {k} se contredisent : indiquez la zone ou les plans de travail +@msg rect applies to raster formats, not {_1} rect s’applique aux formats pixellisés, pas à {_1} +@msg rect is [x, y, width, height] in points rect est [x, y, width, height] en points +@msg rect must be [x, y, width, height] rect doit être [x, y, width, height] +@msg rect must be [x, y, width, height] with a positive width and height rect doit être [x, y, width, height] avec une largeur et une hauteur positives +@msg rect needs finite numbers and a positive width and height rect nécessite des nombres finis et une largeur et une hauteur positives +@msg redraw needs id redraw nécessite id +@msg redraw within must be a finite distance of 0 or more within de redraw doit être une distance finie supérieure ou égale à 0 +@msg reduce of an empty array with no initial value reduce d’un tableau vide sans valeur initiale +@msg registration must be 0..8 registration doit être compris entre 0 et 8 +@msg regular expression flag '{_1}' given twice indicateur d’expression régulière '{_1}' fourni deux fois +@msg release a layer or a group libérez un calque ou un groupe +@msg replace keeps the replaced object's place and transform: drop template, at and rect replace conserve la position et la transformation de l’objet remplacé : retirez template, at et rect +@msg replace: select the one object to replace replace : sélectionnez le seul objet à remplacer +@msg resolution must be a number or screen|medium|high resolution doit être un nombre ou screen|medium|high +@msg resolution must be between 1 and 2400 ppi la résolution doit être comprise entre 1 et 2400 ppp +@msg rotation must be a number of degrees rotation doit être un nombre de degrés +@msg rotation must be finite rotation doit être finie +@msg rows and cols must be 1..50 rows et cols doivent être compris entre 1 et 50 +@msg rows and columns must be 1..500 rows et columns doivent être compris entre 1 et 500 +@msg rows of objects whose appearances differ can't be reordered together les lignes d’objets dont l’aspect diffère ne peuvent pas être réordonnées ensemble +@msg sampled text would exceed the canvas (coordinates out of range) le texte prélevé dépasserait la zone de travail (coordonnées hors limites) +@msg save the document first: Package collects a saved document enregistrez d’abord le document : Assemblage rassemble un document enregistré +@msg save the document first: copy puts the file in its folder enregistrez d’abord le document : copy place le fichier dans son dossier +@msg savedPreset: {e} savedPreset : {e} +@msg scale `{s}`: a factor such as 2x, a width (100w), a height (100h) or a resolution (72ppi) échelle `{s}` : un facteur comme 2x, une largeur (100w), une hauteur (100h) ou une résolution (72ppi) +@msg scale must be a number above 0, at most 64 scale doit être un nombre supérieur à 0, d’au plus 64 +@msg scale must be non-zero scale doit être non nul +@msg scope must be selection or all (got {other}) scope doit valoir selection ou all (reçu : {other}) +@msg scribble is too long le gribouillis est trop long +@msg segment does not exist le segment n’existe pas +@msg select a Shaper group sélectionnez un groupe Shaper +@msg select a blend to set easing or color shift sélectionnez un dégradé de formes pour régler l’accélération ou le décalage de couleur +@msg select a compound shape sélectionnez une forme composée +@msg select a graph sélectionnez un graphe +@msg select a group or layer with visible, unlocked members sélectionnez un groupe ou un calque dont les membres sont visibles et déverrouillés +@msg select a layer sélectionnez un calque +@msg select a text object sélectionnez un objet texte +@msg select an SVG filter sélectionnez un filtre SVG +@msg select an image (or an Image Trace group) sélectionnez une image (ou un groupe de vectorisation de l’image) +@msg select an image, or pass id sélectionnez une image, ou passez id +@msg select an object (or pass `id`) sélectionnez un objet (ou passez `id`) +@msg select an object with a 3D effect sélectionnez un objet doté d’un effet 3D +@msg select an object with a 3D effect, or give its id in ids sélectionnez un objet doté d’un effet 3D, ou indiquez son identifiant dans ids +@msg select an object with a freeform gradient sélectionnez un objet doté d’un dégradé de forme libre +@msg select an object with an opacity mask sélectionnez un objet doté d’un masque d’opacité +@msg select area type sélectionnez du texte captif +@msg select area type (text in a frame) sélectionnez du texte captif (texte dans un cadre) +@msg select area type and closed paths (at least one area type object) sélectionnez du texte captif et des tracés fermés (au moins un objet texte captif) +@msg select area type whose first line can fit its frame sélectionnez du texte captif dont la première ligne tient dans son cadre +@msg select artwork to add its colours sélectionnez l’illustration dont ajouter les couleurs +@msg select artwork to make a group of its colours sélectionnez l’illustration dont créer un groupe de couleurs +@msg select artwork to recolour, or give a colour `group` sélectionnez l’illustration dont redéfinir les couleurs, ou indiquez un groupe de couleurs `group` +@msg select exactly two path ends to join sélectionnez exactement deux extrémités de tracé à joindre +@msg select members of a compound shape sélectionnez des membres d’une forme composée +@msg select objects or give ids sélectionnez des objets ou indiquez ids +@msg select objects to align to the guide sélectionnez les objets à aligner sur le repère +@msg select one Intertwine group sélectionnez un seul groupe Entrelacer +@msg select one image or pass id sélectionnez une seule image ou passez id +@msg select one layer, group or object to rename sélectionnez un seul calque, groupe ou objet à renommer +@msg select paths sélectionnez des tracés +@msg select paths or compound paths sélectionnez des tracés ou des tracés transparents +@msg select rows or objects to collect sélectionnez les lignes ou objets à rassembler +@msg select something to export sélectionnez quelque chose à exporter +@msg select something to export (or turn off selectedOnly) sélectionnez quelque chose à exporter (ou désactivez selectedOnly) +@msg select text to redefine the style from sélectionnez le texte à partir duquel redéfinir le style +@msg select the art and, on top of it, the mask object sélectionnez l’illustration et, au-dessus, l’objet de masque +@msg select the art for the design sélectionnez l’illustration du motif +@msg select the art for the pattern tile sélectionnez l’illustration du carreau de motif +@msg select the art to add first sélectionnez d’abord l’illustration à ajouter +@msg select the art to collect (or give its ids) sélectionnez l’illustration à rassembler (ou indiquez ses identifiants) +@msg select the art to weed sélectionnez l’illustration à écheniller +@msg select the content and a path on top sélectionnez le contenu et un tracé au-dessus +@msg select the images to relink, or pass ids sélectionnez les images à lier à nouveau, ou passez ids +@msg select the images, or pass ids sélectionnez les images, ou passez ids +@msg select the linked images or placed documents, or pass ids sélectionnez les images liées ou les documents importés, ou passez ids +@msg select the object to copy from sélectionnez l’objet à partir duquel copier +@msg select the objects (or give their ids) sélectionnez les objets (ou indiquez leurs identifiants) +@msg select the objects to attach to the path sélectionnez les objets à attacher au tracé +@msg select the objects to bind sélectionnez les objets à lier +@msg select two or more objects to intertwine sélectionnez au moins deux objets à entrelacer +@msg select two or more slices to combine sélectionnez au moins deux tranches à combiner +@msg select type on a path sélectionnez du texte curviligne +@msg select type on a path or area type sélectionnez du texte curviligne ou captif +@msg selection `{m}`: one or each selection `{m}` : one ou each +@msg settings is an object of document.exportForScreens params settings est un objet de paramètres de document.exportForScreens +@msg settings must be an object settings doit être un objet +@msg show `{show}`: all, missing, modified or embedded show `{show}` : all, missing, modified ou embedded +@msg show must be true or false show doit valoir true ou false +@msg side is left or right, not `{v}` side vaut left ou right, pas `{v}` +@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size doit valoir "small", "medium", "large" ou un nombre de points supérieur à 0 et d’au plus {_1} +@msg size must be positive size doit être positif +@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode vaut "fixed" ou "pressure", pas `{other}` +@msg sizeMode is "fixed" or "pressure", not {other} sizeMode vaut "fixed" ou "pressure", pas {other} +@msg sizeVariation must be 0 or more sizeVariation doit être supérieur ou égal à 0 +@msg smallCaps must be true or false smallCaps doit valoir true ou false +@msg sort `{x}`: name, kind or status sort `{x}` : name, kind ou status +@msg source paths are too large for Shaper les tracés source sont trop grands pour Shaper +@msg source text transform is collapsed or unrepresentable la transformation du texte source est écrasée ou impossible à représenter +@msg spot, mode and group apply to solid colours only spot, mode et group s’appliquent uniquement aux couleurs unies +@msg status `{s}`: linked, embedded, missing or modified status `{s}` : linked, embedded, missing ou modified +@msg steps must be 1..1000 steps doit être compris entre 1 et 1000 +@msg steps must be 1..{MAX_STEPS} steps doit être compris entre 1 et {MAX_STEPS} +@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) arrêt {i} : `plates` doit être quatre nombres compris entre 0 et 1 (C, M, Y, K) +@msg stopped before the archive was finished arrêté avant la fin de l’archive +@msg strokeMode must be within|along|across, got {m} strokeMode doit valoir within|along|across, reçu : {m} +@msg strokeOptions must be an object of stroke.set options strokeOptions doit être un objet d’options de stroke.set +@msg strokeWidth must be a positive number of pixels strokeWidth doit être un nombre positif de pixels +@msg subpath {si} is not an open subpath le sous-tracé {si} n’est pas un sous-tracé ouvert +@msg subpaths required when joining the same path subpaths est obligatoire pour joindre un même tracé +@msg swatchConflict must be "merge" or "add", not {v} swatchConflict doit valoir "merge" ou "add", pas {v} +@msg table must be true or false table doit valoir true ou false +@msg takes one name accepte un seul nom +@msg target text transform is collapsed or unrepresentable la transformation du texte cible est écrasée ou impossible à représenter +@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text doit être {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} +@msg text transform is collapsed or unrepresentable la transformation du texte est écrasée ou impossible à représenter +@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces doit être un nombre entier compris entre 2 et 100 +@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}` : utilisez "{_1}" ou "{_2}" +@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage : indiquez {path} ou {name, dataBase64} d’un fichier image, ou une texture telle que la donne effect.list +@msg textureImage: {e} textureImage : {e} +@msg that makes more than {MAX_PIECES} slices cela produit plus de {MAX_PIECES} tranches +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer cela produit trop de morceaux (au plus 100000) ou d’étapes de tirets : simplifiez le motif de tirets ou allongez les tirets +@msg that object is not in the overlap cet objet n’est pas dans le chevauchement +@msg the Affinity document has no page with a size le document Affinity n’a aucune page dotée d’une taille +@msg the CMYK image couldn't be written l’image CMJN n’a pas pu être écrite +@msg the Layers panel is not shown le panneau Calques n’est pas affiché +@msg the Layers panel's search, filter or Show Layers Only hides its row la recherche, le filtre ou l’option Afficher les calques uniquement du panneau Calques masque sa ligne +@msg the Objects on Path group is hidden or locked le groupe Objets sur tracé est masqué ou verrouillé +@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected les réglages de sécurité PDF ont été enregistrés sans leurs mots de passe : indiquez à nouveau openPassword ou permissionsPassword, ou security sans passwordRequired pour exporter sans protection +@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) le fichier RTF a trop de paragraphes ou de changements de mise en forme pour être importé comme texte (au plus {MAX_PARAGRAPHS} paragraphes et {MAX_RUNS} segments) +@msg the RTF file is too large to place as type (at most {_1} MB) le fichier RTF est trop volumineux pour être importé comme texte (au plus {_1} Mo) +@msg the SVG expands to more than {0} nested SVG images le SVG se développe en plus de {0} images SVG imbriquées +@msg the SVG expands to more than {0} objects le SVG se développe en plus de {0} objets +@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes le fichier SVG dépasse {MAX_IMPORT_BYTES} octets +@msg the SVG has no drawable objects le SVG n’a aucun objet dessinable +@msg the SVGZ file unpacks to more than {_1} MB le fichier SVGZ se décompresse en plus de {_1} Mo +@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place le document Word a plus de {MAX_PAGES} pages de propriétés, trop pour être importé +@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place le document Word a plus de {MAX_RUNS} segments de propriétés de caractère ou de paragraphe, trop pour être importé +@msg the Word document has more than {_1} MB of formatting properties, too many to place le document Word a plus de {_1} Mo de propriétés de mise en forme, trop pour être importé +@msg the Word document has too much text or formatting to place as type (at most {MAX_PARAGRAPHS} paragraphs, {MAX_RUNS} runs and {_1} MB of text) le document Word a trop de texte ou de mise en forme pour être importé comme texte (au plus {MAX_PARAGRAPHS} paragraphes, {MAX_RUNS} segments et {_1} Mo de texte) +@msg the Word document has too much text to place as type (at most {_1} MB) le document Word a trop de texte pour être importé comme texte (au plus {_1} Mo) +@msg the Word document is encrypted or protected with a password; save it without one to place it le document Word est chiffré ou protégé par un mot de passe ; enregistrez-le sans mot de passe pour l’importer +@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) le document Word est trop volumineux pour être importé (son flux {name} contient plus de {_1} Mo) +@msg the Word document was last saved with fast save, which VectorCraft doesn't read; save it again with fast save turned off le document Word a été enregistré en dernier avec l’enregistrement rapide, que VectorCraft ne lit pas ; enregistrez-le à nouveau avec l’enregistrement rapide désactivé +@msg the Word document was saved by Word 6.0 or Word 95, which VectorCraft doesn't read; save it as a Word 97-2003 document le document Word a été enregistré par Word 6.0 ou Word 95, que VectorCraft ne lit pas ; enregistrez-le en tant que document Word 97-2003 +@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place le texte du document Word se compose de plus de {MAX_PIECES} morceaux, trop pour être importé +@msg the XML operation needs an XML or XMLList value l’opération XML nécessite une valeur XML ou XMLList +@msg the XPath expression does not evaluate to a node-set l’expression XPath ne donne pas un ensemble de nœuds +@msg the XPath expression ends early l’expression XPath se termine trop tôt +@msg the XPath expression has an unmatched ) l’expression XPath a une ) sans correspondance +@msg the XPath expression has an unmatched ] l’expression XPath a un ] sans correspondance +@msg the XPath expression has an unmatched closing bracket l’expression XPath a un crochet fermant sans correspondance +@msg the XPath expression selects a namespace node, which is not an XML value l’expression XPath sélectionne un nœud d’espace de noms, qui n’est pas une valeur XML +@msg the XPath expression selects the root node, which is not an XML value l’expression XPath sélectionne le nœud racine, qui n’est pas une valeur XML +@msg the XPath function {name} needs a node-set la fonction XPath {name} nécessite un ensemble de nœuds +@msg the XPath function {name} takes the wrong number of arguments la fonction XPath {name} reçoit un nombre d’arguments incorrect +@msg the XPath name functions need a node-set les fonctions de nom XPath nécessitent un ensemble de nœuds +@msg the XPath prefix {shown} is not bound to a namespace le préfixe XPath {shown} n’est lié à aucun espace de noms +@msg the XPath variable ${shown} is not bound la variable XPath ${shown} n’est pas liée +@msg the action set ids are not a list les identifiants des ensembles d’actions ne sont pas une liste +@msg the active paint is not a gradient la peinture active n’est pas un dégradé +@msg the argument list is not an object la liste d’arguments n’est pas un objet +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size les plans de travail ne tiennent pas dans la zone de travail : réduisez leur nombre, l’espacement ou la taille +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out l’illustration est trop volumineuse pour l’EPS : ce qui suit 256 Mo de PostScript a été omis +@msg the bitmaps hold more pixels than one file may les images bitmap contiennent plus de pixels qu’un fichier ne peut en contenir +@msg the character's size can't be measured la taille du caractère ne peut pas être mesurée +@msg the code runs past its last instruction le code s’exécute au-delà de sa dernière instruction +@msg the compound shape is too complex to expand la forme composée est trop complexe pour être décomposée +@msg the container of the listed objects is gone le conteneur des objets listés a disparu +@msg the copy `{user_id}` can't be read as a library la copie `{user_id}` ne peut pas être lue comme une bibliothèque +@msg the copy would leave the canvas la copie sortirait de la zone de travail +@msg the curve tolerance must be from 0.01 to 10 points la tolérance de courbe doit être comprise entre 0.01 et 10 points +@msg the custom dictionary couldn't be saved to {_1}: {e} le dictionnaire personnalisé n’a pas pu être enregistré dans {_1} : {e} +@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is le dictionnaire personnalisé {_1} ne peut pas être lu ({why}), il est donc laissé tel quel +@msg the cut cannot be made on this art: {e} la coupe ne peut pas être réalisée sur cette illustration : {e} +@msg the data isn't an EMF or WMF picture les données ne sont pas une image EMF ou WMF +@msg the data isn't an EPS or PostScript picture les données ne sont pas une image EPS ou PostScript +@msg the deferred paste is no longer available le collage différé n’est plus disponible +@msg the design is too big to draw at every data point of this graph le motif est trop grand pour être dessiné à chaque point de données de ce graphe +@msg the design is too big to draw in every column of this graph le motif est trop grand pour être dessiné dans chaque colonne de ce graphe +@msg the dialog it was opened over is no longer open la boîte de dialogue sur laquelle il a été ouvert n’est plus ouverte +@msg the dictionary stack overflowed la pile des dictionnaires a débordé +@msg the document already has an SVG filter named {name} le document a déjà un filtre SVG nommé {name} +@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it le document a un autre filtre SVG nommé {name} : utilisez svgFilter.apply, ou svgFilter.define avec `replace` pour le modifier +@msg the document has no SVG filter named {name} le document n’a aucun filtre SVG nommé {name} +@msg the document has no SVG filter named {r} le document n’a aucun filtre SVG nommé {r} +@msg the document has no color swatches le document n’a aucune nuance de couleur +@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 les nuances du document contiennent {_1} couleurs ; la réduction au document en accepte au plus 255 +@msg the editing data's hash has no place l’empreinte des données d’édition n’a pas d’emplacement +@msg the entities the document declares expand to too much text les entités déclarées par le document se développent en trop de texte +@msg the envelope's content has no bounds le contenu de l’enveloppe n’a pas de limites +@msg the envelope's mesh is malformed le filet de l’enveloppe est mal formé +@msg the file holds no `variableSets` element le fichier ne contient aucun élément `variableSets` +@msg the file is larger than {_1} MB le fichier dépasse {_1} Mo +@msg the file is too large le fichier est trop volumineux +@msg the file isn't valid SVG: {e} le fichier n’est pas un SVG valide : {e} +@msg the file names no plate le fichier ne désigne aucune plaque +@msg the file was updated or encrypted already le fichier a déjà été mis à jour ou chiffré +@msg the first character of a line has no character before it to kern: it moves only up and down (dy) le premier caractère d’une ligne n’a pas de caractère avant lui avec lequel créer un crénage : il se déplace uniquement vers le haut et le bas (dy) +@msg the font {_1} is not available la police {_1} n’est pas disponible +@msg the format has no file extension le format n’a pas d’extension de fichier +@msg the function's prototype is not an object le prototype de la fonction n’est pas un objet +@msg the graphic can't be read: {e} le graphique ne peut pas être lu : {e} +@msg the graphic is empty le graphique est vide +@msg the graphic's data is damaged les données du graphique sont endommagées +@msg the graphic's document has no artboard le document du graphique n’a pas de plan de travail +@msg the guide is hidden or locked le repère est masqué ou verrouillé +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them l’en-tête `{cell}` de `{name}` serait relu comme la variable `{other}` : renommez l’une des deux +@msg the image `{path}` isn't followed: {r} l’image `{path}` n’est pas suivie : {r} +@msg the image has no embedded pixels l’image n’a pas de pixels incorporés +@msg the image is empty l’image est vide +@msg the image is fully transparent l’image est entièrement transparente +@msg the image is not on an artboard; pass rect l’image n’est pas sur un plan de travail ; passez rect +@msg the image's pixels are missing les pixels de l’image sont manquants +@msg the image's pixels aren't in the document les pixels de l’image ne sont pas dans le document +@msg the image's pixels can't be read: {e} les pixels de l’image ne peuvent pas être lus : {e} +@msg the image's transparency doesn't fit it la transparence de l’image ne lui correspond pas +@msg the job would print more than {MAX_PAGES} pages la tâche imprimerait plus de {MAX_PAGES} pages +@msg the job would print more than {_1} pages la tâche imprimerait plus de {_1} pages +@msg the layers hold no art to mask les calques ne contiennent aucune illustration à masquer +@msg the layout moved la disposition a changé +@msg the library has a group `{name}` already la bibliothèque a déjà un groupe `{name}` +@msg the library has no group `{name}` la bibliothèque n’a pas de groupe `{name}` +@msg the library has no item `{item}` la bibliothèque n’a pas d’élément `{item}` +@msg the library has no item `{other}` la bibliothèque n’a pas d’élément `{other}` +@msg the library holds {MAX_GROUPS} groups already la bibliothèque contient déjà {MAX_GROUPS} groupes +@msg the library holds {MAX_ITEMS} items of this kind already la bibliothèque contient déjà {MAX_ITEMS} éléments de ce type +@msg the library's file is outside the read roots le fichier de la bibliothèque est en dehors des racines de lecture +@msg the method needs an XML value la méthode nécessite une valeur XML +@msg the name is empty le nom est vide +@msg the object has no fill, stroke or effect l’objet n’a ni fond, ni contour, ni effet +@msg the object has no graphic style (pass `name`) l’objet n’a pas de style graphique (passez `name`) +@msg the objects don't match the plan les objets ne correspondent pas au plan +@msg the open and permissions passwords must differ les mots de passe d’ouverture et d’autorisations doivent être différents +@msg the operand stack overflowed la pile des opérandes a débordé +@msg the page has no area la page n’a pas de surface +@msg the page's type in an unread object's box isn't in its page art le texte de la page dans le cadre d’un objet non lu n’est pas dans l’illustration de sa page +@msg the paint behind the proxy is not a gradient la peinture derrière l’échantillon n’est pas un dégradé +@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) la peinture n’est pas un dégradé de forme libre (appliquez-en un avec paint.editGradient {kind}) +@msg the parameters or body do not form one function les paramètres ou le corps ne forment pas une seule fonction +@msg the parent must be a layer or sublayer le parent doit être un calque ou un sous-calque +@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it le filtre SVG collé {name} dessine autrement que le filtre du document qui porte ce nom, et aucun nom libre ne lui convient +@msg the path can't be extended le tracé ne peut pas être prolongé +@msg the path can't be inside one of the objects to attach le tracé ne peut pas être à l’intérieur de l’un des objets à attacher +@msg the path has no segments to measure le tracé n’a aucun segment à mesurer +@msg the path is empty le tracé est vide +@msg the path is too short to shorten that much le tracé est trop court pour être raccourci autant +@msg the pen table holds more than 255 pens la table des plumes contient plus de 255 plumes +@msg the picture has no objects l’image n’a aucun objet +@msg the plates can't be kept les plaques ne peuvent pas être conservées +@msg the position table does not match the instructions la table des positions ne correspond pas aux instructions +@msg the program runs too long le programme s’exécute trop longtemps +@msg the program uses too much memory le programme utilise trop de mémoire +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes le texte de l’invite dépasse {MAX_PROMPT_BYTES} octets +@msg the radix must be between 2 and 36 la base doit être comprise entre 2 et 36 +@msg the rectangle must have a width and a height le rectangle doit avoir une largeur et une hauteur +@msg the rectangle needs a size and must lie on the canvas le rectangle nécessite une taille et doit se trouver dans la zone de travail +@msg the right side of 'in' is not an object le côté droit de 'in' n’est pas un objet +@msg the right side of 'instanceof' is not a function le côté droit de 'instanceof' n’est pas une fonction +@msg the selected art has no width l’illustration sélectionnée n’a pas de largeur +@msg the selected art is nested more than {MAX_DEPTH} levels deep l’illustration sélectionnée est imbriquée sur plus de {MAX_DEPTH} niveaux +@msg the selected art is too large to measure l’illustration sélectionnée est trop grande pour être mesurée +@msg the selection draws nothing la sélection ne dessine rien +@msg the selection has no area la sélection n’a pas de surface +@msg the selection has no art to export la sélection ne contient aucune illustration à exporter +@msg the selection has no bounds la sélection n’a pas de limites +@msg the selection has no graphic style la sélection n’a pas de style graphique +@msg the selection has no outlines to weed around la sélection n’a pas de contours autour desquels écheniller +@msg the slice would leave the canvas la tranche sortirait de la zone de travail +@msg the spot colour couldn't be added le ton direct n’a pas pu être ajouté +@msg the subpath has no ends to extend (it is closed or a single point) le sous-tracé n’a pas d’extrémités à prolonger (il est fermé ou réduit à un point) +@msg the symbols are nested too deeply to expand their instances les symboles sont imbriqués trop profondément pour décomposer leurs occurrences +@msg the text file is too large to place as type (at most {_1} MB) le fichier texte est trop volumineux pour être importé comme texte (au plus {_1} Mo) +@msg the text is empty le texte est vide +@msg the text is over {_1} MB le texte dépasse {_1} Mo +@msg the top object must be a path l’objet du dessus doit être un tracé +@msg the trace was cancelled la vectorisation a été annulée +@msg the trailer has no size le trailer n’a pas de taille +@msg the type object is flattened to nothing l’objet texte est aplati à néant +@msg the value bind was called on is not a function la valeur sur laquelle bind a été appelé n’est pas une fonction +@msg the value is not a constructor la valeur n’est pas un constructeur +@msg the value is not a function la valeur n’est pas une fonction +@msg the value is not a valid XML name la valeur n’est pas un nom XML valide +@msg the value is not an XML attribute name la valeur n’est pas un nom d’attribut XML +@msg the value is not an XML name la valeur n’est pas un nom XML +@msg the value of axis `{tag}` must be a number or null la valeur de l’axe `{tag}` doit être un nombre ou null +@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets la bibliothèque de variables déclare plus de {MAX_LIBRARY_DATASETS} jeux de données +@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities la bibliothèque de variables déclare plus de {MAX_LIBRARY_ENTITIES} entités +@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables la bibliothèque de variables déclare plus de {MAX_LIBRARY_VARIABLES} variables +@msg the variable library declares no variables la bibliothèque de variables ne déclare aucune variable +@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes la bibliothèque de variables dépasse {MAX_LIBRARY_BYTES} octets +@msg the variable library is not UTF-8 la bibliothèque de variables n’est pas en UTF-8 +@msg the variable library is not well-formed XML: {e} la bibliothèque de variables n’est pas du XML bien formé : {e} +@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep la bibliothèque de variables imbrique des éléments sur plus de {MAX_LIBRARY_NESTING} niveaux +@msg the variable library's document type declaration holds something other than entities of plain text la déclaration de type de document de la bibliothèque de variables contient autre chose que des entités de texte brut +@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes les entités de la bibliothèque de variables se développent en plus de {MAX_LIBRARY_BYTES} octets +@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines les lignes d’échenillage croisent les contours de l’illustration trop de fois pour être mesurées ; utilisez moins de lignes +@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap les lignes d’échenillage passent trop près de trop de segments de contour pour être mesurées ; utilisez moins de lignes ou un écart plus petit +@msg the weeding lines would be more than {max_pieces} pieces les lignes d’échenillage feraient plus de {max_pieces} morceaux +@msg the window has no dock la fenêtre n’a pas de zone d’ancrage +@msg the written PDF has no catalog le PDF écrit n’a pas de catalogue +@msg the written PDF has no cross-reference table le PDF écrit n’a pas de table de références croisées +@msg the written PDF's metadata can't be edited les métadonnées du PDF écrit ne peuvent pas être modifiées +@msg the {_1} plate lies outside the file la plaque {_1} se trouve en dehors du fichier +@msg there are no colour swatches to draw il n’y a aucune nuance de couleur à dessiner +@msg there is already a design named `{name}` il existe déjà un motif nommé `{name}` +@msg there is already a design named `{to}` il existe déjà un motif nommé `{to}` +@msg there is already a toolbar named `{name}` il existe déjà une barre d’outils nommée `{name}` +@msg there is already a toolbar named `{to}` il existe déjà une barre d’outils nommée `{to}` +@msg there is no library: create one first il n’y a aucune bibliothèque : créez-en d’abord une +@msg these paths are too degenerate for Shaper ces tracés sont trop dégénérés pour Shaper +@msg this is a binary DXF file, which can't be read: save it from the CAD app as an ASCII DXF and open that ceci est un fichier DXF binaire, qui ne peut pas être lu : enregistrez-le depuis l’application de CAO en DXF ASCII et ouvrez ce dernier +@msg this warp's points can't be edited: use Reset with Mesh first les points de cette déformation ne peuvent pas être modifiés : utilisez d’abord Réinitialiser avec un filet +@msg thumbnail: no page {v} (the {what} has {count}) vignette : aucune page {v} (le {what} en compte {count}) +@msg tileRange of {what}: {e} tileRange de {what} : {e} +@msg title can't be empty title ne peut pas être vide +@msg to (the new name) is required to (le nouveau nom) est obligatoire +@msg to must be fill or stroke to doit valoir fill ou stroke +@msg to must be smooth or corner to doit valoir smooth ou corner +@msg toExponential() digits must be between 0 and 20 les chiffres de toExponential() doivent être compris entre 0 et 20 +@msg toFixed() digits must be between 0 and 20 les chiffres de toFixed() doivent être compris entre 0 et 20 +@msg toISOString is not a function toISOString n’est pas une fonction +@msg toPrecision() argument must be between 1 and 21 l’argument de toPrecision() doit être compris entre 1 et 21 +@msg toSource: a getter cannot be written as source toSource : un getter ne peut pas être écrit sous forme de source +@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource : un tableau doté de propriétés autres que ses éléments, d’une propriété non énumérable ou d’un autre prototype ne peut pas être écrit sous forme de source +@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource : un objet doté d’une propriété non énumérable, ou dont le prototype n’est pas Object.prototype, ne peut pas être écrit sous forme de source +@msg toSource: the source is too long toSource : la source est trop longue +@msg toSource: the value contains itself toSource : la valeur se contient elle-même +@msg toSource: the value has too many entries toSource : la valeur a trop d’entrées +@msg toSource: the value is nested too deep toSource : la valeur est imbriquée trop profondément +@msg toString is not a function toString n’est pas une fonction +@msg too large for a zip archive: package to a folder trop volumineux pour une archive zip : assemblez dans un dossier +@msg too many Shaper edits trop de modifications Shaper +@msg too many `points` (at most 100000) trop de `points` (au plus 100000) +@msg too many arguments trop d’arguments +@msg too many characters: at most {MAX_CHARS} trop de caractères : au plus {MAX_CHARS} +@msg too many dots for this size: lower the frequency trop de points pour cette taille : réduisez la linéature +@msg too many filters over one file trop de filtres sur un même fichier +@msg too many overlapping paths for Shaper trop de tracés qui se chevauchent pour Shaper +@msg too many quantifiers trop de quantificateurs +@msg too many saved clips trop de détourages enregistrés +@msg too many saved graphics states trop d’états graphiques enregistrés +@msg toolbar (a toolbar's name) must be a string toolbar (le nom d’une barre d’outils) doit être une chaîne +@msg toolbar name can't be empty le nom de la barre d’outils ne peut pas être vide +@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v} : "flatten" (par défaut) ou "omit" +@msg two saved selections would be named `{_1}` deux sélections enregistrées seraient nommées `{_1}` +@msg type dimension cannot be represented with this text transform la dimension du texte ne peut pas être représentée avec cette transformation de texte +@msg type dimension must be finite la dimension du texte doit être finie +@msg unexpected resources ressources inattendues +@msg unknown CMYK profile `{name}` profil CMJN inconnu `{name}` +@msg unknown File Info field `{k}` champ Informations sur le fichier inconnu `{k}` +@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; prefix - to turn off; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take an alternate's number as `tag=N` with N from 0 to {_1}, as `salt=2`) fonctionnalité OpenType inconnue `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt ; préfixe - pour désactiver ; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom et aalt acceptent un numéro de variante sous la forme `tag=N` avec N de 0 à {_1}, comme `salt=2`) +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) fonctionnalité OpenType inconnue `{t}` dans featureChanges (les balises qu’accepte `features`) +@msg unknown PDF preset `{name}` (presets: {_1}) paramètre prédéfini PDF inconnu `{name}` (paramètres prédéfinis : {_1}) +@msg unknown Warp style style de déformation inconnu +@msg unknown alignToPath `{a}` alignToPath inconnu `{a}` +@msg unknown arrowhead {v} pointe de flèche inconnue {v} +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) clé artboardLayout inconnue `{k}` (layout, columns, spacing, rightToLeft) +@msg unknown attribute `{attribute}` attribut inconnu `{attribute}` +@msg unknown attribute `{k}` attribut inconnu `{k}` +@msg unknown axis `{other}` (vertical|horizontal|free) axe inconnu `{other}` (vertical|horizontal|free) +@msg unknown blend mode `{b}` mode de fusion inconnu `{b}` +@msg unknown blend mode {b} mode de fusion inconnu {b} +@msg unknown brush type `{o}` type de forme inconnu `{o}` +@msg unknown category `{c}` catégorie inconnue `{c}` +@msg unknown category `{c}` ({_1}) catégorie inconnue `{c}` ({_1}) +@msg unknown character `{c}` caractère inconnu `{c}` +@msg unknown character style number field champ numérique de style de caractère inconnu +@msg unknown dither `{d}` (diffusion, pattern, noise or none) tramage inconnu `{d}` (diffusion, pattern, noise ou none) +@msg unknown effect `{e}` effet inconnu `{e}` +@msg unknown end {e} extrémité inconnue {e} +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) flattenerPreset inconnu `{name}` (High Resolution, Medium Resolution, Low Resolution ou un paramètre enregistré : voir flattener.presets.list) +@msg unknown format `{f}` (vcswatches, gpl, ase or css) format inconnu `{f}` (vcswatches, gpl, ase ou css) +@msg unknown from {f} from inconnu {f} +@msg unknown graph type `{t}` type de graphe inconnu `{t}` +@msg unknown grid colours `{name}` (one of {_1}) couleurs de grille inconnues `{name}` (l’une de ces valeurs : {_1}) +@msg unknown gridType `{t}` gridType inconnu `{t}` +@msg unknown highlight `{v}` mise en surbrillance inconnue `{v}` +@msg unknown intent `{i}` mode de rendu inconnu `{i}` +@msg unknown issue kind {id} type de problème inconnu {id} +@msg unknown issue kind {v} type de problème inconnu {v} +@msg unknown knockout state {k} état de masquage inconnu {k} +@msg unknown language `{language}` (one of {_1}) langue inconnue `{language}` (l’une de ces valeurs : {_1}) +@msg unknown language `{l}` (one of {_1}) langue inconnue `{l}` (l’une de ces valeurs : {_1}) +@msg unknown method `{m}` ({_1}) méthode inconnue `{m}` ({_1}) +@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) mode inconnu `{other}` (gray, rgb, hsb, lab, cmyk ou web) +@msg unknown mode `{other}` (mono|cmyk) mode inconnu `{other}` (mono|cmyk) +@msg unknown mode `{other}` (rgb|cmyk|gray|global) mode inconnu `{other}` (rgb|cmyk|gray|global) +@msg unknown mode `{other}` (set, add, subtract) mode inconnu `{other}` (set, add, subtract) +@msg unknown mode `{o}` mode inconnu `{o}` +@msg unknown mode {m} mode inconnu {m} +@msg unknown model `{other}` modèle inconnu `{other}` +@msg unknown model `{other}` (keep, new or document) modèle inconnu `{other}` (keep, new ou document) +@msg unknown op {o} op inconnu {o} +@msg unknown order `{o}`: toggle, front, forward, backward or back ordre inconnu `{o}` : toggle, front, forward, backward ou back +@msg unknown overprints `{v}` (preserve, simulate or discard) surimpressions inconnues `{v}` (preserve, simulate ou discard) +@msg unknown panel panneau inconnu +@msg unknown panel `{panel}` panneau inconnu `{panel}` +@msg unknown parameter `{key}` paramètre inconnu `{key}` +@msg unknown parameter `{within}settings.{format}`; {why} paramètre inconnu `{within}settings.{format}` ; {why} +@msg unknown parameter `{within}{key}`; {why} paramètre inconnu `{within}{key}` ; {why} +@msg unknown pieLegend `{v}` (none, standard or wedges) pieLegend inconnu `{v}` (none, standard ou wedges) +@msg unknown piePosition `{v}` (even, ratio or stacked) piePosition inconnu `{v}` (even, ratio ou stacked) +@msg unknown pieSort `{v}` (none, all or first) pieSort inconnu `{v}` (none, all ou first) +@msg unknown plate `{n}` plaque inconnue `{n}` +@msg unknown preference `{k}` préférence inconnue `{k}` +@msg unknown profile {id} (see stroke.widthProfile.list) profil inconnu {id} (voir stroke.widthProfile.list) +@msg unknown proof target `{t}` cible d’épreuve inconnue `{t}` +@msg unknown quotes key `{k}` (double, single) clé de guillemets inconnue `{k}` (double, single) +@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) réduction inconnue `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform ou document) +@msg unknown resolution `{v}` résolution inconnue `{v}` +@msg unknown rule `{rule}` ({_1}) règle inconnue `{rule}` ({_1}) +@msg unknown rule `{r}` (see color.harmony) règle inconnue `{r}` (voir color.harmony) +@msg unknown setting `{k}` réglage inconnu `{k}` +@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) forme inconnue `{v}` (circle|ellipse|square|diamond|line) +@msg unknown spacing `{o}` (smooth|steps|distance) espacement inconnu `{o}` (smooth|steps|distance) +@msg unknown tileType `{t}` ({_1}) tileType inconnu `{t}` ({_1}) +@msg unknown units unités inconnues +@msg unknown units `{u}` unités inconnues `{u}` +@msg unknown value axis `{v}` (left, right or both) axe des valeurs inconnu `{v}` (left, right ou both) +@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) variation inconnue `{v}` (tintsShades|warmCool|vividMuted) +@msg unknown warp style `{s}` style de déformation inconnu `{s}` +@msg unknown {key} `{v}` (none, short or full) {key} inconnu `{v}` (none, short ou full) +@msg unknown {key} profile `{n}` profil {key} inconnu `{n}` +@msg unknown {k} key `{key}` (size, position) clé {k} inconnue `{key}` (size, position) +@msg unmatched ')' ')' sans correspondance +@msg unreadable object objet illisible +@msg unsafe destination file name nom de fichier de destination non sûr +@msg unterminated character class classe de caractères non terminée +@msg unterminated group groupe non terminé +@msg use paths or compound paths utilisez des tracés ou des tracés transparents +@msg useArtboards must be true or false, not {v} useArtboards doit valoir true ou false, pas {v} +@msg value for `{var}` must match its variable kind la valeur de `{var}` doit correspondre au type de sa variable +@msg variable `{_1}` is already defined la variable `{_1}` est déjà définie +@msg variable `{name}` has no `trait` la variable `{name}` n’a pas de `trait` +@msg variable `{name}` is already defined la variable `{name}` est déjà définie +@msg variable `{name}` is declared twice la variable `{name}` est déclarée deux fois +@msg variable `{to}` already exists la variable `{to}` existe déjà +@msg view is required view est obligatoire +@msg view must be one of {_1} view doit valoir l’une de ces valeurs : {_1} +@msg warpStyle can only edit an applied Warp effect warpStyle ne peut modifier qu’un effet Déformation appliqué +@msg width and height must be more than 0 and at most {limit} width et height doivent être supérieurs à 0 et d’au plus {limit} +@msg width factors must be finite les facteurs de largeur doivent être finis +@msg width must be positive width doit être positif +@msg width out of range largeur hors limites +@msg width/height must be > 0 width/height doivent être > 0 +@msg wordGap and lineGap need area type in the selection wordGap et lineGap nécessitent du texte captif dans la sélection +@msg x and y must be finite numbers x et y doivent être des nombres finis +@msg x and y must be given together x et y doivent être indiqués ensemble +@msg xpath needs its variables as an object xpath nécessite ses variables sous forme d’objet +@msg xpath needs the expression as a string xpath nécessite l’expression sous forme de chaîne +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone nécessite un ancrage : ancrez à côté d’un panneau avec anchor, ou dans une nouvelle colonne d’ancrage avec side (Start ou End) +@msg zoom must be positive zoom doit être positif +@msg {C}: no effect at index {list} {C} : aucun effet à l’indice {list} +@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET} est protégé et ne peut pas être retiré +@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET} est protégé : enregistrez les réglages sous un autre nom +@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED}, et ses réglages et son cache ne peuvent pas être replacés de {_1} vers {_2} : {e} +@msg {LABEL} produced an empty result {LABEL} a produit un résultat vide +@msg {_1} MB of document, at most {_2} MB {_1} Mo de document, au plus {_2} Mo +@msg {_1} MB of images, at most {_2} MB {_1} Mo d’images, au plus {_2} Mo +@msg {_1} bytes of RGBA data for a {width} × {height} image {_1} octets de données RVBA pour une image de {width} × {height} +@msg {_1} can't be read, or changed since it was read: relink or update it first {_1} ne peut pas être lu, ou a changé depuis sa lecture : liez-le à nouveau ou mettez-le d’abord à jour +@msg {_1} can't go into {into} {_1} ne peut pas aller dans {into} +@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1} ne peut pas limiter l’impression à la basse résolution : choisissez PDF 1.4 ou ultérieur, ou autorisez l’impression haute résolution +@msg {_1} files can't be opened yet les fichiers {_1} ne peuvent pas encore être ouverts +@msg {_1} files can't be password-protected: remove the passwords or choose no standard les fichiers {_1} ne peuvent pas être protégés par mot de passe : supprimez les mots de passe ou ne choisissez aucune norme +@msg {_1} files can't be {_2} les fichiers {_1} ne peuvent pas être {_2} +@msg {_1} files can't carry editing data: turn off preserveEditing les fichiers {_1} ne peuvent pas contenir de données d’édition : désactivez preserveEditing +@msg {_1} files can't have PDF layers: turn off createLayers les fichiers {_1} ne peuvent pas avoir de calques PDF : désactivez createLayers +@msg {_1} files need a creation date, and this platform has no clock to give one les fichiers {_1} nécessitent une date de création, et cette plateforme n’a pas d’horloge pour en fournir une +@msg {_1} is not a folder: {e} {_1} n’est pas un dossier : {e} +@msg {_1} isn't a path {_1} n’est pas un tracé +@msg {_1} options: {e} Options {_1} : {e} +@msg {_1} paints nothing to rasterize {_1} ne peint rien à pixelliser +@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) les mots de passe {_1} peuvent compter jusqu’à {MAX_LEGACY_PASSWORD} caractères ASCII (PDF 1.7 et ultérieur acceptent tous les caractères) +@msg {_1} with linked images or an image map writes several files: use document.export {_1} avec des images liées ou une image réactive écrit plusieurs fichiers : utilisez document.export +@msg {_1} writes one file per artboard here: use document.export, or name one artboard {_1} écrit ici un fichier par plan de travail : utilisez document.export, ou indiquez un seul plan de travail +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} pixels est trop grand pour {_3} (au plus {max} pixels de côté) : réduisez l’échelle +@msg {_1}: belongs to another account {_1} : appartient à un autre compte +@msg {_1}: not a folder of its own {_1} : ce n’est pas un dossier distinct +@msg {_1}: not a plain file {_1} : ce n’est pas un fichier ordinaire +@msg {_1}: {_2} and {_3} are in the way {_1} : {_2} et {_3} gênent +@msg {code}: {0} {code} : {0} +@msg {dest}: {e} {dest} : {e} +@msg {dir}: {e} {dir} : {e} +@msg {doc} has no folder {doc} n’a pas de dossier +@msg {file} isn't linked {file} n’est pas lié +@msg {file} names more than one linked JavaScript file {file} désigne plusieurs fichiers JavaScript liés +@msg {folder} is not a package folder {folder} n’est pas un dossier d’assemblage +@msg {folder}: more than {max} files and folders {folder} : plus de {max} fichiers et dossiers +@msg {folder}: {e} {folder} : {e} +@msg {g} is not a Live Paint group {g} n’est pas un groupe de peinture dynamique +@msg {id} is not a slice {id} n’est pas une tranche +@msg {key} must be 0–{MAX_ADD_AROUND} pt {key} doit être compris entre 0 et {MAX_ADD_AROUND} pt +@msg {key} must be [x,y] {key} doit être [x,y] +@msg {key} must be a percentage {min}..100 {key} doit être un pourcentage de {min} à 100 +@msg {key} must be a positive number, not {n} {key} doit être un nombre positif, pas {n} +@msg {key} must be a positive number, not {v} {key} doit être un nombre positif, pas {v} +@msg {key} must be one of {_1} {key} doit valoir l’une de ces valeurs : {_1} +@msg {key} must be {_1}, got {s} {key} doit valoir {_1}, reçu : {s} +@msg {key} must name a floating panel {key} doit désigner un panneau flottant +@msg {key} must name a panel in another group {key} doit désigner un panneau d’un autre groupe +@msg {key} must name a panel that shows {key} doit désigner un panneau affiché +@msg {key} must name a registered panel {key} doit désigner un panneau enregistré +@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) les fichiers {kind} n’ont pas de transparence, et le fichier aurait {what} : aplatissez-la d’abord (object.flattenTransparency) +@msg {kind} images can't be opened (see document.formats) les images {kind} ne peuvent pas être ouvertes (voir document.formats) +@msg {k} `{v}` is not one of the choices {k} `{v}` ne fait pas partie des choix +@msg {k} has control characters{_1} {k} contient des caractères de contrôle{_1} +@msg {k} is too long (at most {MAX_TEXT} characters) {k} est trop long (au plus {MAX_TEXT} caractères) +@msg {k} must be > 0 {k} doit être > 0 +@msg {k} must be a number (pt) or a length such as "210 mm" {k} doit être un nombre (pt) ou une longueur comme "210 mm" +@msg {k} must be a number from {min} to {max} {k} doit être un nombre compris entre {min} et {max} +@msg {k} must be a number or "auto" {k} doit être un nombre ou "auto" +@msg {k} must be a string {k} doit être une chaîne +@msg {k} must be text {k} doit être du texte +@msg {k} must be true or false {k} doit valoir true ou false +@msg {k} must be {{size?, position?}} {k} doit être {{size?, position?}} +@msg {k}: a custom arrowhead's path needs an area to fill and at most {most} anchors and {most} subpaths, each anchor within {far} head units of its tip {k} : le tracé d’une pointe de flèche personnalisée nécessite une zone à remplir et au plus {most} points d’ancrage et {most} sous-tracés, chaque point d’ancrage à moins de {far} unités de pointe de son extrémité +@msg {k}: bad custom arrowhead path: {e} {k} : tracé de pointe de flèche personnalisée incorrect : {e} +@msg {k}: bad custom arrowhead: {e} {k} : pointe de flèche personnalisée incorrecte : {e} +@msg {k}: can't read `{s}` as a length {k} : impossible de lire `{s}` comme une longueur +@msg {k}: custom must be a name {k} : custom doit être un nom +@msg {label} must be a number at most {max} {label} doit être un nombre d’au plus {max} +@msg {label} must be a number from -1296 to 1296 {label} doit être un nombre compris entre -1296 et 1296 +@msg {label} must be a number from 1 to 10000 {label} doit être un nombre compris entre 1 et 10000 +@msg {name} `{s}`: {ids} {name} `{s}` : {ids} +@msg {name} is built in and can't be deleted {name} est intégré et ne peut pas être supprimé +@msg {name} must be {lo}–{hi}{unit} (got {v}) {name} doit être compris entre {lo} et {hi}{unit} (reçu : {v}) +@msg {name}: CRC or size mismatch {name} : CRC ou taille incohérents +@msg {name}: only this Affinity file's embedded preview could be read; use File › Open to see it with its warning, or export SVG or PDF from Affinity before placing {name} : seul l’aperçu incorporé de ce fichier Affinity a pu être lu ; utilisez Fichier › Ouvrir pour l’afficher avec son avertissement, ou exportez en SVG ou PDF depuis Affinity avant de l’importer +@msg {name}: {e} {name} : {e} +@msg {native}; its embedded preview can't be read either ({e}) {native} ; son aperçu incorporé ne peut pas non plus être lu ({e}) +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path} : copy n’accepte un fichier DCS que si ses plaques sont à l’intérieur ; liez-le à nouveau là où se trouvent ses fichiers de plaques +@msg {path}: larger than {_1} MB {path} : dépasse {_1} Mo +@msg {shown} is not an XPath 1.0 function {shown} n’est pas une fonction XPath 1.0 +@msg {std} files are CMYK, and the file would have colours in {_1} les fichiers {std} sont en CMJN, et le fichier aurait des couleurs en {_1} +@msg {std} files embed their fonts, and the file would have one that isn't les fichiers {std} incorporent leurs polices, et le fichier en aurait une qui ne l’est pas +@msg {target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway {target} est le fichier à partir duquel ce document a été lu, et sa lecture a omis des éléments ({what}) : l’écraser les ferait perdre définitivement. Écrivez plutôt un autre fichier, ou passez acknowledgeLoss: true pour le remplacer quand même +@msg {ty} brushes need selected art (or params.{k}) les formes {ty} nécessitent une illustration sélectionnée (ou params.{k}) +@msg {v} is not an asset id {v} n’est pas un identifiant de ressource +@msg {v} names no layer or sublayer {v} ne désigne aucun calque ni sous-calque +@msg {what} can't be intertwined {what} ne peut pas être entrelacé +@msg {what} doesn't fit the paper with its bleed and marks {what} ne tient pas sur le papier avec son fond perdu et ses repères +@msg {what} is not a function {what} n’est pas une fonction +@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what} imprimerait plus de {MAX_COPIES_PER_SHEET} exemplaires sur une feuille : agrandissez-le +@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} s’imprimerait sur plus de {MAX_TILES} mosaïques : réduisez-le +@msg {what} {i} out of range {what} {i} hors limites +@msg Alignment must be one of the choices Alignement doit être l’un des choix proposés +@msg Line Gap must be a number Écart entre les lignes doit être un nombre +@msg `at` must be a point [x, y] `at` doit être un point [x, y] +@msg `points` must hold from 2 to 100000 points `points` doit contenir de 2 à 100000 points +@msg `rect` must be [x, y, width, height] `rect` doit être [x, y, width, height] +@msg `tolerance` must be from 0 to 100 pt `tolerance` doit être compris entre 0 et 100 pt +@msg `warichuAlign` must be one of {_1} `warichuAlign` doit valoir l’une de ces valeurs : {_1} +@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines` doit être un nombre entier compris entre {_1} et {_2} +@msg bad point {v}: expected [x, y] point incorrect {v} : [x, y] attendu +@msg give exactly one of `at`, `rect` and `points` indiquez exactement un paramètre parmi `at`, `rect` et `points` +@msg the line is too long, or the paths it passes too many, to check la ligne est trop longue, ou elle traverse trop de tracés, pour être vérifiée +@msg the paths are too many or too tangled to find where they cross les tracés sont trop nombreux ou trop enchevêtrés pour trouver où ils se croisent +@msg the rectangle or line reaches too many paths to check le rectangle ou la ligne atteint trop de tracés pour être vérifié +@msg {name} must be a number {name} doit être un nombre Banner Hems and Grommets… Ourlets et œillets de banderole… Banner Hems and Grommets Ourlets et œillets de banderole Hem Ourlet diff --git a/crates/ui-egui/src/i18n/gu.tsv b/crates/ui-egui/src/i18n/gu.tsv new file mode 100644 index 0000000000..716d851655 --- /dev/null +++ b/crates/ui-egui/src/i18n/gu.tsv @@ -0,0 +1,4682 @@ +# Gujarati (gu) catalog. Columns, separated by a TAB: context source translation +# +# Clean-room: written from the plain meaning of the English labels. Loanwords are transliterated +# in Gujarati script where that is how the words are spoken; established Gujarati words are used otherwise. +# Product names, file extensions, keyboard keys, units and placeholders stay as in the English. +# Original 4,286 rows reviewed by a native Gujarati-speaking contributor. +# All 387 later additions reviewed and approved by the contributor on 2026-11-14; 11 wording corrections applied. + — missing — ગુમ + — substituted by {style} — {style} દ્વારા બદલ્યું + — {n} characters from another font — બીજા ફોન્ટમાંથી {n} અક્ષરો + a pin to warp | વાળવા માટે એક પિન | + a point to edit it | તેને સંપાદિત કરવા એક બિંદુ | + a stroke to add a width point | પહોળાઈ બિંદુ ઉમેરવા એક સ્ટ્રોક | + across a selected object to set the gradient direction ગ્રેડિયન્ટ દિશા સેટ કરવા પસંદ કરેલી ઑબ્જેક્ટ પર ખેંચો + across paths to push them along | તેમને આગળ ધકેલવા પાથ પર ખેંચો | + an anchor or path segment to select it | તેને પસંદ કરવા એન્કર અથવા પાથ સેગમેન્ટ | + an object to copy its attributes | તેના ગુણધર્મોની નકલ કરવા ઑબ્જેક્ટ | + an object, then another to blend them | ઑબ્જેક્ટ, પછી તેમને મિશ્ર કરવા બીજી ઑબ્જેક્ટ | + cancels | Double-click the tool for options રદ કરે છે | વિકલ્પો માટે ટૂલ પર ડબલ-ક્લિક કરો + for exact size ચોક્કસ કદ માટે + for exact values ચોક્કસ મૂલ્યો માટે + from center | કેન્દ્રથી | + keeps its proportions) તેના પ્રમાણ જાળવે છે) + near a selected pin to rotate | ફેરવવા પસંદ કરેલી પિનની નજીક | + on the canvas to create one ( એક બનાવવા કેનવાસ પર ( + out from a highlighted edge to place its dimension | તેનું માપ મૂકવા પ્રકાશિત કિનારીથી બહાર | + over paths to roughen their outlines | તેમની રૂપરેખા ખરબચડી કરવા પાથ ઉપર | + over paths, and hold still to keep going | ચાલુ રાખવા સ્થિર રાખો, પાથ ઉપર | + right or left to zoom in or out | ઝૂમ ઇન કે આઉટ કરવા જમણે કે ડાબે | + selects several, or draws one inside an artboard) ઘણા પસંદ કરે છે, અથવા આર્ટબોર્ડની અંદર એક દોરે છે) + the art to add a pin | પિન ઉમેરવા આર્ટ | + the object to duplicate ડુપ્લિકેટ કરવા ઑબ્જેક્ટ + the object to select | પસંદ કરવા ઑબ્જેક્ટ | + to add ઉમેરવા માટે + to add a corner point | કૉર્નર બિંદુ ઉમેરવા | + to add a smooth point | સ્મૂધ બિંદુ ઉમેરવા | + to add smooth points | સ્મૂધ બિંદુઓ ઉમેરવા | + to apply the selection's to it | પસંદગીનું તેના પર લાગુ કરવા | + to blend from it | તેમાંથી મિશ્ર કરવા | + to change one side | એક બાજુ બદલવા | + to close | બંધ કરવા | + to constrain proportions | પ્રમાણ મર્યાદિત કરવા | + to copy | નકલ કરવા | + to create area type | એરિયા ટાઇપ બનાવવા | + to create point type | પોઇન્ટ ટાઇપ બનાવવા | + to draw | દોરવા | + to exit editing સંપાદન બંધ કરવા + to finish સમાપ્ત કરવા + to keep zooming ઝૂમ ચાલુ રાખવા + to move | ખસેડવા | + to move in perspective | પર્સ્પેક્ટિવમાં ખસેડવા | + to pan the view દૃશ્ય પેન કરવા + to remove pins પિન દૂર કરવા + to remove points બિંદુઓ દૂર કરવા + to sample a color રંગનો નમૂનો લેવા + to scale in perspective પર્સ્પેક્ટિવમાં સ્કેલ કરવા + to search every command દરેક આદેશ શોધવા + to select an artboard | આર્ટબોર્ડ પસંદ કરવા | + to select more pins | વધુ પિન પસંદ કરવા | + to select more points | વધુ બિંદુઓ પસંદ કરવા | + to select multiple objects | ઘણી ઑબ્જેક્ટ પસંદ કરવા | + to set spacing and orientation અંતર અને દિશા સેટ કરવા + to set the reference point | સંદર્ભ બિંદુ સેટ કરવા | + to size the brush ( બ્રશનું કદ સેટ કરવા ( + to toggle corner | કૉર્નર ટૉગલ કરવા | + to transform | રૂપાંતર કરવા | + to zoom in | ઝૂમ ઇન કરવા | + to zoom into an area વિસ્તાર પર ઝૂમ ઇન કરવા + to zoom out | ઝૂમ આઉટ કરવા | + two points, then move and click to place an aligned, horizontal or vertical dimension | બે બિંદુઓ, પછી સંરેખિત, આડું કે ઊભું માપ મૂકવા ખસેડો અને ક્લિક કરો | + while dragging to move perpendicular to the plane | ખેંચતી વખતે સમતલને લંબરૂપે ખસેડવા | + "In" Control Points "In" કંટ્રોલ પોઇન્ટ્સ + "Out" Control Points "Out" કંટ્રોલ પોઇન્ટ્સ + % of all commits બધા કમિટ્સના % + + Add Scale + સ્કેલ ઉમેરો + 0.2 (quality) to 100 (speed) 0.2 (ગુણવત્તા) થી 100 (ઝડપ) + 1 Color Job… 1 રંગ જૉબ… + 1 {unit} on the artboard stands for {real} {unit} in the scene. આર્ટબોર્ડ પરનો 1 {unit} દ્રશ્યમાં {real} {unit} દર્શાવે છે. + 2 Color Job… 2 રંગ જૉબ… + 3 Color Job… 3 રંગ જૉબ… + 30° Angles 30° ખૂણા + 3D (Classic) 3D (ક્લાસિક) + 3D Box 3D બૉક્સ + 3D Box Tool 3D બૉક્સ ટૂલ + 3D Export Format 3D નિકાસ ફોર્મેટ + 3D Lighting Preset 3D લાઇટિંગ પ્રીસેટ + 3D Ribbon 3D રિબન + 3D and Materials 3D અને મટીરિયલ્સ + 45° Angles 45° ખૂણા + 60° Angles 60° ખૂણા + 90° & 45° & 30° Angles 90° અને 45° અને 30° ખૂણા + 90° & 45° Angles 90° અને 45° ખૂણા + 90° Angles 90° ખૂણા + : : + < 1 sec < 1 સેક + <અપારદર્શકતા માસ્ક> + A File… એક ફાઇલ… + A Script Is Running સ્ક્રિપ્ટ ચાલી રહી છે + A file dialog is open: choose there first ફાઇલ સંવાદ ખુલ્લો છે: પહેલાં ત્યાં પસંદ કરો + A later row takes this shortcut પછીની પંક્તિ આ શૉર્ટકટ લે છે + A legacy .ai file holds one artboard: each artboard you check is saved as a separate file. જૂની .ai ફાઇલમાં એક આર્ટબોર્ડ છે: તમે ચકાસો તે દરેક આર્ટબોર્ડ અલગ ફાઇલ તરીકે સાચવાય છે. + A lower value follows the stroke closely; a higher one gives fewer points and smoother curves. ઓછું મૂલ્ય સ્ટ્રોકને નજીકથી અનુસરે છે; વધુ મૂલ્ય ઓછા બિંદુઓ અને સ્મૂધ વક્રો આપે છે. + A new preset starting from the selected one પસંદ કરેલાથી શરૂ થતો નવો પ્રીસેટ + A saved copy of the selected preset પસંદ કરેલા પ્રીસેટની સાચવેલી નકલ + A script can change the open documents and can write files through the commands it runs. Run only scripts you trust. સ્ક્રિપ્ટ ખુલ્લા દસ્તાવેજો બદલી શકે છે અને તે ચલાવે છે તે આદેશો દ્વારા ફાઇલો લખી શકે છે. ફક્ત વિશ્વસનીય સ્ક્રિપ્ટ ચલાવો. + About વિશે + About VectorCraft VectorCraft વિશે + Absolute સંપૂર્ણ + Absolute Position સંપૂર્ણ સ્થાન + Accelerated પ્રવેગિત + Accent Color ઍક્સેન્ટ રંગ + Accented Edges… ઍક્સેન્ટેડ કિનારીઓ… + Accurate ચોક્કસ + Action Options ક્રિયા વિકલ્પો + Actions ક્રિયાઓ + Activate Document દસ્તાવેજ સક્રિય કરો + Activate Panel પેનલ સક્રિય કરો + Active Perspective Plane સક્રિય પર્સ્પેક્ટિવ સમતલ + Actual Size વાસ્તવિક કદ + Add ઉમેરો + Add Anchor (Pen) એન્કર ઉમેરો (પેન) + Add Anchor Point એન્કર બિંદુ ઉમેરો + Add Anchor Point Tool એન્કર બિંદુ ઉમેરો ટૂલ + Add Anchor Points એન્કર બિંદુઓ ઉમેરો + Add Anchors… એન્કર ઉમેરો… + Add Content કન્ટેન્ટ ઉમેરો + Add Effect ઇફેક્ટ ઉમેરો + Add Event ઇવેન્ટ ઉમેરો + Add Font Files ફોન્ટ ફાઇલો ઉમેરો + Add Fonts ફોન્ટ ઉમેરો + Add Freeform Line ફ્રીફોર્મ લાઇન ઉમેરો + Add Freeform Point ફ્રીફોર્મ બિંદુ ઉમેરો + Add Guide ગાઇડ ઉમેરો + Add Guides ગાઇડ ઉમેરો + Add JavaScript File JavaScript ફાઇલ ઉમેરો + Add Light લાઇટ ઉમેરો + Add Mesh Line મેશ લાઇન ઉમેરો + Add New નવું ઉમેરો + Add New Effect નવી ઇફેક્ટ ઉમેરો + Add New Fill નવું ફિલ ઉમેરો + Add New Stroke નવો સ્ટ્રોક ઉમેરો + Add Points બિંદુઓ ઉમેરો + Add Points at Crossings છેદ પર બિંદુઓ ઉમેરો + Add Points to Segments સેગમેન્ટમાં બિંદુઓ ઉમેરો + Add Points to Segments… સેગમેન્ટમાં બિંદુઓ ઉમેરો… + Add Selected Artwork (Alt: as one asset) પસંદ કરેલી આર્ટવર્ક ઉમેરો (Alt: એક સંપત્તિ તરીકે) + Add Selected Artwork as Multiple Assets પસંદ કરેલી આર્ટવર્ક ઘણી સંપત્તિ તરીકે ઉમેરો + Add Selected Artwork as One Asset પસંદ કરેલી આર્ટવર્ક એક સંપત્તિ તરીકે ઉમેરો + Add Spine Point સ્પાઇન બિંદુ ઉમેરો + Add Used Colors વપરાયેલા રંગ ઉમેરો + Add as Effect ઇફેક્ટ તરીકે ઉમેરો + Add space to fit ફિટ કરવા જગ્યા ઉમેરો + Add to Brushes બ્રશમાં ઉમેરો + Add to Custom Dictionary કસ્ટમ ડિક્શનરીમાં ઉમેરો + Add to Dictionary ડિક્શનરીમાં ઉમેરો + Add to Graphic Styles ગ્રાફિક સ્ટાઇલમાં ઉમેરો + Add to Library લાઇબ્રેરીમાં ઉમેરો + Add to Profiles પ્રોફાઇલમાં ઉમેરો + Add to Profiles… પ્રોફાઇલમાં ઉમેરો… + Add to Selection પસંદગીમાં ઉમેરો + Add to Swatches સ્વેચમાં ઉમેરો + Add to Symbols સિમ્બોલમાં ઉમેરો + Add to the Current Shortcuts વર્તમાન શૉર્ટકટમાં ઉમેરો + Additional Fonts Folder વધારાનું ફોન્ટ ફોલ્ડર + Additional Plug-ins Folder વધારાનું પ્લગ-ઇન ફોલ્ડર + Adjust Adjoining Width Points અડીને આવેલા પહોળાઈ બિંદુઓ ગોઠવો + Adjust Color Balance… રંગ બૅલેન્સ ગોઠવો… + Adjust Colors રંગ ગોઠવો + Adjust Options: ગોઠવણ વિકલ્પો: + Adjustment ગોઠવણ + Advanced અદ્યતન + Advanced Character Formats અદ્યતન અક્ષર ફોર્મેટ + Advanced Options અદ્યતન વિકલ્પો + After First: પ્રથમ પછી: + Align સંરેખિત કરો + Align Bottom નીચેથી સંરેખિત કરો + Align Center કેન્દ્રમાં સંરેખિત કરો + Align Objects: ઑબ્જેક્ટ સંરેખિત કરો: + Align Stroke to Center સ્ટ્રોકને કેન્દ્રમાં સંરેખિત કરો + Align Stroke to Inside સ્ટ્રોકને અંદરની બાજુએ સંરેખિત કરો + Align Stroke to Outside સ્ટ્રોકને બહારની બાજુએ સંરેખિત કરો + Align Stroke: સ્ટ્રોક સંરેખિત કરો: + Align To: સંરેખિત કરો: + Align Top ઉપરથી સંરેખિત કરો + Align center કેન્દ્રમાં સંરેખિત કરો + Align left ડાબે સંરેખિત કરો + Align right જમણે સંરેખિત કરો + Align to Artboard આર્ટબોર્ડ પર સંરેખિત કરો + Align to Glyph Bounds ગ્લિફ સીમા પર સંરેખિત કરો + Align to Key Object કી ઑબ્જેક્ટ પર સંરેખિત કરો + Align to Page પેજ પર સંરેખિત કરો + Align to Path પાથ પર સંરેખિત કરો + Align to Pixel Grid પિક્સેલ ગ્રિડ પર સંરેખિત કરો + Align to Selection પસંદગી પર સંરેખિત કરો + Align: સંરેખિત કરો: + Alignment Guides સંરેખણ ગાઇડ + Alignment: સંરેખણ: + All બધા + All Artboards બધા આર્ટબોર્ડ + All Artwork Above ઉપરની બધી આર્ટવર્ક + All Caps બધા કૅપ્સ + All Categories બધી કેટેગરી + All Scripts બધી લિપિ + All Text Objects બધી ટેક્સ્ટ ઑબ્જેક્ટ + All Tools બધા ટૂલ + All on Active Artboard સક્રિય આર્ટબોર્ડ પરના બધા + All on Same Layers સમાન લેયર પરના બધા + All printer's marks બધા પ્રિન્ટરના નિશાન + All the art બધી આર્ટ + Alt key toggles to Smooth Tool Alt કી સ્મૂધ ટૂલ પર ટૉગલ કરે છે + Alt: Alt: + Alter Paths for Appearance દેખાવ માટે પાથ બદલો + Alternates for Current Selection વર્તમાન પસંદગી માટે વૈકલ્પિક + Amber એમ્બર + Ambient Light એમ્બિયન્ટ લાઇટ + Amount માત્રા + Amount: માત્રા: + Anchor Eraser Brush એન્કર ઇરેઝર બ્રશ + Anchor Inspector એન્કર ઇન્સ્પેક્ટર + Anchor Point એન્કર બિંદુ + Anchor Point Tool એન્કર બિંદુ ટૂલ + Anchor Points એન્કર બિંદુઓ + Anchor Points, Handle, and Bounding Box Display એન્કર બિંદુઓ, હેન્ડલ અને બાઉન્ડિંગ બૉક્સ ડિસ્પ્લે + Anchor/Path Labels એન્કર/પાથ લેબલ + Anchors selected: {count} પસંદ કરેલા એન્કર: {count} + Anchors: એન્કર: + Angle ખૂણો + Angle: ખૂણો: + Angled Strokes… ત્રાંસા સ્ટ્રોક… + Angles ખૂણા + Animated Zoom ઍનિમેટેડ ઝૂમ + Anti-Alias એન્ટિ-એલિયાસ + Anti-alias એન્ટિ-એલિયાસ + Anti-alias Rasters રાસ્ટર એન્ટિ-એલિયાસ + Anti-aliased Artwork એન્ટિ-એલિયાસ્ડ આર્ટવર્ક + Anti-aliasing Method એન્ટિ-એલિયાસિંગ પદ્ધતિ + Anti-aliasing: એન્ટિ-એલિયાસિંગ: + Appearance દેખાવ + Appearance Attribute દેખાવ ગુણધર્મ + Appearance Item દેખાવ આઇટમ + Appearance Panel દેખાવ પેનલ + Appearance of Black કાળાનો દેખાવ + Appearance panel દેખાવ પેનલ + Applies the next time VectorCraft starts. VectorCraft આગલી વખતે શરૂ થશે ત્યારે લાગુ થશે. + Applies when the linked file is relinked or updated. લિંક કરેલી ફાઇલ ફરી લિંક અથવા અપડેટ થાય ત્યારે લાગુ થાય છે. + Apply લાગુ કરો + Apply Brush બ્રશ લાગુ કરો + Apply Bullets બુલેટ લાગુ કરો + Apply Character Style અક્ષર સ્ટાઇલ લાગુ કરો + Apply Data Set ડેટા સેટ લાગુ કરો + Apply Effect ઇફેક્ટ લાગુ કરો + Apply Fill to Stroke સ્ટ્રોક પર ફિલ લાગુ કરો + Apply Gradient Preset ગ્રેડિયન્ટ પ્રીસેટ લાગુ કરો + Apply Graphic Style ગ્રાફિક સ્ટાઇલ લાગુ કરો + Apply Last Color છેલ્લો રંગ લાગુ કરો + Apply Last Effect છેલ્લી ઇફેક્ટ લાગુ કરો + Apply Last Gradient છેલ્લો ગ્રેડિયન્ટ લાગુ કરો + Apply Nearest Swatch નજીકનો સ્વેચ લાગુ કરો + Apply Numbers નંબર લાગુ કરો + Apply Paragraph Style ફકરા સ્ટાઇલ લાગુ કરો + Apply SVG Filter SVG ફિલ્ટર લાગુ કરો + Apply SVG Filter… SVG ફિલ્ટર લાગુ કરો… + Apply Stroke to Fill ફિલ પર સ્ટ્રોક લાગુ કરો + Apply To: આના પર લાગુ કરો: + Apply as Filter ફિલ્ટર તરીકે લાગુ કરો + Apply the preferences in a file Export Preferences saved Export Preferences દ્વારા સાચવેલી ફાઇલની પસંદગીઓ લાગુ કરો + Apply to All બધા પર લાગુ કરો + Apply to Selected Anchors પસંદ કરેલા એન્કર પર લાગુ કરો + Apply {name} {name} લાગુ કરો + Approximate path અંદાજિત પાથ + Arc આર્ક + Arc Lower… નીચે આર્ક… + Arc Segment Tool Options આર્ક સેગમેન્ટ ટૂલ વિકલ્પો + Arc Tool આર્ક ટૂલ + Arc Upper… ઉપર આર્ક… + Arch… કમાન… + Arc… આર્ક… + Area / Path Type એરિયા / પાથ ટાઇપ + Area Graph Tool એરિયા ગ્રાફ ટૂલ + Area Text એરિયા ટેક્સ્ટ + Area Text Objects એરિયા ટેક્સ્ટ ઑબ્જેક્ટ + Area Type એરિયા ટાઇપ + Area Type Options એરિયા ટાઇપ વિકલ્પો + Area Type Options… એરિયા ટાઇપ વિકલ્પો… + Area Type Tool એરિયા ટાઇપ ટૂલ + Around Object ઑબ્જેક્ટની આસપાસ + Arrange ગોઠવો + Arrange Intertwine Area ઇન્ટરટ્વાઇન વિસ્તાર ગોઠવો + Arrange Tools ટૂલ ગોઠવો + Arrange by Column કૉલમ પ્રમાણે ગોઠવો + Arrange by Row પંક્તિ પ્રમાણે ગોઠવો + Arrange in Grid ગ્રિડમાં ગોઠવો + Arrange in Grid… ગ્રિડમાં ગોઠવો… + Arrange: Bring to Front ગોઠવો: સૌથી આગળ લાવો + Arrowheads: તીરના માથાં: + Art Brush આર્ટ બ્રશ + Art Brush Options આર્ટ બ્રશ વિકલ્પો + Art Ribbons આર્ટ રિબન + Art using a deleted global swatch keeps its colour. કાઢી નાખેલો ગ્લોબલ સ્વેચ વાપરતી આર્ટ પોતાનો રંગ જાળવે છે. + ArtCraft Website ArtCraft વેબસાઇટ + ArtCraft website ArtCraft વેબસાઇટ + Artboard આર્ટબોર્ડ + Artboard Guides with Cmd/Ctrl + Ruler Drag Cmd/Ctrl + રૂલર ખેંચીને આર્ટબોર્ડ ગાઇડ + Artboard Options આર્ટબોર્ડ વિકલ્પો + Artboard Options… આર્ટબોર્ડ વિકલ્પો… + Artboard Tool આર્ટબોર્ડ ટૂલ + Artboard {number} આર્ટબોર્ડ {number} + Artboard {n} of {total} આર્ટબોર્ડ {total} માંથી {n} + Artboard: આર્ટબોર્ડ: + Artboards આર્ટબોર્ડ + Artboards: {n} આર્ટબોર્ડ: {n} + Artistic કલાત્મક + Artwork આર્ટવર્ક + Artwork Cleanup આર્ટવર્ક ક્લીનઅપ + Artwork Colors આર્ટવર્ક રંગ + As Multiple Assets ઘણી સંપત્તિ તરીકે + As Single Asset એક સંપત્તિ તરીકે + Ask When Modified બદલાય ત્યારે પૂછો + Aspect Ratio પાસા ગુણોત્તર + Assamese આસામી + Asset Export સંપત્તિ નિકાસ + Asset Export Settings સંપત્તિ નિકાસ સેટિંગ્સ + Assets સંપત્તિ + Assign સોંપો + Assign Profile પ્રોફાઇલ સોંપો + Assign Profile… પ્રોફાઇલ સોંપો… + Assign: સોંપો: + Attach જોડો + Attach Objects on Path પાથ પર ઑબ્જેક્ટ જોડો + Attach to Active Plane સક્રિય સમતલ પર જોડો + Attribute: ગુણધર્મ: + Attributes ગુણધર્મો + Author Title: લેખકનું શીર્ષક: + Author: લેખક: + Auto ઑટો + Auto Leading: ઑટો લીડિંગ: + Auto Size New Area Type નવા એરિયા ટાઇપનું ઑટો કદ + Auto Spell Check ઑટો સ્પેલ ચેક + Auto-Between ઑટો-વચ્ચે + Auto-Centered ઑટો-કેન્દ્રિત + Auto-Collapse Iconic Panels આઇકન પેનલ ઑટો-સંકોચો + Auto-Overlap ઑટો-ઓવરલેપ + Auto-Rotate (turn the paper to each artboard) ઑટો-ફેરવો (દરેક આર્ટબોર્ડ માટે કાગળ ફેરવો) + Auto-Simplify ઑટો-સરળ બનાવો + Auto-Sliced ઑટો-સ્લાઇસ્ડ + Auto-Smooth ઑટો-સ્મૂધ + Automatic સ્વચાલિત + Automatic flatness સ્વચાલિત ફ્લેટનેસ + Automatically આપમેળે + Automatically Save Recovery Data રિકવરી ડેટા આપમેળે સાચવો + Automation ઑટોમેશન + Average સરેરાશ + Average & Join સરેરાશ અને જોડો + Average & Join… સરેરાશ અને જોડો… + Average… સરેરાશ… + Axis side અક્ષની બાજુ + BLANK DOCUMENT PRESETS ખાલી દસ્તાવેજ પ્રીસેટ + Back પાછળ + Background Color: બૅકગ્રાઉન્ડ રંગ: + Background Contents બૅકગ્રાઉન્ડ કન્ટેન્ટ + Background: બૅકગ્રાઉન્ડ: + Bar Graph Tool બાર ગ્રાફ ટૂલ + Bas Relief… બાસ રિલીફ… + Base Along: સાથે આધાર: + Baseline બેઝલાઇન + Baseline Shift બેઝલાઇન શિફ્ટ + Basic Character Formats મૂળભૂત અક્ષર ફોર્મેટ + Basic Shapes મૂળભૂત આકાર + Batch બૅચ + Batch Trace Folder બૅચ ટ્રેસ ફોલ્ડર + Batch… બૅચ… + Before Last: છેલ્લા પહેલાં: + Begin Recording રેકોર્ડિંગ શરૂ કરો + Begin Undo Group પૂર્વવત્ ગ્રુપ શરૂ કરો + Bend: વાળો: + Bengali બંગાળી + Bevel બેવલ + Bevel Inside અંદરથી બેવલ + Bevel Join બેવલ જોઇન + Bevel Shape બેવલ આકાર + Bevel both sides બંને બાજુ બેવલ + Binary assets added બાઇનરી સંપત્તિ ઉમેરાઈ + Binary assets removed બાઇનરી સંપત્તિ દૂર કરી + Bind Variable વેરિએબલ બાંધો + Bitmap બિટમેપ + Black કાળો + Black and white stay as they are. Dark colors become shades of the stroke color and light colors become tints of it. કાળો અને સફેદ જેમના તેમ રહે છે. ઘેરા રંગ સ્ટ્રોક રંગની છાંયડા બને છે અને આછા રંગ તેની ટિન્ટ બને છે. + Black becomes the stroke color and white stays white. Other colors become tints of the stroke color by their lightness. કાળો સ્ટ્રોક રંગ બને છે અને સફેદ સફેદ રહે છે. અન્ય રંગ તેમની હળવાશ પ્રમાણે સ્ટ્રોક રંગની ટિન્ટ બને છે. + Bleed બ્લીડ + Bleed: બ્લીડ: + Bleeds બ્લીડ + Blend બ્લેન્ડ + Blend Front to Back આગળથી પાછળ બ્લેન્ડ + Blend Horizontally આડું બ્લેન્ડ + Blend Info બ્લેન્ડ માહિતી + Blend Options બ્લેન્ડ વિકલ્પો + Blend Options… બ્લેન્ડ વિકલ્પો… + Blend Steps બ્લેન્ડ સ્ટેપ + Blend Tool બ્લેન્ડ ટૂલ + Blend Vertically ઊભું બ્લેન્ડ + Blending Mode બ્લેન્ડિંગ મોડ + Blends and Transparency બ્લેન્ડ અને પારદર્શિતા + Bloat ફુલાવો + Bloat Tool ફુલાવો ટૂલ + Blob Brush બ્લૉબ બ્રશ + Blob Brush Tool બ્લૉબ બ્રશ ટૂલ + Blue વાદળી + Blur બ્લર + Bold બોલ્ડ + Both sides બંને બાજુ + Bottom નીચે + Bottom Left નીચે ડાબે + Bottom Left: નીચે ડાબે: + Bottom Right નીચે જમણે + Bottom Right: નીચે જમણે: + Bottom to Top નીચેથી ઉપર + Bottom-Justified Tab નીચે-જસ્ટિફાઇડ ટૅબ + Bottom-to-Bottom Leading નીચેથી નીચે લીડિંગ + Bottom: નીચે: + Bracket never closed કૌંસ ક્યારેય બંધ થયો નથી + Break Link લિંક તોડો + Break Link to Graphic Style ગ્રાફિક સ્ટાઇલની લિંક તોડો + Break Link to Symbol સિમ્બોલની લિંક તોડો + Break Text ટેક્સ્ટ તોડો + Break Text into Glyphs ટેક્સ્ટને ગ્લિફમાં તોડો + Break Text into Lines ટેક્સ્ટને લાઇનમાં તોડો + Break Text into Paragraphs ટેક્સ્ટને ફકરામાં તોડો + Break Text into Words ટેક્સ્ટને શબ્દોમાં તોડો + Break Thread થ્રેડ તોડો + Brick Offset: ઈંટ ઑફસેટ: + Brightness તેજ + Brightness/Contrast… તેજ/કૉન્ટ્રાસ્ટ… + Brightness: તેજ: + Brights તેજસ્વી + Bring All to Front બધાને સૌથી આગળ લાવો + Bring Forward આગળ લાવો + Bring to Front સૌથી આગળ લાવો + Bristle Brush બ્રિસ્ટલ બ્રશ + Bristle Brush Options બ્રિસ્ટલ બ્રશ વિકલ્પો + Bristle Brush Strokes બ્રિસ્ટલ બ્રશ સ્ટ્રોક + Browser બ્રાઉઝર + Brush Affects: બ્રશ અસર કરે છે: + Brush Art બ્રશ આર્ટ + Brush Definition બ્રશ વ્યાખ્યા + Brush Libraries બ્રશ લાઇબ્રેરી + Brush Library બ્રશ લાઇબ્રેરી + Brush Options બ્રશ વિકલ્પો + Brush Options… બ્રશ વિકલ્પો… + Brush Scale Options બ્રશ સ્કેલ વિકલ્પો + Brush Strokes બ્રશ સ્ટ્રોક + Brushes બ્રશ + Built-in presets are read-only: New… starts an editable copy. બિલ્ટ-ઇન પ્રીસેટ ફક્ત વાંચવા પૂરતા છે: નવું… સંપાદિત કરી શકાય તેવી નકલ શરૂ કરે છે. + Built-in presets don't change: New makes a copy you can edit. બિલ્ટ-ઇન પ્રીસેટ બદલાતા નથી: નવું સંપાદિત કરી શકાય તેવી નકલ બનાવે છે. + Built-in presets don't change: New… starts an editable copy. બિલ્ટ-ઇન પ્રીસેટ બદલાતા નથી: નવું… સંપાદિત કરી શકાય તેવી નકલ શરૂ કરે છે. + Bulgarian બલ્ગેરિયન + Bulge… ફુગાવો… + Bullet બુલેટ + Bullet Character: બુલેટ અક્ષર: + Bullets બુલેટ + Bullets and Numbering બુલેટ અને નંબરિંગ + Bullets and Numbering… બુલેટ અને નંબરિંગ… + Burasagari Burasagari + Butt Cap બટ કૅપ + By Columns કૉલમ પ્રમાણે + By Layer લેયર પ્રમાણે + By Object ઑબ્જેક્ટ પ્રમાણે + By Rows પંક્તિ પ્રમાણે + Byte Order: બાઇટ ક્રમ: + CMYK CMYK + CMYK Color CMYK રંગ + CMYK Images CMYK ઇમેજ + CMYK colours print with their own values; off, they go through the colour settings too. CMYK રંગો પોતાની કિંમતો સાથે છાપે છે; બંધ હોય ત્યારે તે રંગ સેટિંગમાંથી પણ પસાર થાય છે. + CMYK: CMYK: + CSS Properties CSS ગુણધર્મો + Calligraphic Brush કેલિગ્રાફિક બ્રશ + Calligraphic Brush Options કેલિગ્રાફિક બ્રશ વિકલ્પો + Calligraphic Nibs કેલિગ્રાફિક નિબ + Can't export “{name}”. “{name}” નિકાસ કરી શકાતી નથી. + Can't open “{name}”. “{name}” ખોલી શકાતી નથી. + Can't print these settings: see the Summary આ સેટિંગ્સ છાપી શકાતી નથી: સારાંશ જુઓ + Can't save “{name}”. “{name}” સાચવી શકાતી નથી. + Cancel રદ કરો + Cancel Export નિકાસ રદ કરો + Cancel Image Trace ઇમેજ ટ્રેસ રદ કરો + Cancel Key Object કી ઑબ્જેક્ટ રદ કરો + Cancel Opening ખોલવાનું રદ કરો + Cancel Pattern Editing પેટર્ન સંપાદન રદ કરો + Cancel Plug-in Run પ્લગ-ઇન ચલાવવાનું રદ કરો + Canvas કેનવાસ + Canvas Color કેનવાસ રંગ + Cap Height કેપ ઊંચાઈ + Cap: કેપ: + Caps Lock Cursor State કેપ્સ લૉક કર્સર સ્થિતિ + Capture Data Set ડેટા સેટ કૅપ્ચર કરો + Cascade ક્રમમાં ગોઠવો + Case કેસ + Catalan કેટાલાન + Center કેન્દ્ર + Center Artwork આર્ટવર્ક કેન્દ્રિત કરો + Center on Artboard આર્ટબોર્ડ પર કેન્દ્રિત કરો + Center-Justified Tab કેન્દ્ર-સંરેખિત ટેબ + Centimeters સેન્ટિમીટર + Chalk & Charcoal… ચૉક અને ચારકોલ… + Chamfer ચેમ્ફર + Change બદલો + Change All બધું બદલો + Change Case કેસ બદલો + Change Panel Layout પેનલ લેઆઉટ બદલો + Change how the objects its effect is applied to are drawn જે ઑબ્જેક્ટ્સ પર તેની અસર લાગુ થાય છે તે કેવી રીતે દોરાય તે બદલો + Change in Custom Dictionary કસ્ટમ ડિક્શનરીમાં બદલો + Change to Artboard Rulers આર્ટબોર્ડ રૂલર પર બદલો + Change to Global Rulers ગ્લોબલ રૂલર પર બદલો + Change to Left-to-Right Layout ડાબેથી જમણે લેઆઉટ પર બદલો + Change to Right-to-Left Layout જમણેથી ડાબે લેઆઉટ પર બદલો + Change, add and delete objects in the document દસ્તાવેજમાં ઑબ્જેક્ટ બદલો, ઉમેરો અને કાઢી નાખો + Changed since the last Refresh. છેલ્લા રિફ્રેશ પછી બદલાયું. + Changed since the last check. છેલ્લી તપાસ પછી બદલાયું. + Changes allowed: મંજૂર બદલાવ: + Changes made since it was last saved will be lost. છેલ્લે સાચવ્યા પછી કરેલા બદલાવ ગુમ થશે. + Changes the reference app's preferences સંદર્ભ એપની પસંદગીઓ બદલે છે + Changing these settings changes how raster effects look when exported, flattened or expanded. આ સેટિંગ્સ બદલવાથી નિકાસ, ફ્લેટન કે વિસ્તૃત કરતી વખતે રાસ્ટર અસરો કેવી દેખાય છે તે બદલાય છે. + Channel ચેનલ + Character અક્ષર + Character / Paragraph અક્ષર / ફકરો + Character Alignment અક્ષર સંરેખણ + Character Color અક્ષર રંગ + Character Direction અક્ષર દિશા + Character Rotation અક્ષર પરિભ્રમણ + Character Set: અક્ષર સેટ: + Character Style Options અક્ષર સ્ટાઇલ વિકલ્પો + Character Styles અક્ષર સ્ટાઇલ + Character options અક્ષર વિકલ્પો + Character: અક્ષર: + Charcoal… ચારકોલ… + Check Document દસ્તાવેજ તપાસો + Check Links લિંક તપાસો + Check Script સ્ક્રિપ્ટ તપાસો + Check Script… સ્ક્રિપ્ટ તપાસો… + Check Selection પસંદગી તપાસો + Check Spelling જોડણી તપાસો + Check Spelling… જોડણી તપાસો… + Check an artboard to save. સાચવવા માટે આર્ટબોર્ડ પસંદ કરો. + Check for Updates… અપડેટ્સ તપાસો… + Checked ચકાસેલું + Chinese ચાઇનીઝ + Choose a folder ફોલ્ડર પસંદ કરો + Choose the file to relink each linked file to. Files with the size and contents saved with the link come first. દરેક લિંક થયેલી ફાઇલને ફરી લિંક કરવા માટે ફાઇલ પસંદ કરો. લિંક સાથે સાચવેલા કદ અને સામગ્રીવાળી ફાઇલો પહેલા આવશે. + Choose… પસંદ કરો… + Chop Design ચૉપ ડિઝાઇન + Chosen: {n} of {total} પસંદ કરેલ: {total} માંથી {n} + Chrome… ક્રોમ… + Circle વર્તુળ + Classic ક્લાસિક + Clean Up સાફ કરો + Clean Up… સાફ કરો… + Clear સાફ કરો + Clear All બધું સાફ કરો + Clear All Tabs બધા ટેબ સાફ કરો + Clear Appearance દેખાવ સાફ કરો + Clear Events ઇવેન્ટ્સ સાફ કરો + Clear Guides ગાઇડ્સ સાફ કરો + Clear History ઇતિહાસ સાફ કરો + Clear Plug-in Cache પ્લગ-ઇન કેશ સાફ કરો + Clear Recent Files તાજેતરની ફાઇલો સાફ કરો + Clear Script Console સ્ક્રિપ્ટ કન્સોલ સાફ કરો + Clear Selection પસંદગી સાફ કરો + Click ક્લિક + Click an anchor point એન્કર પોઇન્ટ પર ક્લિક કરો + Click or drag ક્લિક કરો અથવા ખેંચો + Click the art with the Gradient tool to add or select points. પોઇન્ટ ઉમેરવા કે પસંદ કરવા ગ્રેડિયન્ટ ટૂલ વડે આર્ટવર્ક પર ક્લિક કરો. + Click the first point પહેલા પોઇન્ટ પર ક્લિક કરો + Click the ramp or a type button to apply a gradient. ગ્રેડિયન્ટ લાગુ કરવા માટે રૅમ્પ અથવા ટાઇપ બટન પર ક્લિક કરો. + Click to apply to the selected type પસંદ કરેલા ટેક્સ્ટ પર લાગુ કરવા ક્લિક કરો + Click to choose a swatch, Shift-click to mix a colour સ્વેચ પસંદ કરવા ક્લિક કરો, રંગ મિક્સ કરવા Shift-ક્લિક કરો + Click to edit સંપાદન માટે ક્લિક કરો + Click to edit the mask; Alt-click to view only the mask; Shift-click to disable or enable it માસ્ક સંપાદિત કરવા ક્લિક કરો; માત્ર માસ્ક જોવા Alt-ક્લિક કરો; તેને બંધ કે ચાલુ કરવા Shift-ક્લિક કરો + Click to place each file at 100%, or drag to size it; arrow keys switch files, Esc skips one. દરેક ફાઇલ 100% પર મૂકવા ક્લિક કરો, અથવા કદ આપવા ખેંચો; તીર કી ફાઇલો બદલે છે, Esc એક છોડે છે. + Click to select, double-click to edit પસંદ કરવા ક્લિક કરો, સંપાદન માટે ડબલ-ક્લિક કરો + Click to toggle visibility દૃશ્યતા બદલવા ક્લિક કરો + Click to zoom in, Alt-click to zoom out, drag to pan, double-click to fit મોટું કરવા ક્લિક કરો, નાનું કરવા Alt-ક્લિક કરો, ખસેડવા ખેંચો, ફિટ કરવા ડબલ-ક્લિક કરો + Click, then press the key this code stands for (Esc cancels) ક્લિક કરો, પછી આ કોડ જે કી માટે છે તે દબાવો (Esc રદ કરે છે) + Clip ક્લિપ + Clip Complex Regions જટિલ વિસ્તારો ક્લિપ કરો + Clip to Art આર્ટવર્ક પર ક્લિપ કરો + Clip to Artboard આર્ટબોર્ડ પર ક્લિપ કરો + Clip to Bounding Box બાઉન્ડિંગ બૉક્સ પર ક્લિપ કરો + Clipboard Formats ક્લિપબોર્ડ ફોર્મેટ્સ + Clipboard Handling ક્લિપબોર્ડ હેન્ડલિંગ + Clipboard as PDF PDF તરીકે ક્લિપબોર્ડ + Clipboard as PNG PNG તરીકે ક્લિપબોર્ડ + Clipboard as SVG SVG તરીકે ક્લિપબોર્ડ + Clipboard as Text ટેક્સ્ટ તરીકે ક્લિપબોર્ડ + Clipping Mask ક્લિપિંગ માસ્ક + Clipping Masks ક્લિપિંગ માસ્ક + Clockwise ઘડિયાળની દિશામાં + Close બંધ કરો + Close All બધું બંધ કરો + Close Gaps With Paths પાથ વડે ગેપ બંધ કરો + Close Panel પેનલ બંધ કરો + Close Path પાથ બંધ કરો + Close Path… પાથ બંધ કરો… + Close or Join બંધ કરો અથવા જોડો + Close paths when ends are within: છેડા આ અંતરમાં હોય ત્યારે પાથ બંધ કરો: + Close the circular ends of open profiles. Partial sweeps keep their cut sides open. ખુલ્લી પ્રોફાઇલના ગોળ છેડા બંધ કરો. આંશિક સ્વીપ તેમની કાપેલી બાજુઓ ખુલ્લી રાખે છે. + Closing bracket with no opening one ખુલતા કૌંસ વગરનો બંધ થતો કૌંસ + Coincidence: સંયોગ: + Collapse Panel Group પેનલ ગ્રૂપ સંકોચો + Collapse Panels to Icons પેનલ્સને આઇકનમાં સંકોચો + Collapse to Icons આઇકનમાં સંકોચો + Collate ક્રમમાં ગોઠવો + Collect for Export નિકાસ માટે એકત્ર કરો + Collect in New Layer નવા લેયરમાં એકત્ર કરો + Color રંગ + Color Adjustments રંગ ગોઠવણ + Color Burn કલર બર્ન + Color Dodge કલર ડોજ + Color Effects રંગ અસરો + Color Effects Panel રંગ અસરો પેનલ + Color Group: રંગ ગ્રૂપ: + Color Group: {name} રંગ ગ્રૂપ: {name} + Color Guide રંગ ગાઇડ + Color Guide Options રંગ ગાઇડ વિકલ્પો + Color Guide Options… રંગ ગાઇડ વિકલ્પો… + Color Halftone… કલર હાફટોન… + Color Library… રંગ લાઇબ્રેરી… + Color Mode રંગ મોડ + Color Mode: રંગ મોડ: + Color Model: રંગ મોડેલ: + Color Models રંગ મોડેલ + Color Panel Options રંગ પેનલ વિકલ્પો + Color Picker રંગ પિકર + Color Picker… રંગ પિકર… + Color Reduction: રંગ ઘટાડો: + Color Scheme રંગ યોજના + Color Settings રંગ સેટિંગ્સ + Color Settings… રંગ સેટિંગ્સ… + Color Space રંગ સ્પેસ + Color Swatches રંગ સ્વેચ + Color Table રંગ ટેબલ + Color Themes રંગ થીમ + Color Type: રંગ પ્રકાર: + Color Wheel રંગ વ્હીલ + Color blindness – Deuteranopia-type રંગ અંધત્વ – ડ્યુટેરેનોપિયા પ્રકાર + Color blindness – Protanopia-type રંગ અંધત્વ – પ્રોટેનોપિયા પ્રકાર + Color blindness – Tritanopia-type રંગ અંધત્વ – ટ્રિટેનોપિયા પ્રકાર + Color conversion: રંગ રૂપાંતર: + Color field રંગ ક્ષેત્ર + Color images રંગીન ઇમેજ + Color wheel રંગ વ્હીલ + Color: રંગ: + Colored Pencil… રંગીન પેન્સિલ… + Colorization કલરાઇઝેશન + Colorization Tips કલરાઇઝેશન ટિપ્સ + Colorize કલરાઇઝ કરો + Colors રંગો + Colors: રંગો: + Column Graph Tool કૉલમ ગ્રાફ ટૂલ + Column Type: કૉલમ પ્રકાર: + Column Width: કૉલમ પહોળાઈ: + Columns: કૉલમ: + Column… કૉલમ… + Combine Point Text પોઇન્ટ ટેક્સ્ટ જોડો + Combine Slices સ્લાઇસ જોડો + Coming soon — tracked in the parity plan જલદી આવશે — પેરિટી પ્લાનમાં ટ્રેક થાય છે + Command API Reference કમાન્ડ API સંદર્ભ + Command Click to Select Objects Behind પાછળના ઑબ્જેક્ટ પસંદ કરવા Command-ક્લિક કરો + Command Dialog કમાન્ડ ડાયલોગ + Comment not closed ટિપ્પણી બંધ થઈ નથી + Commits કમિટ્સ + Community સમુદાય + Company કંપની + Compare with Image ઇમેજ સાથે સરખાવો + Compatibility: સુસંગતતા: + Compatible Gradient and Gradient Mesh Printing સુસંગત ગ્રેડિયન્ટ અને ગ્રેડિયન્ટ મેશ પ્રિન્ટિંગ + Compiled or not text કમ્પાઇલ થયેલું છે કે નહીં તે ટેક્સ્ટ + Compiled script: ask its author for the .jsx file કમ્પાઇલ થયેલી સ્ક્રિપ્ટ: .jsx ફાઇલ માટે તેના લેખકને પૂછો + Complement પૂરક + Complexity: જટિલતા: + Composite Fonts કમ્પોઝિટ ફોન્ટ્સ + Composite Fonts… કમ્પોઝિટ ફોન્ટ્સ… + Composite colours are converted to it; separations separate with it when it is a CMYK profile. કમ્પોઝિટ રંગો તેમાં રૂપાંતરિત થાય છે; CMYK પ્રોફાઇલ હોય ત્યારે સેપરેશન તેની સાથે અલગ પડે છે. + Compound Path કમ્પાઉન્ડ પાથ + Compound Shapes કમ્પાઉન્ડ આકારો + Compress (RLE) કમ્પ્રેસ (RLE) + Compress text and line art ટેક્સ્ટ અને લાઇન આર્ટ કમ્પ્રેસ કરો + Compression: કમ્પ્રેશન: + Concave અંતર્ગોળ + Concentric Dividers: કેન્દ્રિત ડિવાઇડર: + Condition identifier: શરત ઓળખકર્તા: + Configure Shape આકાર ગોઠવો + Conflict {n} of {count} વિરોધાભાસ {count} માંથી {n} + Connect જોડો + Connect Selected End Points પસંદ કરેલા અંતિમ પોઇન્ટ જોડો + Consider Open Filled Paths as Closed ભરેલા ખુલ્લા પાથને બંધ ગણો + Consolidate All Windows બધી વિન્ડો એકત્ર કરો + Constrain મર્યાદિત કરો + Constrain Angle ખૂણો મર્યાદિત કરો + Constrain Width and Height Proportions પહોળાઈ અને ઊંચાઈનો પ્રમાણ મર્યાદિત કરો + Construction Guides બાંધકામ ગાઇડ્સ + Contextual Alternates સંદર્ભિત વૈકલ્પિક અક્ષરો + Contextual Task Bar સંદર્ભિત ટાસ્ક બાર + Continue ચાલુ રાખો + Continue Action ક્રિયા ચાલુ રાખો + Continue in Background બેકગ્રાઉન્ડમાં ચાલુ રાખો + Contour કન્ટૂર + Contour Tool કન્ટૂર ટૂલ + Contributors યોગદાનકર્તાઓ + Control કંટ્રોલ + Conté Crayon… કોન્ટે ક્રેયોન… + Convert રૂપાંતર કરો + Convert All Spot Colors to Process બધા સ્પોટ રંગોને પ્રોસેસમાં રૂપાંતર કરો + Convert All Strokes to Outlines બધા સ્ટ્રોકને આઉટલાઇનમાં રૂપાંતર કરો + Convert All Text to Outlines બધા ટેક્સ્ટને આઉટલાઇનમાં રૂપાંતર કરો + Convert Anchor Point એન્કર પોઇન્ટ રૂપાંતર કરો + Convert Anchor Points એન્કર પોઇન્ટ્સ રૂપાંતર કરો + Convert Color રંગ રૂપાંતર કરો + Convert Document Color Mode દસ્તાવેજ રંગ મોડ રૂપાંતર કરો + Convert Layers to Symbols લેયર્સને સિમ્બોલમાં રૂપાંતર કરો + Convert Process to Global પ્રોસેસને ગ્લોબલમાં રૂપાંતર કરો + Convert Selected Anchor Points to Corner પસંદ કરેલા એન્કર પોઇન્ટ્સને ખૂણામાં રૂપાંતર કરો + Convert Selected Anchor Points to Smooth પસંદ કરેલા એન્કર પોઇન્ટ્સને સ્મૂધમાં રૂપાંતર કરો + Convert To Area Type એરિયા ટાઇપમાં રૂપાંતર કરો + Convert To Point Type પોઇન્ટ ટાઇપમાં રૂપાંતર કરો + Convert text to outlines ટેક્સ્ટને આઉટલાઇનમાં રૂપાંતર કરો + Convert to Artboards આર્ટબોર્ડમાં રૂપાંતર કરો + Convert to CMYK CMYK માં રૂપાંતર કરો + Convert to Grayscale ગ્રેસ્કેલમાં રૂપાંતર કરો + Convert to Parametric Shape પેરામેટ્રિક આકારમાં રૂપાંતર કરો + Convert to RGB RGB માં રૂપાંતર કરો + Convert to Shape આકારમાં રૂપાંતર કરો + Convert to Straight Lines સીધી રેખાઓમાં રૂપાંતર કરો + Convert to Text ટેક્સ્ટમાં રૂપાંતર કરો + Convert to sRGB sRGB માં રૂપાંતર કરો + Convert: રૂપાંતર: + Convex બહિર્ગોળ + Copies per Sheet (as many as fit, in a grid) પ્રતિ શીટ નકલો (ગ્રિડમાં જેટલી બેસે તેટલી) + Copies: નકલો: + Copy નકલ + Copy (make a transformed copy) નકલ (રૂપાંતરિત નકલ બનાવો) + Copy All બધું નકલ કરો + Copy All Styles બધી સ્ટાઇલ નકલ કરો + Copy Appearance દેખાવ નકલ કરો + Copy Artboard આર્ટબોર્ડ નકલ કરો + Copy Color Value (Hex) રંગ મૂલ્યની નકલ કરો (Hex) + Copy Guide ગાઇડ નકલ કરો + Copy Report રિપોર્ટ નકલ કરો + Copy Script Console સ્ક્રિપ્ટ કન્સોલ નકલ કરો + Copy Selected પસંદ કરેલું નકલ કરો + Copy Selected Style પસંદ કરેલી સ્ટાઇલ નકલ કરો + Copy Width Point પહોળાઈ પોઇન્ટ નકલ કરો + Copy in Perspective પર્સ્પેક્ટિવમાં નકલ કરો + Copy into the document's folder દસ્તાવેજના ફોલ્ડરમાં નકલ કરો + Copy only fonts you own or are licensed to install. ફક્ત તમારી માલિકીના અથવા ઇન્સ્ટોલ કરવાના લાઇસન્સવાળા ફોન્ટ્સની નકલ કરો. + Copy to User Defined વપરાશકર્તા-વ્યાખ્યાયિતમાં નકલ કરો + Copyright કૉપીરાઇટ + Copyright Info URL: કૉપીરાઇટ માહિતી URL: + Copyright Notice: કૉપીરાઇટ સૂચના: + Copyright Status: કૉપીરાઇટ સ્થિતિ: + Copyright Symbol કૉપીરાઇટ ચિહ્ન + Coral કોરલ + Corner ખૂણો + Corner Point Angle Threshold: ખૂણા પોઇન્ટ ખૂણા મર્યાદા: + Corner Radius ખૂણાની ત્રિજ્યા + Corner Radius: ખૂણાની ત્રિજ્યા: + Corner Tool ખૂણા ટૂલ + Corner: ખૂણો: + Corners ખૂણાઓ + Corners selected: {count} પસંદ કરેલા ખૂણા: {count} + Corners… ખૂણાઓ… + Cove કોવ + Craquelure… ક્રેક્લ્યુર… + Create બનાવો + Create Clipping Mask ક્લિપિંગ માસ્ક બનાવો + Create Compound Path From Ellipses અંડાકારમાંથી કમ્પાઉન્ડ પાથ બનાવો + Create Dimension ડાયમેન્શન બનાવો + Create From: આમાંથી બનાવો: + Create Gradient Mesh ગ્રેડિયન્ટ મેશ બનાવો + Create Gradient Mesh… ગ્રેડિયન્ટ મેશ બનાવો… + Create Graph ગ્રાફ બનાવો + Create New Layer નવો લેયર બનાવો + Create New Library નવી લાઇબ્રેરી બનાવો + Create New Sublayer નવો સબલેયર બનાવો + Create New Swatch… નવો સ્વેચ બનાવો… + Create Object Mosaic ઑબ્જેક્ટ મોઝેઇક બનાવો + Create Object Mosaic… ઑબ્જેક્ટ મોઝેઇક બનાવો… + Create Outlines આઉટલાઇન બનાવો + Create PDF layers from layers and sublayers લેયર અને સબલેયરમાંથી PDF લેયર બનાવો + Create PDF-Compatible File PDF-સુસંગત ફાઇલ બનાવો + Create Path પાથ બનાવો + Create Slice સ્લાઇસ બનાવો + Create Slice from Selection પસંદગીમાંથી સ્લાઇસ બનાવો + Create Slices from Guides ગાઇડ્સમાંથી સ્લાઇસ બનાવો + Create Sub-folders સબ-ફોલ્ડર બનાવો + Create Swatch Legend સ્વેચ લિજેન્ડ બનાવો + Create Swatch Legend… સ્વેચ લિજેન્ડ બનાવો… + Create Text ટેક્સ્ટ બનાવો + Create Trim Marks ટ્રિમ માર્ક્સ બનાવો + Create from Guides ગાઇડ્સમાંથી બનાવો + Create from Selection પસંદગીમાંથી બનાવો + Created બનાવેલ + Created: બનાવેલ: + Crisp ક્રિસ્પ + Croatian ક્રોએશિયન + Crop ક્રોપ + Crop Image ઇમેજ ક્રોપ કરો + Crop Marks ક્રોપ માર્ક્સ + Crop To આના પર ક્રોપ કરો + Crosshatch… ક્રોસહૅચ… + Crystallize ક્રિસ્ટલાઇઝ + Crystallize Tool ક્રિસ્ટલાઇઝ ટૂલ + Crystallize… ક્રિસ્ટલાઇઝ… + Current Brush વર્તમાન બ્રશ + Current Colors ({total}) વર્તમાન રંગો ({total}) + Current Symbol વર્તમાન સિમ્બોલ + Cursor Swatch Preview કર્સર સ્વેચ પૂર્વાવલોકન + Curvature વક્રતા + Curvature Tool વક્રતા ટૂલ + Curve વક્ર + Curved વક્ર + Curves… કર્વ્સ… + Custom કસ્ટમ + Customize… કસ્ટમાઇઝ કરો… + Cut કાપો + Cut Artboard આર્ટબોર્ડ કાપો + Cut Path પાથ કાપો + Cut Path at Selected Anchor Points પસંદ કરેલા એન્કર પોઇન્ટ પર પાથ કાપો + Cutout… કટઆઉટ… + Czech ચેક + DXF Import Options DXF આયાત વિકલ્પો + DXF Info DXF માહિતી + DXF Options DXF વિકલ્પો + DXF Options Dialog DXF વિકલ્પો ડાયલોગ + Danish ડેનિશ + Dark ડાર્ક + Dark Gray ડાર્ક ગ્રે + Dark Rough ડાર્ક રફ + Dark Strokes… ડાર્ક સ્ટ્રોક્સ… + Darken ઘાટું કરો + Darkness ઘાટાપણું + Dash ડૅશ + Dash: ડૅશ: + Dashed ડૅશવાળું + Dashed Line ડૅશવાળી રેખા + Dashes to Geometry… ડૅશને જ્યામિતિમાં ફેરવો… + Data Recovery ડેટા પુનઃપ્રાપ્તિ + Data Set ડેટા સેટ + Data Sets ડેટા સેટ્સ + Data… ડેટા… + Dates તારીખો + De-Interlace… ડી-ઇન્ટરલેસ… + Decimal-Justified Tab દશાંશ-સંરેખિત ટેબ + Decimal: દશાંશ: + Decorative સુશોભિત + Decrease Font Size ફોન્ટ કદ ઘટાડો + Decrease Font Size by Five Steps ફોન્ટ કદ પાંચ પગલાં ઘટાડો + Deep Ungroup ડીપ અનગ્રૂપ + Deep Ungroup… ડીપ અનગ્રૂપ… + Default ડિફૉલ્ટ + Default Fill and Stroke ડિફૉલ્ટ ભરો અને સ્ટ્રોક + Default Fill and Stroke (D) ડિફૉલ્ટ ભરો અને સ્ટ્રોક (D) + Default Language ડિફૉલ્ટ ભાષા + Default Swatches ડિફૉલ્ટ સ્વેચ + Deferred Effects સ્થગિત અસરો + Define Grid… ગ્રિડ વ્યાખ્યાયિત કરો… + Define Perspective Grid પર્સ્પેક્ટિવ ગ્રિડ વ્યાખ્યાયિત કરો + Define Perspective Grid… પર્સ્પેક્ટિવ ગ્રિડ વ્યાખ્યાયિત કરો… + Define a variable first પહેલા ચલ વ્યાખ્યાયિત કરો + Delete કાઢી નાખો + Delete Action Sets ક્રિયા સેટ કાઢી નાખો + Delete Action Step ક્રિયા પગલું કાઢી નાખો + Delete All બધું કાઢી નાખો + Delete All Slices બધી સ્લાઇસ કાઢી નાખો + Delete Anchor Point એન્કર પોઇન્ટ કાઢી નાખો + Delete Anchor Point Tool એન્કર પોઇન્ટ કાઢી નાખો ટૂલ + Delete Anchor Points એન્કર પોઇન્ટ્સ કાઢી નાખો + Delete Artboard આર્ટબોર્ડ કાઢી નાખો + Delete Artboards આર્ટબોર્ડ્સ કાઢી નાખો + Delete Brush બ્રશ કાઢી નાખો + Delete Character Style અક્ષર સ્ટાઇલ કાઢી નાખો + Delete Data Set ડેટા સેટ કાઢી નાખો + Delete Data Sets ડેટા સેટ્સ કાઢી નાખો + Delete Design ડિઝાઇન કાઢી નાખો + Delete Empty Artboards ખાલી આર્ટબોર્ડ કાઢી નાખો + Delete Export Preset નિકાસ પ્રીસેટ કાઢી નાખો + Delete Font ફોન્ટ કાઢી નાખો + Delete Freeform Point ફ્રીફોર્મ પોઇન્ટ કાઢી નાખો + Delete Gradient Preset ગ્રેડિયન્ટ પ્રીસેટ કાઢી નાખો + Delete Graphic Style ગ્રાફિક સ્ટાઇલ કાઢી નાખો + Delete Graphic Style (Alt-click: without asking) ગ્રાફિક સ્ટાઇલ કાઢી નાખો (Alt-ક્લિક: પૂછ્યા વગર) + Delete Graphic Styles ગ્રાફિક સ્ટાઇલ્સ કાઢી નાખો + Delete Guide ગાઇડ કાઢી નાખો + Delete Guides ગાઇડ્સ કાઢી નાખો + Delete Image Trace Preset ઇમેજ ટ્રેસ પ્રીસેટ કાઢી નાખો + Delete Instances ઇન્સ્ટન્સ કાઢી નાખો + Delete Library લાઇબ્રેરી કાઢી નાખો + Delete Library Group લાઇબ્રેરી ગ્રૂપ કાઢી નાખો + Delete Library Item લાઇબ્રેરી આઇટમ કાઢી નાખો + Delete Light લાઇટ કાઢી નાખો + Delete Mesh Point મેશ પોઇન્ટ કાઢી નાખો + Delete PDF Preset PDF પ્રીસેટ કાઢી નાખો + Delete Paragraph Style ફકરા સ્ટાઇલ કાઢી નાખો + Delete Pattern પેટર્ન કાઢી નાખો + Delete Perspective Grid Preset પર્સ્પેક્ટિવ ગ્રિડ પ્રીસેટ કાઢી નાખો + Delete Point પોઇન્ટ કાઢી નાખો + Delete Preset પ્રીસેટ કાઢી નાખો + Delete Print Preset પ્રિન્ટ પ્રીસેટ કાઢી નાખો + Delete Profile પ્રોફાઇલ કાઢી નાખો + Delete SVG Filter SVG ફિલ્ટર કાઢી નાખો + Delete Save for Web Preset વેબ માટે સાચવો પ્રીસેટ કાઢી નાખો + Delete Selected Item પસંદ કરેલી આઇટમ કાઢી નાખો + Delete Selection પસંદગી કાઢી નાખો + Delete Slice સ્લાઇસ કાઢી નાખો + Delete Slices સ્લાઇસ કાઢી નાખો + Delete Spine Point સ્પાઇન પોઇન્ટ કાઢી નાખો + Delete Stop સ્ટોપ કાઢી નાખો + Delete Style સ્ટાઇલ કાઢી નાખો + Delete Styles સ્ટાઇલ્સ કાઢી નાખો + Delete Swatch સ્વેચ કાઢી નાખો + Delete Symbol સિમ્બોલ કાઢી નાખો + Delete Symbols સિમ્બોલ્સ કાઢી નાખો + Delete Theme થીમ કાઢી નાખો + Delete Toolbar ટૂલબાર કાઢી નાખો + Delete Transparency Flattener Preset ટ્રાન્સપરન્સી ફ્લેટનર પ્રીસેટ કાઢી નાખો + Delete Variable ચલ કાઢી નાખો + Delete Variables ચલો કાઢી નાખો + Delete View વ્યૂ કાઢી નાખો + Delete Width Point પહોળાઈ પોઇન્ટ કાઢી નાખો + Delete Workspace વર્કસ્પેસ કાઢી નાખો + Delete from Custom Dictionary કસ્ટમ ડિક્શનરીમાંથી કાઢી નાખો + Delete the graphic style “{name}”? ગ્રાફિક સ્ટાઇલ “{name}” કાઢી નાખીએ? + Delete the library “{name}”? લાઇબ્રેરી “{name}” કાઢી નાખીએ? + Delete the selected brushes? પસંદ કરેલા બ્રશ કાઢી નાખીએ? + Delete the selected symbol? પસંદ કરેલો સિમ્બોલ કાઢી નાખીએ? + Delete the selected symbols? પસંદ કરેલા સિમ્બોલ કાઢી નાખીએ? + Delete theme થીમ કાઢી નાખો + Delete this saved preset આ સાચવેલો પ્રીસેટ કાઢી નાખો + Delete “{name}”? “{name}” કાઢી નાખીએ? + Depth ઊંડાઈ + Depth: ઊંડાઈ: + Desaturate અસંતૃપ્ત કરો + Description વર્ણન + Description: વર્ણન: + Deselect પસંદગી રદ કરો + Design… ડિઝાઇન… + Desired ઇચ્છિત + Destination Folder: ગંતવ્ય ફોલ્ડર: + Destination: ગંતવ્ય: + Detail: વિગત: + Devices ડિવાઇસ + Dialog ડાયલોગ + Diameter: વ્યાસ: + Diamond ડાયમંડ + Difference ડિફરન્સ + Differences from {preset}: {preset} થી તફાવત: + Diffuse ડિફ્યુઝ + Diffuse Glow… ડિફ્યુઝ ગ્લો… + Digits અંકો + Dim Copies to: ઝાંખી નકલો: + Dim Images to: ઝાંખી છબીઓ: + Dimension પરિમાણ + Dimension Tool પરિમાણ ટૂલ + Dimension Tool Options પરિમાણ ટૂલ વિકલ્પો + Dimensions પરિમાણો + Direct Selection Tool ડાયરેક્ટ સિલેક્શન ટૂલ + Direct-select anchor points to edit them. સંપાદિત કરવા એન્કર પોઇન્ટ્સ ડાયરેક્ટ-સિલેક્ટ કરો. + Direction Handles દિશા હેન્ડલ્સ + Direction: દિશા: + Directives નિર્દેશો + Disable Auto Add/Delete ઑટો ઍડ/ડિલીટ બંધ કરો + Disable Opacity Mask ઓપેસિટી માસ્ક બંધ કરો + Disable and Continue બંધ કરો અને ચાલુ રાખો + Disabled plug-ins બંધ કરેલા પ્લગ-ઇન્સ + Discard કાઢી નાખો + Discard Empty Type ખાલી ટાઇપ કાઢી નાખો + Discard Recovered Documents પુનઃપ્રાપ્ત દસ્તાવેજો કાઢી નાખો + Discard White Overprint in Output આઉટપુટમાં સફેદ ઓવરપ્રિન્ટ કાઢી નાખો + Discretionary Hyphen વૈકલ્પિક હાઇફન + Discretionary Ligatures વૈકલ્પિક લિગેચર્સ + Display All Blacks Accurately બધા બ્લેક્સ ચોક્કસ રીતે બતાવો + Display All Blacks as Rich Black બધા બ્લેક્સ રિચ બ્લેક તરીકે બતાવો + Display Bitmaps as Anti-aliased Images in Pixel Preview પિક્સેલ પ્રિવ્યૂમાં બિટમેપ્સ એન્ટી-એલિઆસ્ડ છબીઓ તરીકે બતાવો + Display Options પ્રદર્શન વિકલ્પો + Display Print Size at 100% Zoom 100% ઝૂમ પર પ્રિન્ટ કદ બતાવો + Display name પ્રદર્શન નામ + Distort વિકૃત કરો + Distort & Transform વિકૃત કરો અને રૂપાંતરિત કરો + Distort Appearance દેખાવ વિકૃત કરો + Distort Linear Gradients લીનિયર ગ્રેડિયન્ટ્સ વિકૃત કરો + Distort Pattern Fills પેટર્ન ફિલ્સ વિકૃત કરો + Distortion વિકૃતિ + Distribute વિતરિત કરો + Distribute Evenly સમાન રીતે વિતરિત કરો + Distribute Objects: ઑબ્જેક્ટ્સ વિતરિત કરો: + Distribute Spacing અંતર વિતરિત કરો + Distribute Spacing: અંતર વિતરિત કરો: + Dither ડિથર + Dither: ડિથર: + Divide વિભાજિત કરો + Divide Horizontally Into આડી રીતે આમાં વિભાજિત કરો + Divide Objects Below નીચેના ઑબ્જેક્ટ્સ વિભાજિત કરો + Divide Slices સ્લાઇસ વિભાજિત કરો + Divide Slices… સ્લાઇસ વિભાજિત કરો… + Divide Vertically Into ઊભી રીતે આમાં વિભાજિત કરો + Divide and Outline Will Remove Unpainted Artwork "વિભાજિત કરો અને રૂપરેખા" રંગ વગરની આર્ટવર્ક દૂર કરશે + Divide into Dashes ડેશમાં વિભાજિત કરો + Divide into Dashes… ડેશમાં વિભાજિત કરો… + Do not show this warning again આ ચેતવણી ફરી બતાવશો નહીં + Do you want to save the changes you made to “{name}”? તમે "{name}" માં કરેલા ફેરફારો સાચવવા માંગો છો? + Dock Control Bar ડોક કંટ્રોલ બાર + Dock Panel ડોક પેનલ + Dock to Bottom નીચે ડોક કરો + Dock to Top ઉપર ડોક કરો + Document દસ્તાવેજ + Document Color Mode દસ્તાવેજ કલર મોડ + Document Health Check દસ્તાવેજ હેલ્થ ચેક + Document Info દસ્તાવેજ માહિતી + Document Ink Options દસ્તાવેજ ઇન્ક વિકલ્પો + Document JSON દસ્તાવેજ JSON + Document Library દસ્તાવેજ લાઇબ્રેરી + Document Name દસ્તાવેજ નામ + Document Raster Effects Settings દસ્તાવેજ રાસ્ટર ઇફેક્ટ્સ સેટિંગ્સ + Document Raster Effects Settings Dialog દસ્તાવેજ રાસ્ટર ઇફેક્ટ્સ સેટિંગ્સ ડાયલૉગ + Document Raster Effects Settings… દસ્તાવેજ રાસ્ટર ઇફેક્ટ્સ સેટિંગ્સ… + Document Setup દસ્તાવેજ સેટઅપ + Document Setup… દસ્તાવેજ સેટઅપ… + Document Title: દસ્તાવેજ શીર્ષક: + Document Tools દસ્તાવેજ ટૂલ્સ + Document model દસ્તાવેજ મોડેલ + Document open password દસ્તાવેજ ખોલવાનો પાસવર્ડ + Documents you create appear here. તમે બનાવેલા દસ્તાવેજો અહીં દેખાશે. + Don't Relink ફરીથી લિંક કરશો નહીં + Don't Save સાચવશો નહીં + Don't Show Center સેન્ટર બતાવશો નહીં + Done પૂર્ણ + Done Editing Pattern પેટર્ન સંપાદન પૂર્ણ + Dot Color from Art આર્ટમાંથી ડોટ રંગ + Dots ટપકાં + Double Click To Isolate અલગ કરવા ડબલ ક્લિક કરો + Double Column ડબલ કૉલમ + Double Left Quotation Marks ડબલ ડાબા અવતરણ ચિહ્નો + Double Quotes: ડબલ અવતરણ ચિહ્નો: + Double Right Quotation Marks ડબલ જમણા અવતરણ ચિહ્નો + Double-click ડબલ-ક્લિક + Double-click to edit સંપાદિત કરવા ડબલ-ક્લિક કરો + Double-click to edit the characters' paint અક્ષરોનો પેઇન્ટ સંપાદિત કરવા ડબલ-ક્લિક કરો + Double-click to insert દાખલ કરવા ડબલ-ક્લિક કરો + Double-click to rename નામ બદલવા ડબલ-ક્લિક કરો + Double-click to replace બદલવા ડબલ-ક્લિક કરો + Double-click to target the contents સામગ્રી લક્ષ્ય બનાવવા ડબલ-ક્લિક કરો + Download: ડાઉનલોડ: + Drag ખેંચો + Drag Direction ખેંચવાની દિશા + Drag a handle હેન્ડલ ખેંચો + Drag an item here, or right-click one, to put it in this group. આ જૂથમાં મૂકવા અહીં આઇટમ ખેંચો, અથવા કોઈ એક પર જમણું-ક્લિક કરો. + Drag art here, or select it and click +. Hold Alt for one asset of several objects. અહીં આર્ટ ખેંચો, અથવા તેને પસંદ કરીને + પર ક્લિક કરો. ઘણા ઑબ્જેક્ટ્સમાંથી એક એસેટ માટે Alt દબાવી રાખો. + Drag art or swatches here, or use + to add the selection's colors and text styles. અહીં આર્ટ અથવા સ્વેચ ખેંચો, અથવા પસંદગીના રંગો અને ટેક્સ્ટ સ્ટાઇલ ઉમેરવા + વાપરો. + Drag onto art to apply this appearance આ દેખાવ લાગુ કરવા આર્ટ પર ખેંચો + Drag rings to rotate. Shift snaps to 15°. ફેરવવા રિંગ્સ ખેંચો. Shift 15° પર સ્નેપ કરે છે. + Drag the art to move it on the paper કાગળ પર ખસેડવા આર્ટ ખેંચો + Drag the light, or its rings. Shift snaps to 15°. લાઇટ અથવા તેની રિંગ્સ ખેંચો. Shift 15° પર સ્નેપ કરે છે. + Drag to float as a toolbar ટૂલબાર તરીકે ફ્લોટ કરવા ખેંચો + Drag to float the toolbar ટૂલબાર ફ્લોટ કરવા ખેંચો + Drag to move ખસેડવા ખેંચો + Drag to the window's left or right edge to dock ડોક કરવા વિન્ડોની ડાબી કે જમણી કિનારી પર ખેંચો + Drag values or labels. Double-click a value to type. મૂલ્યો અથવા લેબલ્સ ખેંચો. ટાઇપ કરવા મૂલ્ય પર ડબલ-ક્લિક કરો. + Draw દોરો + Draw Dashed Lines as Solid Lines ડેશ્ડ રેખાઓ સોલિડ રેખાઓ તરીકે દોરો + Draw Hidden Faces છુપાયેલી સપાટીઓ દોરો + Draw in Perspective પર્સ્પેક્ટિવમાં દોરો + Draw: દોરો: + Drawer menu ડ્રોઅર મેનુ + Drawing : real size — 1:10 labels 1 unit on the page as 10. ડ્રોઇંગ: વાસ્તવિક કદ — 1:10 પૃષ્ઠ પર 1 એકમને 10 તરીકે લેબલ કરે છે. + Drawing Mode ડ્રોઇંગ મોડ + Drop Artwork આર્ટવર્ક ડ્રોપ કરો + Drop Shadow… ડ્રોપ શેડો… + Drop here to take it out of its group તેને જૂથમાંથી બહાર કાઢવા અહીં ડ્રોપ કરો + Dry Brush… ડ્રાય બ્રશ… + Duotone Gradients ડ્યુઓટોન ગ્રેડિયન્ટ્સ + Duotone… ડ્યુઓટોન… + Duplicate ડુપ્લિકેટ + Duplicate Action or Action Set ઍક્શન અથવા ઍક્શન સેટની ડુપ્લિકેટ + Duplicate Artboard આર્ટબોર્ડ ડુપ્લિકેટ + Duplicate Artboards આર્ટબોર્ડ્સ ડુપ્લિકેટ + Duplicate Brush બ્રશ ડુપ્લિકેટ + Duplicate Character Style કૅરેક્ટર સ્ટાઇલ ડુપ્લિકેટ + Duplicate Effect ઇફેક્ટ ડુપ્લિકેટ + Duplicate Effects ઇફેક્ટ્સ ડુપ્લિકેટ + Duplicate Graphic Style ગ્રાફિક સ્ટાઇલ ડુપ્લિકેટ + Duplicate Item આઇટમ ડુપ્લિકેટ + Duplicate Paragraph Style પેરેગ્રાફ સ્ટાઇલ ડુપ્લિકેટ + Duplicate Rows પંક્તિઓ ડુપ્લિકેટ + Duplicate Selected Item પસંદ કરેલી આઇટમ ડુપ્લિકેટ + Duplicate Selection પસંદગી ડુપ્લિકેટ + Duplicate Slice સ્લાઇસ ડુપ્લિકેટ + Duplicate Swatch સ્વેચ ડુપ્લિકેટ + Duplicate Symbol સિમ્બોલ ડુપ્લિકેટ + Duplicate Toolbar ટૂલબાર ડુપ્લિકેટ + Dutch ડચ + Dynamic Symbol ડાયનેમિક સિમ્બોલ + EPS Options EPS વિકલ્પો + EPS Options Dialog EPS વિકલ્પો ડાયલૉગ + Each Design Represents: દરેક ડિઝાઇન દર્શાવે છે: + Each data set saves over the same file, which keeps only the last data set. દરેક ડેટા સેટ એક જ ફાઇલ પર સાચવાય છે, જેમાં માત્ર છેલ્લો ડેટા સેટ રહે છે. + Each page prints as one image, at the document's raster effects resolution. દરેક પૃષ્ઠ દસ્તાવેજના રાસ્ટર ઇફેક્ટ્સ રિઝોલ્યુશન પર એક છબી તરીકે પ્રિન્ટ થાય છે. + Earth Tones અર્થ ટોન્સ + Ease In ઇઝ ઇન + Ease In Out ઇઝ ઇન આઉટ + Ease Out ઇઝ આઉટ + Ease Out In ઇઝ આઉટ ઇન + East Asian Type પૂર્વ એશિયાઈ ટાઇપ + Edit સંપાદિત કરો + Edit Artboards આર્ટબોર્ડ્સ સંપાદિત કરો + Edit Character Style કૅરેક્ટર સ્ટાઇલ સંપાદિત કરો + Edit Clipping Path ક્લિપિંગ પાથ સંપાદિત કરો + Edit Color Group કલર ગ્રૂપ સંપાદિત કરો + Edit Colors રંગો સંપાદિત કરો + Edit Contents સામગ્રી સંપાદિત કરો + Edit Custom Dictionary કસ્ટમ શબ્દકોશ સંપાદિત કરો + Edit Custom Dictionary… કસ્ટમ શબ્દકોશ સંપાદિત કરો… + Edit Data Set ડેટા સેટ સંપાદિત કરો + Edit Data Set… ડેટા સેટ સંપાદિત કરો… + Edit Envelope એન્વેલોપ સંપાદિત કરો + Edit Freeform Point ફ્રીફોર્મ પોઇન્ટ સંપાદિત કરો + Edit Gradient ગ્રેડિયન્ટ સંપાદિત કરો + Edit Opacity Mask ઓપેસિટી માસ્ક સંપાદિત કરો + Edit Original મૂળ સંપાદિત કરો + Edit PDF Preset PDF પ્રીસેટ સંપાદિત કરો + Edit Parametric Shape પેરામેટ્રિક આકાર સંપાદિત કરો + Edit Path પાથ સંપાદિત કરો + Edit Pattern પેટર્ન સંપાદિત કરો + Edit Perspective Grid પર્સ્પેક્ટિવ ગ્રિડ સંપાદિત કરો + Edit Print Preset પ્રિન્ટ પ્રીસેટ સંપાદિત કરો + Edit SVG Filter SVG ફિલ્ટર સંપાદિત કરો + Edit Selection પસંદગી સંપાદિત કરો + Edit Selection… પસંદગી સંપાદિત કરો… + Edit Symbol સિમ્બોલ સંપાદિત કરો + Edit Text ટેક્સ્ટ સંપાદિત કરો + Edit Theme થીમ સંપાદિત કરો + Edit Toolbar ટૂલબાર સંપાદિત કરો + Edit Views વ્યૂઝ સંપાદિત કરો + Edit Views… વ્યૂઝ સંપાદિત કરો… + Edit or Apply Color Group કલર ગ્રૂપ સંપાદિત કરો અથવા લાગુ કરો + Edit or Apply Color Group… કલર ગ્રૂપ સંપાદિત કરો અથવા લાગુ કરો… + Edit or Apply Colors રંગો સંપાદિત કરો અથવા લાગુ કરો + Edit selected paths within: પસંદ કરેલા પાથ આમાં સંપાદિત કરો: + Edit the applied effect, or add another one? લાગુ ઇફેક્ટ સંપાદિત કરીએ, અથવા બીજી ઉમેરીએ? + Edit the point પોઇન્ટ સંપાદિત કરો + Edit the stop સ્ટોપ સંપાદિત કરો + Edit theme થીમ સંપાદિત કરો + Editable સંપાદનયોગ્ય + Edit… સંપાદિત કરો… + Effect ઇફેક્ટ + Effect Gallery ઇફેક્ટ ગેલેરી + Effect Gallery Dialog ઇફેક્ટ ગેલેરી ડાયલૉગ + Effect Gallery… ઇફેક્ટ ગેલેરી… + Effect Options ઇફેક્ટ વિકલ્પો + Effective PPI અસરકારક PPI + Effects ઇફેક્ટ્સ + Effect… ઇફેક્ટ… + Ellipse એલિપ્સ + Ellipse Properties: એલિપ્સ ગુણધર્મો: + Ellipse Tool એલિપ્સ ટૂલ + Ellipse… એલિપ્સ… + Ellipsis એલિપ્સિસ + Em Box Em બૉક્સ + Em Box Bottom/Left Em બૉક્સ નીચે/ડાબે + Em Box Center Em બૉક્સ કેન્દ્ર + Em Box Top/Right Em બૉક્સ ઉપર/જમણે + Em Dash Em ડૅશ + Em Space Em સ્પેસ + Embed એમ્બેડ કરો + Embed Fonts (Glyphs Used) ફૉન્ટ્સ એમ્બેડ કરો (વપરાયેલા ગ્લિફ્સ) + Embed Fonts (for other applications) ફૉન્ટ્સ એમ્બેડ કરો (અન્ય ઍપ્લિકેશન્સ માટે) + Embed ICC Profile ICC પ્રોફાઇલ એમ્બેડ કરો + Embed ICC Profiles ICC પ્રોફાઇલ્સ એમ્બેડ કરો + Embed Image છબી એમ્બેડ કરો + Embed Image(s) છબી(ઓ) એમ્બેડ કરો + Embed page thumbnails પૃષ્ઠ થંબનેલ્સ એમ્બેડ કરો + Embedded એમ્બેડ કરેલું + Embedded Image એમ્બેડ કરેલી છબી + Emulsion: ઇમલ્શન: + En Dash En ડૅશ + En Space En સ્પેસ + Enable Background Export બૅકગ્રાઉન્ડ એક્સપોર્ટ ચાલુ કરો + Enable Background Save બૅકગ્રાઉન્ડ સેવ ચાલુ કરો + Enable Contextual Task Bar કૉન્ટેક્સ્ચ્યુઅલ ટાસ્ક બાર ચાલુ કરો + Enable Guides for 9-Slice Scaling 9-સ્લાઇસ સ્કેલિંગ માટે ગાઇડ્સ ચાલુ કરો + Enable Missing Glyph Protection ગુમ ગ્લિફ સુરક્ષા ચાલુ કરો + Enable Opacity Mask ઓપેસિટી માસ્ક ચાલુ કરો + Enable Rubber Band for આ માટે રબર બૅન્ડ ચાલુ કરો + Enable Touch Gestures ટચ ગેસ્ચર ચાલુ કરો + Enable Touch Workspace ટચ વર્કસ્પેસ ચાલુ કરો + Enable copying of text, images and other content ટેક્સ્ટ, છબીઓ અને અન્ય સામગ્રીની નકલ ચાલુ કરો + Enable in-menu font previews મેનૂમાં ફૉન્ટ પ્રિવ્યૂ ચાલુ કરો + Enable on Re-launch ફરી લૉન્ચ થાય ત્યારે ચાલુ કરો + Enable plaintext metadata પ્લેનટેક્સ્ટ મેટાડેટા ચાલુ કરો + Enable text access for screen readers સ્ક્રીન રીડર્સ માટે ટેક્સ્ટ ઍક્સેસ ચાલુ કરો + Enclosing Lasso એન્ક્લોઝિંગ લાસો + Enclosing Lasso Tool એન્ક્લોઝિંગ લાસો ટૂલ + Encoding એન્કોડિંગ + Encoding and writing the file… ફાઇલ એન્કોડ કરી લખાઈ રહી છે… + Encoding: એન્કોડિંગ: + Encryption: {name} (set by Compatibility) એન્ક્રિપ્શન: {name} (કોમ્પેટિબિલિટી દ્વારા સેટ) + End Tile ટાઇલ સમાપ્ત કરો + End Undo Group અનડુ ગ્રૂપ સમાપ્ત કરો + End arrowhead અંતિમ તીર + End: અંત: + Ended at a host limit હોસ્ટ મર્યાદા પર સમાપ્ત + Ended at a limit મર્યાદા પર સમાપ્ત + Ended by an internal fault આંતરિક ખામીથી સમાપ્ત + Ended with an error ભૂલ સાથે સમાપ્ત + English: UK અંગ્રેજી: UK + English: USA અંગ્રેજી: USA + Enter દાખલ કરો + Enter Isolation Mode આઇસોલેશન મોડ દાખલ કરો + Entry: પ્રવેશ: + Envelope Distort એન્વેલોપ ડિસ્ટોર્ટ + Envelope Info એન્વેલોપ માહિતી + Envelope Mesh એન્વેલોપ મેશ + Envelope Options એન્વેલોપ વિકલ્પો + Envelope Options… એન્વેલોપ વિકલ્પો… + Equalize Handles હેન્ડલ્સ સમાન કરો + Eraser ઇરેઝર + Eraser Tool ઇરેઝર ટૂલ + Error ભૂલ + Esc Esc + Essentials એસેન્શિયલ્સ + Essentials Classic એસેન્શિયલ્સ ક્લાસિક + Event: ઇવેન્ટ: + Every (minutes) દર (મિનિટ) + Every option is at its default. દરેક વિકલ્પ ડિફૉલ્ટ પર છે. + Every option matches {base}. દરેક વિકલ્પ {base} સાથે મેળ ખાય છે. + Every-line Composer એવરી-લાઇન કમ્પોઝર + Exact dash lengths ચોક્કસ ડેશ લંબાઈ + Exceptions (comma separated) અપવાદો (અલ્પવિરામથી અલગ) + Exclude બાકાત રાખો + Exclusion બાકાત + Exit બહાર નીકળો + Exit Isolation Mode આઇસોલેશન મોડમાંથી બહાર નીકળો + Exit Symbol Editing Mode સિમ્બોલ સંપાદન મોડમાંથી બહાર નીકળો + Expand વિસ્તૃત કરો + Expand Appearance દેખાવ વિસ્તૃત કરો + Expand Appearance first (Object › Expand Appearance) પહેલા દેખાવ વિસ્તૃત કરો (ઑબ્જેક્ટ › દેખાવ વિસ્તૃત કરો) + Expand Blend બ્લેન્ડ વિસ્તૃત કરો + Expand Brush Strokes બ્રશ સ્ટ્રોક્સ વિસ્તૃત કરો + Expand Compound Shape કમ્પાઉન્ડ આકાર વિસ્તૃત કરો + Expand Dashes into Paths ડેશને પાથમાં વિસ્તૃત કરો + Expand Envelope એન્વેલોપ વિસ્તૃત કરો + Expand Gradient Mesh ગ્રેડિયન્ટ મેશ વિસ્તૃત કરો + Expand Gradient To ગ્રેડિયન્ટ આ સુધી વિસ્તૃત કરો + Expand Image Trace ઇમેજ ટ્રેસ વિસ્તૃત કરો + Expand Instances ઇન્સ્ટન્સ વિસ્તૃત કરો + Expand Layers Panel Rows લેયર્સ પેનલ પંક્તિઓ વિસ્તૃત કરો + Expand Live Paint લાઇવ પેઇન્ટ વિસ્તૃત કરો + Expand Objects on Path પાથ પરના ઑબ્જેક્ટ્સ વિસ્તૃત કરો + Expand Options વિસ્તૃત વિકલ્પો + Expand Repeat રિપીટ વિસ્તૃત કરો + Expand Shape આકાર વિસ્તૃત કરો + Expand Shaper Group શેપર ગ્રૂપ વિસ્તૃત કરો + Expand: વિસ્તૃત કરો: + Expand… વિસ્તૃત કરો… + Export એક્સપોર્ટ + Export 3D Assets 3D એસેટ્સ એક્સપોર્ટ કરો + Export 3D Object 3D ઑબ્જેક્ટ એક્સપોર્ટ કરો + Export All Open Documents બધા ખુલ્લા દસ્તાવેજો એક્સપોર્ટ કરો + Export All Open Documents as PDF બધા ખુલ્લા દસ્તાવેજો PDF તરીકે એક્સપોર્ટ કરો + Export All Open Documents as SVG બધા ખુલ્લા દસ્તાવેજો SVG તરીકે એક્સપોર્ટ કરો + Export All… બધું એક્સપોર્ટ કરો… + Export Artboard આર્ટબોર્ડ એક્સપોર્ટ કરો + Export As આ રીતે એક્સપોર્ટ કરો + Export As PNG… PNG તરીકે એક્સપોર્ટ કરો… + Export As SVG… SVG તરીકે એક્સપોર્ટ કરો… + Export Assets એસેટ્સ એક્સપોર્ટ કરો + Export As… આ રીતે એક્સપોર્ટ કરો… + Export CSS CSS એક્સપોર્ટ કરો + Export CSS… CSS એક્સપોર્ટ કરો… + Export Composite Fonts કમ્પોઝિટ ફૉન્ટ્સ એક્સપોર્ટ કરો + Export DXF DXF એક્સપોર્ટ કરો + Export Data Sets… ડેટા સેટ એક્સપોર્ટ કરો… + Export Document દસ્તાવેજ એક્સપોર્ટ કરો + Export EPS EPS એક્સપોર્ટ કરો + Export Export Presets એક્સપોર્ટ પ્રીસેટ્સ એક્સપોર્ટ કરો + Export Keyboard Shortcuts as Text… કીબોર્ડ શૉર્ટકટ્સ ટેક્સ્ટ તરીકે એક્સપોર્ટ કરો… + Export Keyboard Shortcuts… કીબોર્ડ શૉર્ટકટ્સ એક્સપોર્ટ કરો… + Export Library લાઇબ્રેરી એક્સપોર્ટ કરો + Export Option એક્સપોર્ટ વિકલ્પ + Export PDF PDF એક્સપોર્ટ કરો + Export PDF Presets PDF પ્રીસેટ્સ એક્સપોર્ટ કરો + Export PDFs As: PDF આ રીતે એક્સપોર્ટ કરો: + Export Perspective Grid Presets પર્સ્પેક્ટિવ ગ્રિડ પ્રીસેટ્સ એક્સપોર્ટ કરો + Export Preferences પસંદગીઓ એક્સપોર્ટ કરો + Export Preferences… પસંદગીઓ એક્સપોર્ટ કરો… + Export Presets પ્રીસેટ્સ એક્સપોર્ટ કરો + Export Preset… પ્રીસેટ એક્સપોર્ટ કરો… + Export Print Presets પ્રિન્ટ પ્રીસેટ્સ એક્સપોર્ટ કરો + Export Selected Art Only ફક્ત પસંદ કરેલી આર્ટ એક્સપોર્ટ કરો + Export Selected CSS… પસંદ કરેલ CSS એક્સપોર્ટ કરો… + Export Selection… પસંદગી એક્સપોર્ટ કરો… + Export Settings એક્સપોર્ટ સેટિંગ્સ + Export Transparency Flattener Presets ટ્રાન્સપરન્સી ફ્લેટનર પ્રીસેટ્સ એક્સપોર્ટ કરો + Export Type: એક્સપોર્ટ પ્રકાર: + Export and Clipboard Transparency Flattener Settings એક્સપોર્ટ અને ક્લિપબોર્ડ ટ્રાન્સપરન્સી ફ્લેટનર સેટિંગ્સ + Export for Screens સ્ક્રીન માટે એક્સપોર્ટ કરો + Export for Screens Settings સ્ક્રીન માટે એક્સપોર્ટ સેટિંગ્સ + Export for Screens… સ્ક્રીન માટે એક્સપોર્ટ કરો… + Export the selected assets (else all of them) પસંદ કરેલી એસેટ્સ એક્સપોર્ટ કરો (નહીંતર બધી) + Export to: આમાં એક્સપોર્ટ કરો: + Export {count} Selected {count} પસંદ કરેલા એક્સપોર્ટ કરો + Export: એક્સપોર્ટ: + Export… એક્સપોર્ટ કરો… + Exposure… એક્સપોઝર… + Extend Path પાથ લંબાવો + Extend Path Tool પાથ લંબાવો ટૂલ + Extend Path… પાથ લંબાવો… + Extrude એક્સ્ટ્રુડ + Extrude & Bevel (Classic)… એક્સ્ટ્રુડ અને બેવલ (ક્લાસિક)… + Extrude & Bevel… એક્સ્ટ્રુડ અને બેવલ… + Eyedropper આઇડ્રોપર + Eyedropper Applies: આઇડ્રોપર લાગુ કરે છે: + Eyedropper Options આઇડ્રોપર વિકલ્પો + Eyedropper Picks Up: આઇડ્રોપર ઉપાડે છે: + Eyedropper Tool આઇડ્રોપર ટૂલ + Eyedropper: click the art to sample the key color આઇડ્રોપર: મુખ્ય રંગ લેવા આર્ટ પર ક્લિક કરો + Eyedropper: click the art to sample the stop's colour આઇડ્રોપર: સ્ટોપનો રંગ લેવા આર્ટ પર ક્લિક કરો + Fade Region Outside Artboard આર્ટબોર્ડ બહારનો વિસ્તાર ઝાંખો કરો + Feather… ફેધર… + Feet ફૂટ + Feet & Inches ફૂટ અને ઇંચ + Fewer Elements ઓછા ઘટકો + Fidelity: ફિડેલિટી: + Figure Space ફિગર સ્પેસ + Figure: ફિગર: + File ફાઇલ + File Format: ફાઇલ ફોર્મેટ: + File Formats ફાઇલ ફોર્મેટ્સ + File Handling ફાઇલ હેન્ડલિંગ + File Info ફાઇલ માહિતી + File Info Dialog ફાઇલ માહિતી ડાયલૉગ + File Info… ફાઇલ માહિતી… + File Name: ફાઇલનું નામ: + File SHA-256: {hash} ફાઇલ SHA-256: {hash} + File or URL: ફાઇલ અથવા URL: + File {n} of {total} ફાઇલ {n} / {total} + File › Place… brings in an image, linked or embedded. ફાઇલ › મૂકો… છબી લિંક્ડ અથવા એમ્બેડ કરેલી લાવે છે. + Files ફાઇલો + Files of a PDF standard can't be password-protected. PDF સ્ટાન્ડર્ડની ફાઇલો પાસવર્ડ-સુરક્ષિત કરી શકાતી નથી. + Files of this standard tag their colours with ICC profiles. આ સ્ટાન્ડર્ડની ફાઇલો તેમના રંગોને ICC પ્રોફાઇલ્સ સાથે ટૅગ કરે છે. + Fill ભરો + Fill & Stroke ભરો અને સ્ટ્રોક + Fill (X), double-click for the Color Picker ભરો (X), કલર પીકર માટે ડબલ-ક્લિક કરો + Fill Arc આર્ક ભરો + Fill Color ભરવાનો રંગ + Fill Grid ગ્રિડ ભરો + Fill Line લાઇન ભરો + Fill New Type Objects With Placeholder Text નવા ટાઇપ ઑબ્જેક્ટ્સ પ્લેસહોલ્ડર ટેક્સ્ટથી ભરો + Fill Rule ફિલ નિયમ + Fill Tint ફિલ ટિન્ટ + Fill and Stroke ભરો અને સ્ટ્રોક + Fill new strokes નવા સ્ટ્રોક ભરો + Fill with Placeholder Text પ્લેસહોલ્ડર ટેક્સ્ટથી ભરો + Fill: ભરો: + Fill: click for swatches, Shift-click for the color mixer ભરો: સ્વેચ માટે ક્લિક કરો, કલર મિક્સર માટે Shift-ક્લિક કરો + Filled Dot ભરેલું ટપકું + Filled Square ભરેલો ચોરસ + Film Grain… ફિલ્મ ગ્રેઇન… + Filter ફિલ્ટર + Filter Layers Panel Rows ફિલ્ટર લેયર્સ પેનલ પંક્તિઓ + Find શોધો + Find Font ફૉન્ટ શોધો + Find Font Files ફૉન્ટ ફાઇલ્સ શોધો + Find Fonts ફૉન્ટ્સ શોધો + Find Fonts in Folder… ફોલ્ડરમાં ફૉન્ટ્સ શોધો… + Find Font… ફૉન્ટ શોધો… + Find In આમાં શોધો + Find Linked Files લિંક્ડ ફાઇલો શોધો + Find Linked Files in Folder… ફોલ્ડરમાં લિંક્ડ ફાઇલો શોધો… + Find Nearest Swatch નજીકનો સ્વેચ શોધો + Find Nearest Swatch… નજીકનો સ્વેચ શોધો… + Find Next આગળ શોધો + Find Objects ઑબ્જેક્ટ્સ શોધો + Find and Replace… શોધો અને બદલો… + Find in Folder… ફોલ્ડરમાં શોધો… + Fine noise hides banding in the gradient fill ઝીણો નોઇઝ ગ્રેડિયન્ટ ફિલમાં બેન્ડિંગ છુપાવે છે + Finished પૂર્ણ થયું + Finnish ફિનિશ + First પહેલું + First Artboard પહેલું આર્ટબોર્ડ + First Line Indent: પ્રથમ લાઇન ઇન્ડેન્ટ: + First Page પહેલું પૃષ્ઠ + First commit પ્રથમ કમિટ + First-line Left Indent પ્રથમ લાઇન ડાબો ઇન્ડેન્ટ + Fisheye… ફિશઆઇ… + Fish… ફિશ… + Fit ફિટ + Fit All બધું ફિટ કરો + Fit All in Window વિન્ડોમાં બધું ફિટ કરો + Fit Artboard in Window વિન્ડોમાં આર્ટબોર્ડ ફિટ કરો + Fit Headline હેડલાઇન ફિટ કરો + Fit dashes to corners and ends ડેશને ખૂણાઓ અને છેડા પર ફિટ કરો + Fit in Window વિન્ડોમાં ફિટ કરો + Fit on Screen સ્ક્રીન પર ફિટ કરો + Fit to Artwork Bounds આર્ટવર્ક સીમા પર ફિટ કરો + Fit to Corners and Ends ખૂણા અને છેડા પર ફિટ કરો + Fit to Selected Art પસંદ કરેલી આર્ટ પર ફિટ કરો + Fix All બધું ઠીક કરો + Fix All Issues બધી સમસ્યાઓ ઠીક કરો + Fix Issues સમસ્યાઓ ઠીક કરો + Fix Selected પસંદ કરેલું ઠીક કરો + Flag… ફ્લેગ… + Flare ફ્લેર + Flare Tool ફ્લેર ટૂલ + Flare Tool Options ફ્લેર ટૂલ વિકલ્પો + Flat Image ફ્લેટ છબી + Flatness: ફ્લૅટનેસ: + Flatten ફ્લેટન કરો + Flatten Artwork આર્ટવર્ક ફ્લેટન કરો + Flatten Transparency ટ્રાન્સપરન્સી ફ્લેટન કરો + Flatten Transparency… ટ્રાન્સપરન્સી ફ્લેટન કરો… + Flattener Preview ફ્લેટનર પ્રિવ્યૂ + Flattener preset: ફ્લેટનર પ્રીસેટ: + Flip Across આડું ફ્લિપ કરો + Flip Along સાથે ફ્લિપ કરો + Flip Along Horizontal Axis આડી ધરી સાથે ફ્લિપ કરો + Flip Along Vertical Axis ઊભી અક્ષ સાથે ફ્લિપ કરો + Flip Horizontal આડું ફ્લિપ કરો + Flip Row Order પંક્તિનો ક્રમ ફ્લિપ કરો + Flip Vertical ઊભું ફ્લિપ કરો + Flip: ફ્લિપ: + Float All in Windows બધા વિન્ડોમાં ફ્લોટ કરો + Float Panel પેનલ ફ્લોટ કરો + Float Tool Group ટૂલ ગ્રૂપ ફ્લોટ કરો + Float in Window વિન્ડોમાં ફ્લોટ કરો + Folder ફોલ્ડર + Folder Name: ફોલ્ડરનું નામ: + Font ફોન્ટ + Font Family ફોન્ટ ફેમિલી + Font Family & Style ફોન્ટ ફેમિલી અને સ્ટાઇલ + Font Family, Style & Size ફોન્ટ ફેમિલી, સ્ટાઇલ અને સાઇઝ + Font Family: ફોન્ટ ફેમિલી: + Font List ફોન્ટ યાદી + Font Preview Size ફોન્ટ પૂર્વાવલોકન સાઇઝ + Font Size ફોન્ટ સાઇઝ + Font Style: ફોન્ટ સ્ટાઇલ: + Font: ફોન્ટ: + Fonts ફોન્ટ્સ + Fonts in Document દસ્તાવેજમાં ફોન્ટ્સ + Fonts in Document: {count} દસ્તાવેજમાં ફોન્ટ્સ: {count} + Fonts whose licence doesn't allow embedding are not copied. જે ફોન્ટ્સનું લાઇસન્સ એમ્બેડિંગની મંજૂરી આપતું નથી તે નકલ થશે નહીં. + For Fractions: અપૂર્ણાંક માટે: + Force ફોર્સ + Forced Line Break ફરજિયાત લાઇન બ્રેક + Forget Import Matches આયાત મેળ ભૂલી જાઓ + Format ફોર્મેટ + Format Options ફોર્મેટ વિકલ્પો + Format Presets ફોર્મેટ પ્રીસેટ્સ + Format Settings ફોર્મેટ સેટિંગ્સ + Format: ફોર્મેટ: + Formats: ફોર્મેટ્સ: + Fractional Widths અપૂર્ણાંક પહોળાઈ + Fractions અપૂર્ણાંકો + Free Distort ફ્રી ડિસ્ટોર્ટ + Free Distort… ફ્રી ડિસ્ટોર્ટ… + Free Transform ફ્રી ટ્રાન્સફોર્મ + Free Transform Tool ફ્રી ટ્રાન્સફોર્મ ટૂલ + Freeform ફ્રીફોર્મ + Freeform Gradient ફ્રીફોર્મ ગ્રેડિયન્ટ + French ફ્રેન્ચ + Frequencies and angles are the output device's to apply. ફ્રિક્વન્સી અને એંગલ આઉટપુટ ડિવાઇસ લાગુ કરશે. + Frequency ફ્રિક્વન્સી + Fresco… ફ્રેસ્કો… + From: થી: + Front આગળ + Full Document સંપૂર્ણ દસ્તાવેજ + Full Justify સંપૂર્ણ જસ્ટિફાય + Full Screen Mode ફુલ સ્ક્રીન મોડ + Full Screen Mode with Menu Bar મેનુ બાર સાથે ફુલ સ્ક્રીન મોડ + Full Tone ફુલ ટોન + Fully Enclosed સંપૂર્ણપણે બંધ + Fuzziness: ફઝીનેસ: + GPL palettes keep solid colours only, as RGB. GPL પેલેટમાં ફક્ત સોલિડ રંગો RGB તરીકે રાખવામાં આવે છે. + GPU Performance GPU પર્ફોર્મન્સ + Game HUD ગેમ HUD + Game Trails ગેમ ટ્રેઇલ્સ + Gamma ગામા + Gamut Check ગેમટ ચેક + Gamut Warning ગેમટ ચેતવણી + Gap Detection ગેપ ડિટેક્શન + Gap Length: ગેપની લંબાઈ: + Gap Options ગેપ વિકલ્પો + Gap Options… ગેપ વિકલ્પો… + Gap Preview Color: ગેપ પૂર્વાવલોકન રંગ: + Gap: ગેપ: + Gaussian Blur… ગૉસિયન બ્લર… + General સામાન્ય + Generate CSS CSS જનરેટ કરો + Generate CSS: the whole document's CSS while nothing is selected CSS જનરેટ કરો: કંઈ પસંદ ન હોય ત્યારે સમગ્ર દસ્તાવેજનું CSS + Geometric Guides ભૌમિતિક માર્ગદર્શિકાઓ + Geometric Patterns ભૌમિતિક પેટર્ન + German જર્મન + German: Swiss જર્મન: સ્વિસ + Get Preferences પસંદગીઓ મેળવો + Get Text Range ટેક્સ્ટ રેન્જ મેળવો + Glass… ગ્લાસ… + Global ગ્લોબલ + Global Brush Dimensions ગ્લોબલ બ્રશ પરિમાણો + Global Edit Options ગ્લોબલ એડિટ વિકલ્પો + Global Process Color ગ્લોબલ પ્રોસેસ રંગ + Glowing Edges… ગ્લોઇંગ એજિસ… + Glyph Bounds ગ્લિફ બાઉન્ડ્સ + Glyph Scaling: ગ્લિફ સ્કેલિંગ: + Glyphs ગ્લિફ્સ + Go To Link લિંક પર જાઓ + Go to Artboard આર્ટબોર્ડ પર જાઓ + Grab bag ગ્રેબ બેગ + Gradient ગ્રેડિયન્ટ + Gradient Fill: click to apply, drag onto art ગ્રેડિયન્ટ ભરો: લાગુ કરવા ક્લિક કરો, આર્ટ પર ખેંચો + Gradient Finishes ગ્રેડિયન્ટ ફિનિશ + Gradient Mesh ગ્રેડિયન્ટ મેશ + Gradient Presets ગ્રેડિયન્ટ પ્રીસેટ્સ + Gradient Tool ગ્રેડિયન્ટ ટૂલ + Gradient Vector ગ્રેડિયન્ટ વેક્ટર + Gradient across stroke સ્ટ્રોકની આરપાર ગ્રેડિયન્ટ + Gradient along stroke સ્ટ્રોક સાથે ગ્રેડિયન્ટ + Gradient and Mesh Resolution: ગ્રેડિયન્ટ અને મેશ રિઝોલ્યુશન: + Gradient swatches ગ્રેડિયન્ટ સ્વેચ + Gradient within stroke સ્ટ્રોકની અંદર ગ્રેડિયન્ટ + Gradients ગ્રેડિયન્ટ્સ + Grain ગ્રેઇન + Grain… ગ્રેઇન… + Graph ગ્રાફ + Graph Column ગ્રાફ કૉલમ + Graph Data ગ્રાફ ડેટા + Graph Design ગ્રાફ ડિઝાઇન + Graph Marker ગ્રાફ માર્કર + Graph Type ગ્રાફ પ્રકાર + Graphic Pen… ગ્રાફિક પેન… + Graphic Style ગ્રાફિક સ્ટાઇલ + Graphic Style Libraries ગ્રાફિક સ્ટાઇલ લાઇબ્રેરીઓ + Graphic Style Library ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી + Graphic Style Options ગ્રાફિક સ્ટાઇલ વિકલ્પો + Graphic Style Options… ગ્રાફિક સ્ટાઇલ વિકલ્પો… + Graphic Styles ગ્રાફિક સ્ટાઇલ્સ + Graphic Styles Options ગ્રાફિક સ્ટાઇલ્સ વિકલ્પો + Graphics ગ્રાફિક્સ + Graphics Processor ગ્રાફિક્સ પ્રોસેસર + Graphics: {adapter} ગ્રાફિક્સ: {adapter} + Gravity ગ્રેવિટી + Grays ગ્રે + Grays and Neutrals ગ્રે અને ન્યુટ્રલ + Grayscale ગ્રેસ્કેલ + Grayscale Images ગ્રેસ્કેલ છબીઓ + Grayscale images ગ્રેસ્કેલ છબીઓ + Green લીલો + Grid ગ્રિડ + Grid Color & Opacity ગ્રિડ રંગ અને અપારદર્શિતા + Grid Colors: ગ્રિડ રંગો: + Grid Repeat ગ્રિડ પુનરાવર્તન + Grid Size: ગ્રિડ સાઇઝ: + Grid View ગ્રિડ દૃશ્ય + Grid by Column કૉલમ પ્રમાણે ગ્રિડ + Grid by Row પંક્તિ પ્રમાણે ગ્રિડ + Gridline every દર ગ્રિડલાઇન + Gridline every: દર ગ્રિડલાઇન: + Grids In Back ગ્રિડ પાછળ + Group ગ્રૂપ + Group Overlapping Objects ઓવરલેપ થતી ઑબ્જેક્ટ્સ ગ્રૂપ કરો + Group Selection Tool ગ્રૂપ પસંદગી ટૂલ + Groups ગ્રૂપ્સ + Growth: વૃદ્ધિ: + Guide Label: ગાઇડ લેબલ: + Guides ગાઇડ્સ + Guides & Grid ગાઇડ્સ અને ગ્રિડ + Gujarati ગુજરાતી + Gutter: ગટર: + H Spacing: H સ્પેસિંગ: + H: H: + Ha Ha + Hair Space હેર સ્પેસ + Halftone Pattern… હાફટોન પેટર્ન… + Halo હેલો + Hand Tool હેન્ડ ટૂલ + Hand-Drawn હાથથી દોરેલું + Handles હેન્ડલ્સ + Hard હાર્ડ + Hard Light હાર્ડ લાઇટ + Hard Mix હાર્ડ મિક્સ + Harmony Rules: હાર્મની નિયમો: + Harmony Sets હાર્મની સેટ્સ + Health Check હેલ્થ ચેક + Height ઊંચાઈ + Height: ઊંચાઈ: + Help મદદ + Help Bar મદદ બાર + Help › Search Commands… મદદ › આદેશો શોધો… + Hidden છુપાયેલું + Hidden Layer છુપાયેલ લેયર + Hidden Objects: છુપાયેલી ઑબ્જેક્ટ્સ: + Hide છુપાવો + Hide Artboards આર્ટબોર્ડ છુપાવો + Hide Bar બાર છુપાવો + Hide Bounding Box બાઉન્ડિંગ બૉક્સ છુપાવો + Hide Code કોડ છુપાવો + Hide Corner Widget કૉર્નર વિજેટ છુપાવો + Hide Corner Widget for angles greater than આથી વધુ એંગલ માટે કૉર્નર વિજેટ છુપાવો + Hide Edges કિનારીઓ છુપાવો + Hide Font Height Options ફોન્ટ ઊંચાઈ વિકલ્પો છુપાવો + Hide Gradient Annotator ગ્રેડિયન્ટ એનોટેટર છુપાવો + Hide Grid ગ્રિડ છુપાવો + Hide Guides ગાઇડ્સ છુપાવો + Hide Hidden Characters છુપાયેલા અક્ષરો છુપાવો + Hide Live Paint Gaps લાઇવ પેઇન્ટ ગેપ છુપાવો + Hide Note નોંધ છુપાવો + Hide Options વિકલ્પો છુપાવો + Hide Others બીજા છુપાવો + Hide Presets પ્રીસેટ્સ છુપાવો + Hide Print Tiling પ્રિન્ટ ટાઇલિંગ છુપાવો + Hide Rulers રૂલર છુપાવો + Hide Slices સ્લાઇસ છુપાવો + Hide Snap to Glyph Options ગ્લિફ સ્નેપ વિકલ્પો છુપાવો + Hide Template ટેમ્પલેટ છુપાવો + Hide Text Threads ટેક્સ્ટ થ્રેડ્સ છુપાવો + Hide Thumbnail થંબનેલ છુપાવો + Hide Thumbnails થંબનેલ્સ છુપાવો + Hide Transparency Grid પારદર્શિતા ગ્રિડ છુપાવો + Hide VectorCraft VectorCraft છુપાવો + Hide Video Rulers વિડિઓ રૂલર છુપાવો + High Performance (discrete) ઉચ્ચ પર્ફોર્મન્સ (અલગ) + Highlight હાઇલાઇટ + Highlight Alternate Glyphs વૈકલ્પિક ગ્લિફ હાઇલાઇટ કરો + Highlight Area વિસ્તાર હાઇલાઇટ કરો + Highlight Font Name ફોન્ટનું નામ હાઇલાઇટ કરો + Highlight Intensity હાઇલાઇટ તીવ્રતા + Highlight Kerning કર્નિંગ હાઇલાઇટ કરો + Highlight Rows પંક્તિઓ હાઇલાઇટ કરો + Highlight Size હાઇલાઇટ સાઇઝ + Highlight Substituted Fonts બદલાયેલા ફોન્ટ્સ હાઇલાઇટ કરો + Highlight Substituted Glyphs બદલાયેલા ગ્લિફ હાઇલાઇટ કરો + Highlight Tracking ટ્રેકિંગ હાઇલાઇટ કરો + Highlight Variable વેરિએબલ હાઇલાઇટ કરો + Highlight anchors on mouse over માઉસ ફેરવતાં એન્કર હાઇલાઇટ કરો + Highlight: હાઇલાઇટ: + Hindi હિન્દી + History ઇતિહાસ + History Options… ઇતિહાસ વિકલ્પો… + History States ઇતિહાસ સ્થિતિઓ + Hold હોલ્ડ + Hold a Tool Shortcut to Use the Tool Temporarily ટૂલને અસ્થાયી રૂપે વાપરવા માટે ટૂલ શૉર્ટકટ દબાવી રાખો + Hollow હોલો + Hollow Circle હોલો વર્તુળ + Home હોમ + Horizon Height: ક્ષિતિજ ઊંચાઈ: + Horizontal આડું + Horizontal & Vertical Align Center આડું અને ઊભું કેન્દ્ર સંરેખિત કરો + Horizontal Align Center આડું કેન્દ્ર સંરેખિત કરો + Horizontal Align Left આડું ડાબે સંરેખિત કરો + Horizontal Align Right આડું જમણે સંરેખિત કરો + Horizontal Distribute Center આડું કેન્દ્ર વિતરિત કરો + Horizontal Distribute Left આડું ડાબે વિતરિત કરો + Horizontal Distribute Right આડું જમણે વિતરિત કરો + Horizontal Distribute Space આડી જગ્યા વિતરિત કરો + Horizontal Dividers: આડા ડિવાઇડર: + Horizontal Grid: આડો ગ્રિડ: + Horizontal Guide આડી ગાઇડ + Horizontal Plane આડું પ્લેન + Horizontal Scale આડું સ્કેલ + Horizontal Scale % આડું સ્કેલ % + Horizontal: આડું: + How colours outside the press's gamut are separated. પ્રેસના ગેમટની બહારના રંગો કેવી રીતે અલગ કરવામાં આવે છે. + Hue હ્યૂ + Hue Shift હ્યૂ શિફ્ટ + Hue ring હ્યૂ રિંગ + Hue/Saturation… હ્યૂ/સંતૃપ્તિ… + Hyphen Limit: હાઇફન મર્યાદા: + Hyphenate હાઇફનેટ કરો + Hyphenate Capitalized Words કૅપિટલ શબ્દો હાઇફનેટ કરો + Hyphenation હાઇફનેશન + Hyphenation Zone: હાઇફનેશન ઝોન: + Hyphenation… હાઇફનેશન… + ICC Profile ICC પ્રોફાઇલ + ICF Bottom/Left ICF નીચે/ડાબે + ICF Box ICF બૉક્સ + ICF Top/Right ICF ઉપર/જમણે + ID: {id} ID: {id} + Identify Objects By ઑબ્જેક્ટ્સ આ રીતે ઓળખો + If a drawing tablet's pen doesn't draw, choose an X11 (XWayland) option. જો ડ્રોઇંગ ટેબ્લેટની પેન દોરતી ન હોય, તો X11 (XWayland) વિકલ્પ પસંદ કરો. + Ignore અવગણો + Ignore All બધું અવગણો + Ignore Artboards (all the art as one page) આર્ટબોર્ડ અવગણો (બધી આર્ટ એક પેજ તરીકે) + Image ઇમેજ + Image Info ઇમેજ માહિતી + Image Map: ઇમેજ મેપ: + Image Size ઇમેજ સાઇઝ + Image Trace ઇમેજ ટ્રેસ + Image Trace (Make and Expand) ઇમેજ ટ્રેસ (બનાવો અને વિસ્તૃત કરો) + Image Trace Presets ઇમેજ ટ્રેસ પ્રીસેટ્સ + Image Trace Preview ઇમેજ ટ્રેસ પૂર્વાવલોકન + Image Trace View ઇમેજ ટ્રેસ દૃશ્ય + Image Tracing ઇમેજ ટ્રેસિંગ + Image: ઇમેજ: + Images ઇમેજ + Images: ઇમેજ: + Images: link to the file instead of only embedding it. ઇમેજ: ફક્ત એમ્બેડ કરવાને બદલે ફાઇલની લિંક. + Import Composite Fonts કમ્પોઝિટ ફોન્ટ્સ આયાત કરો + Import Data Sets… ડેટા સેટ આયાત કરો… + Import Data… ડેટા આયાત કરો… + Import Export Presets આયાત-નિકાસ પ્રીસેટ્સ + Import Keyboard Shortcuts… કીબોર્ડ શૉર્ટકટ આયાત કરો… + Import Library લાઇબ્રેરી આયાત કરો + Import Matches આયાત મેળ + Import PDF PDF આયાત કરો + Import PDF Presets PDF આયાત પ્રીસેટ્સ + Import Perspective Grid Presets પર્સ્પેક્ટિવ ગ્રિડ પ્રીસેટ્સ આયાત કરો + Import Preferences પસંદગીઓ આયાત કરો + Import Preferences… પસંદગીઓ આયાત કરો… + Import Preset… પ્રીસેટ આયાત કરો… + Import Print Presets પ્રિન્ટ પ્રીસેટ્સ આયાત કરો + Import SVG Filter SVG ફિલ્ટર આયાત કરો + Import SVG Filter… SVG ફિલ્ટર આયાત કરો… + Import Shortcut Set શૉર્ટકટ સેટ આયાત કરો + Import Shortcut Set… શૉર્ટકટ સેટ આયાત કરો… + Import Transparency Flattener Presets પારદર્શિતા ફ્લેટનર પ્રીસેટ્સ આયાત કરો + Imported. Press OK to keep the imported set. આયાત થયું. આયાત કરેલો સેટ રાખવા ઠીક દબાવો. + Import… આયાત કરો… + In Merge Mode, Clicking Stroke Splits the Path મર્જ મોડમાં, સ્ટ્રોક પર ક્લિક કરવાથી પાથ વિભાજિત થાય છે + In Tangent Handles ટેન્જન્ટ હેન્ડલમાં + In VectorCraft VectorCraft માં + In the File ફાઇલમાં + In the selected objects પસંદ કરેલી ઑબ્જેક્ટ્સમાં + Inches ઇંચ + Include Action Steps ઍક્શન સ્ટેપ્સ શામેલ કરો + Include Artboard Fill આર્ટબોર્ડ ભરણ શામેલ કરો + Include Bleed બ્લીડ શામેલ કરો + Include CMYK PostScript in RGB Files RGB ફાઇલોમાં CMYK PostScript શામેલ કરો + Include Document Thumbnails દસ્તાવેજ થંબનેલ્સ શામેલ કરો + Include Editing Data for Other Apps અન્ય ઍપ્સ માટે એડિટિંગ ડેટા શામેલ કરો + Include Hidden Layers છુપાયેલા લેયર શામેલ કરો + Include Linked Files લિંક કરેલી ફાઇલો શામેલ કરો + Include Metadata મેટાડેટા શામેલ કરો + Include Objects On Canvas કેનવાસ પરની ઑબ્જેક્ટ્સ શામેલ કરો + Include SVG Code SVG કોડ શામેલ કરો + Include Swatches for Tints ટિન્ટ માટે સ્વેચ શામેલ કરો + Include non-printing layers બિન-પ્રિન્ટિંગ લેયર શામેલ કરો + Includes શામેલ છે + Includes itself પોતાને શામેલ કરે છે + Increase Font Size ફોન્ટ સાઇઝ વધારો + Increase Font Size by Five Steps ફોન્ટ સાઇઝ પાંચ સ્ટેપ વધારો + Inflate ફુલાવો + Inflate… ફુલાવો… + Info માહિતી + Ink ઇંક + Ink Outlines… ઇંક આઉટલાઇન… + Inks ઇંક્સ + Inner Corner Tile આંતરિક કૉર્નર ટાઇલ + Inner Glow… ઇનર ગ્લો… + Inner Radius: આંતરિક ત્રિજ્યા: + Insert Break Character બ્રેક અક્ષર દાખલ કરો + Insert Character અક્ષર દાખલ કરો + Insert Inline Symbol ઇનલાઇન સિમ્બોલ દાખલ કરો + Insert Kashidas કાશીદા દાખલ કરો + Insert Special Character ખાસ અક્ષર દાખલ કરો + Insert Stop સ્ટોપ દાખલ કરો + Insert Whitespace Character ખાલી જગ્યાનો અક્ષર દાખલ કરો + Inside અંદર + Inspect Document દસ્તાવેજ તપાસો + Inspect Object ઑબ્જેક્ટ તપાસો + Inspect Plug-in પ્લગઇન તપાસો + Install Plug-in પ્લગઇન ઇન્સ્ટૉલ કરો + Install Plug-in… પ્લગઇન ઇન્સ્ટૉલ કરો… + Install “{name}” in place of the plug-in with the same ID? સમાન ID ધરાવતા પ્લગઇનની જગ્યાએ “{name}” ઇન્સ્ટૉલ કરીએ? + Install “{name}”, which asks for a heavy budget? “{name}” ઇન્સ્ટૉલ કરીએ, જેને ભારે બજેટની જરૂર છે? + Install “{name}”, which asks for these permissions? “{name}” ઇન્સ્ટૉલ કરીએ, જે આ પરવાનગીઓ માંગે છે? + Intensity તીવ્રતા + Intensity: તીવ્રતા: + Intent: ઇન્ટેન્ટ: + Interface Language ઇન્ટરફેસ ભાષા + Interlaced ઇન્ટરલેસ્ડ + Internet Standard RGB (sRGB) ઇન્ટરનેટ સ્ટાન્ડર્ડ RGB (sRGB) + Intersect છેદ + Intertwine ગૂંથો + Invalid panel layout; restored the default dock અમાન્ય પેનલ લેઆઉટ; ડિફોલ્ટ ડૉક પુનઃસ્થાપિત કર્યું + Inventory Icons ઇન્વેન્ટરી આઇકન + Inverse ઇન્વર્સ + Invert ઊલટાવો + Invert Colors રંગો ઊલટાવો + Invert Mask માસ્ક ઊલટાવો + Invert Pie પાઇ ઊલટાવો + Inverted Round ઊલટું ગોળ + Invoke Distance Guides ડિસ્ટન્સ ગાઇડ્સ ચાલુ કરો + Isolate અલગ કરો + Isolate Blending બ્લેન્ડિંગ અલગ કરો + Isolate Group ગ્રૂપ અલગ કરો + Isolate Selected Group પસંદ કરેલું ગ્રૂપ અલગ કરો + Isolate Selected Object પસંદ કરેલી ઑબ્જેક્ટ અલગ કરો + Isometric Bottom આઇસોમેટ્રિક નીચે + Isometric Left આઇસોમેટ્રિક ડાબે + Isometric Right આઇસોમેટ્રિક જમણે + Isometric Top આઇસોમેટ્રિક ઉપર + Issues fixed: {count} સુધારેલી સમસ્યાઓ: {count} + It may take minutes, so it runs in the background, where you can cancel it. તેમાં મિનિટો લાગી શકે છે, તેથી તે બૅકગ્રાઉન્ડમાં ચાલે છે, જ્યાં તમે તેને રદ કરી શકો છો. + Italian ઇટાલિયન + Italic ઇટાલિક + Its graphics, colors and text styles are deleted from this machine. આ મશીન પરથી તેના ગ્રાફિક્સ, રંગો અને ટેક્સ્ટ સ્ટાઇલ કાઢી નાખવામાં આવશે. + Japanese જાપાનીઝ + JavaScript Files JavaScript ફાઇલો + JavaScript: JavaScript: + Join જોડો + Join Our Discord અમારા Discord માં જોડાઓ + Join Point Text જોઇન્ટ પોઇન્ટ ટેક્સ્ટ + Join Tool જોઇન ટૂલ + Join our Discord અમારા Discord માં જોડાઓ + Join the ArtCraft community on Discord (discord.gg/artcraft) Discord (discord.gg/artcraft) પર ArtCraft સમુદાયમાં જોડાઓ + Join the lines of each paragraph દરેક ફકરાની લાઇનો જોડો + Justification જસ્ટિફિકેશન + Justification… જસ્ટિફિકેશન… + Justify Vertically ઊભું જસ્ટિફાય કરો + Justify all lines બધી લાઇન જસ્ટિફાય કરો + Justify with last line aligned left છેલ્લી લાઇન ડાબે સંરેખિત સાથે જસ્ટિફાય કરો + Kana કાના + Kanji કાંજી + Kannada કન્નડ + Kashidas કાશીદા + Keep Formatting ફોર્મેટિંગ રાખો + Keep Original મૂળ રાખો + Keep Plain Text સાદો ટેક્સ્ટ રાખો + Keep Shared Points Together વહેંચાયેલા પોઇન્ટ સાથે રાખો + Keep Tools Beside the Canvas ટૂલ્સ કેનવાસની બાજુમાં રાખો + Keep a cache of its own, which you can clear તેનું પોતાનું કેશ રાખો, જે તમે સાફ કરી શકો છો + Keep a link to an image file instead of only embedding it ઇમેજ ફાઇલને ફક્ત એમ્બેડ કરવાને બદલે તેની લિંક રાખો + Keep both sides the same width બંને બાજુ સમાન પહોળાઈ રાખો + Keep data of its own with the document દસ્તાવેજ સાથે પોતાનો ડેટા રાખો + Keep graphics, colors and text styles here to use them in any document. Libraries are stored on this machine — no account required. કોઈપણ દસ્તાવેજમાં વાપરવા માટે અહીં ગ્રાફિક્સ, રંગો અને ટેક્સ્ટ સ્ટાઇલ રાખો. લાઇબ્રેરીઓ આ મશીન પર સંગ્રહિત થાય છે — ખાતું જરૂરી નથી. + Keep selected પસંદ કરેલું રાખો + Keep settings of its own પોતાની સેટિંગ્સ રાખો + Keep this row's colors (exclude) આ પંક્તિના રંગ રાખો (બાકાત કરો) + Keep visible surfaces only when expanding વિસ્તૃત કરતી વખતે ફક્ત દેખાતી સપાટીઓ રાખો + Kerning કર્નિંગ + Key Color: કી રંગ: + Keyboard Increment કીબોર્ડ ઇન્ક્રીમેન્ટ + Keyboard Shortcut Conflicts કીબોર્ડ શૉર્ટકટ વિવાદો + Keyboard Shortcut Set કીબોર્ડ શૉર્ટકટ સેટ + Keyboard Shortcuts… કીબોર્ડ શૉર્ટકટ… + Keys કી + Keywords: કીવર્ડ્સ: + Kind પ્રકાર + Kinsoku Set Kinsoku Set + Kinsoku Shori Type Kinsoku Shori Type + Knife નાઇફ + Knockout Group નૉકઆઉટ ગ્રૂપ + LZW Compression LZW કમ્પ્રેશન + Lab Spot Colors: Lab સ્પોટ રંગો: + Landscape લેન્ડસ્કેપ + Language ભાષા + Language Options ભાષા વિકલ્પો + Language: ભાષા: + Large મોટું + Large Tabs મોટા ટૅબ + Largest: સૌથી મોટું: + Lasso Tool લાસો ટૂલ + Last છેલ્લું + Last Artboard છેલ્લું આર્ટબોર્ડ + Last Effect: {name} છેલ્લી ઇફેક્ટ: {name} + Last Effect… છેલ્લી ઇફેક્ટ… + Last Page છેલ્લું પેજ + Last commit છેલ્લો કમિટ + Latin લેટિન + Layer Options લેયર વિકલ્પો + Layer Options… લેયર વિકલ્પો… + Layers લેયર્સ + Layers Panel Options લેયર્સ પેનલ વિકલ્પો + Layout લેઆઉટ + Layout Order: લેઆઉટ ક્રમ: + Layout: લેઆઉટ: + Leading લીડિંગ + Leave Out of Global Edit ગ્લોબલ એડિટમાંથી બાકાત રાખો + Left ડાબે + Left Grid: ડાબો ગ્રિડ: + Left Indent ડાબો ઇન્ડેન્ટ + Left Indent: ડાબો ઇન્ડેન્ટ: + Left Plane ડાબું પ્લેન + Left to Right ડાબેથી જમણે + Left-Justified Tab ડાબે-જસ્ટિફાઇડ ટૅબ + Left-to-Right Paragraph Direction ડાબેથી જમણે ફકરાની દિશા + Left/Right: ડાબે/જમણે: + Left: ડાબે: + Legacy (version 7) લેગસી (વર્ઝન 7) + Legacy .ai Options લેગસી .ai વિકલ્પો + Legacy Macintosh RGB (Gamma 1.8) લેગસી Macintosh RGB (Gamma 1.8) + Legacy vector clipboard લેગસી વેક્ટર ક્લિપબોર્ડ + Legacy vector clipboard (no transparency) લેગસી વેક્ટર ક્લિપબોર્ડ (પારદર્શિતા વગર) + Length લંબાઈ + Length X-Axis: X-અક્ષ લંબાઈ: + Length Y-Axis: Y-અક્ષ લંબાઈ: + Less ઓછું + Letter Spacing: અક્ષર અંતર: + Level: સ્તર: + Levels… લેવલ્સ… + Libraries લાઇબ્રેરીઓ + Library લાઇબ્રેરી + Library Panel Options લાઇબ્રેરી પૅનલ વિકલ્પો + Library: લાઇબ્રેરી: + Ligatures are off while the text is tracked. ટ્રૅક થયેલા લખાણ દરમિયાન લિગેચર બંધ છે. + Light લાઇટ + Light Gray લાઇટ ગ્રે + Light Intensity લાઇટ તીવ્રતા + Light Rough લાઇટ રફ + Light behind the object ઑબ્જેક્ટની પાછળ લાઇટ + Lighten હળવું કરો + Lighting લાઇટિંગ + Lights લાઇટ્સ + Limit Color Guide to Library રંગ ગાઇડને લાઇબ્રેરી સુધી મર્યાદિત કરો + Limit colors to swatch library રંગોને સ્વેચ લાઇબ્રેરી સુધી મર્યાદિત કરો + Limit colors to swatch library: {name} રંગોને સ્વેચ લાઇબ્રેરી સુધી મર્યાદિત કરો: {name} + Limit to Library: લાઇબ્રેરી સુધી મર્યાદિત કરો: + Limit: મર્યાદા: + Limited મર્યાદિત + Line લાઇન + Line Art and Text Resolution: લાઇન આર્ટ અને લખાણ રિઝોલ્યુશન: + Line Breaks લાઇન બ્રેક + Line Color લાઇન રંગ + Line Cut લાઇન કટ + Line Cut Tool લાઇન કટ ટૂલ + Line Endings: લાઇનના અંત: + Line Graph Tool લાઇન ગ્રાફ ટૂલ + Line Segment લાઇન સેગમેન્ટ + Line Segment Tool લાઇન સેગમેન્ટ ટૂલ + Line Segment Tool Options લાઇન સેગમેન્ટ ટૂલ વિકલ્પો + Line delta લાઇન ડેલ્ટા + Line {line} લાઇન {line} + Line {line}: {name}: {status} લાઇન {line}: {name}: {status} + Line-end Punctuation Half Width લાઇન-અંતના વિરામચિહ્ન અર્ધ પહોળાઈ + Linear Gradient લીનિયર ગ્રેડિયન્ટ + Lines લાઇન્સ + Lines +/− લાઇન્સ +/− + Lines added ઉમેરાયેલી લાઇનો + Lines deleted કાઢી નાખેલી લાઇનો + Lines: clicks add points joined by a line લાઇન્સ: ક્લિક કરવાથી લાઇન વડે જોડાયેલા બિંદુઓ ઉમેરાય + Link લિંક + Link Block Series બ્લોક શ્રેણી લિંક કરો + Link Info લિંક માહિતી + Link JavaScript Files… JavaScript ફાઇલો લિંક કરો… + Link Opacity Mask ઓપેસિટી માસ્ક લિંક કરો + Link harmony colors હાર્મની રંગો લિંક કરો + Link start and end arrowhead scales શરૂઆત અને અંતના એરોહેડ સ્કેલ લિંક કરો + Link the mask માસ્ક લિંક કરો + Linked લિંક કરેલું + Linked File લિંક કરેલી ફાઇલ + Linked File Not Found લિંક કરેલી ફાઇલ મળી નથી + Linked Files લિંક કરેલી ફાઇલો + Links લિંક્સ + Linux Display Server Linux ડિસ્પ્લે સર્વર + Liquify લિક્વિફાઇ + List Data Sets ડેટા સેટની યાદી + List Folder ફોલ્ડરની યાદી + List Graphic Styles ગ્રાફિક સ્ટાઇલની યાદી + List Installed Scripts ઇન્સ્ટોલ થયેલી સ્ક્રિપ્ટની યાદી + List Keyboard Shortcuts કીબોર્ડ શૉર્ટકટની યાદી + List Patterns પેટર્નની યાદી + List Plug-ins પ્લગ-ઇનની યાદી + List Preferences પસંદગીઓની યાદી + List Slices સ્લાઇસની યાદી + List Toolbars ટૂલબારની યાદી + List Variables વેરિએબલની યાદી + List View યાદી દૃશ્ય + List Workspaces વર્કસ્પેસની યાદી + Live Paint લાઇવ પેઇન્ટ + Live Paint Bucket લાઇવ પેઇન્ટ બકેટ + Live Paint Bucket (Stroke) લાઇવ પેઇન્ટ બકેટ (સ્ટ્રોક) + Live Paint Info લાઇવ પેઇન્ટ માહિતી + Live Paint Selection Tool લાઇવ પેઇન્ટ પસંદગી ટૂલ + Live Shape લાઇવ શેપ + Live Shape Properties લાઇવ શેપ ગુણધર્મો + Live curve fitting લાઇવ કર્વ ફિટિંગ + Live preview લાઇવ પૂર્વાવલોકન + Load Actions… ઍક્શન લોડ કરો… + Load All Styles બધી સ્ટાઇલ લોડ કરો + Load All Styles… બધી સ્ટાઇલ લોડ કરો… + Load Character Styles કેરેક્ટર સ્ટાઇલ લોડ કરો + Load Character Styles… કેરેક્ટર સ્ટાઇલ લોડ કરો… + Load Color Profile… કલર પ્રોફાઇલ લોડ કરો… + Load EPS into Clipboard EPS ને ક્લિપબોર્ડમાં લોડ કરો + Load Image into Clipboard ઇમેજ ક્લિપબોર્ડમાં લોડ કરો + Load Metafile into Clipboard મેટાફાઇલ ક્લિપબોર્ડમાં લોડ કરો + Load PDF into Clipboard PDF ક્લિપબોર્ડમાં લોડ કરો + Load Paragraph Styles ફકરા સ્ટાઇલ લોડ કરો + Load Paragraph Styles… ફકરા સ્ટાઇલ લોડ કરો… + Load Place Cursor પ્લેસ કર્સર લોડ કરો + Load Profile… પ્રોફાઇલ લોડ કરો… + Load SVG into Clipboard SVG ને ક્લિપબોર્ડમાં લોડ કરો + Load Text into Clipboard લખાણ ક્લિપબોર્ડમાં લોડ કરો + Load Texture… ટેક્સ્ચર લોડ કરો… + Load Variable Library… વેરિએબલ લાઇબ્રેરી લોડ કરો… + Loads native code નેટિવ કોડ લોડ કરે છે + Local Libraries સ્થાનિક લાઇબ્રેરીઓ + Locate Library… લાઇબ્રેરી શોધો… + Locate Object ઑબ્જેક્ટ શોધો + Locate Selected Objects પસંદ કરેલા ઑબ્જેક્ટ શોધો + Location સ્થાન + Location: સ્થાન: + Lock લૉક + Lock (⌘2) લૉક (⌘2) + Lock All બધું લૉક કરો + Lock All Except Selected પસંદ કરેલા સિવાય બધું લૉક કરો + Lock Artboards આર્ટબોર્ડ લૉક કરો + Lock Grid ગ્રિડ લૉક કરો + Lock Guides ગાઇડ લૉક કરો + Lock Guides by Default ડિફૉલ્ટ રૂપે ગાઇડ લૉક કરો + Lock Others અન્ય લૉક કરો + Lock Selected પસંદ કરેલું લૉક કરો + Lock Slices સ્લાઇસ લૉક કરો + Lock Station Point સ્ટેશન પૉઇન્ટ લૉક કરો + Lock Variables વેરિએબલ લૉક કરો + Locked લૉક કરેલું + Long લાંબું + Longest: સૌથી લાંબું: + Lossless (lossy WebP isn't available yet) લોસલેસ (લોસી WebP હજી ઉપલબ્ધ નથી) + Lossy: લોસી: + Luminosity લ્યુમિનોસિટી + Magic Wand મૅજિક વૅન્ડ + Magic Wand Options મૅજિક વૅન્ડ વિકલ્પો + Magic Wand Settings મૅજિક વૅન્ડ સેટિંગ્સ + Magic Wand Tool મૅજિક વૅન્ડ ટૂલ + Maintain Envelope Shape એન્વેલપનો આકાર જાળવો + Make બનાવો + Make Blend બ્લેન્ડ બનાવો + Make Clipping Mask ક્લિપિંગ માસ્ક બનાવો + Make Compound Path કમ્પાઉન્ડ પાથ બનાવો + Make Compound Shape કમ્પાઉન્ડ શેપ બનાવો + Make Corner કૉર્નર બનાવો + Make Dynamic ડાયનેમિક બનાવો + Make Envelope એન્વેલપ બનાવો + Make Graph Dynamic ગ્રાફ ડાયનેમિક બનાવો + Make Guides ગાઇડ બનાવો + Make Image Dynamic ઇમેજ ડાયનેમિક બનાવો + Make Intertwine ઇન્ટરટ્વાઇન બનાવો + Make Live Paint લાઇવ પેઇન્ટ બનાવો + Make Mask માસ્ક બનાવો + Make Opacity Mask ઓપેસિટી માસ્ક બનાવો + Make Pattern પેટર્ન બનાવો + Make Pixel Perfect પિક્સેલ પરફેક્ટ બનાવો + Make Sides Equal બાજુઓ સરખી બનાવો + Make Slice સ્લાઇસ બનાવો + Make Smooth સ્મૂથ બનાવો + Make Text Dynamic લખાણ ડાયનેમિક બનાવો + Make Text Wrap લખાણ રૅપ બનાવો + Make Visibility Dynamic દૃશ્યતા ડાયનેમિક બનાવો + Make Visible દૃશ્યમાન બનાવો + Make a theme on Create and save it. થીમ બનાવીને તેને સાચવો. + Make all bleed settings the same બધી બ્લીડ સેટિંગ્સ સરખી બનાવો + Make and Expand બનાવો અને વિસ્તૃત કરો + Make the bleed values differ બ્લીડ મૂલ્યો અલગ બનાવો + Make with Mesh… મેશ વડે બનાવો… + Make with Top Object ટોચના ઑબ્જેક્ટ વડે બનાવો + Make with Warp… વૉર્પ વડે બનાવો… + Make/Release Clipping Mask ક્લિપિંગ માસ્ક બનાવો/છોડો + Malayalam મલયાલમ + Manage Toolbars ટૂલબાર સંચાલિત કરો + Manage Toolbars… ટૂલબાર સંચાલિત કરો… + Manage Workspaces વર્કસ્પેસ સંચાલિત કરો + Manage Workspaces… વર્કસ્પેસ સંચાલિત કરો… + Manually જાતે + Map Markers મૅપ માર્કર + Marathi મરાઠી + Mark Older Files as [Converted] When Opened ખોલતી વખતે જૂની ફાઇલોને [Converted] તરીકે ચિહ્નિત કરો + Mark as trapped ટ્રૅપ થયેલું તરીકે ચિહ્નિત કરો + Marker… માર્કર… + Marks ચિહ્નો + Marquee Selection માર્કી પસંદગી + Mask માસ્ક + Match મેળ + Match User Interface Brightness યુઝર ઇન્ટરફેસની તેજસ્વિતા સરખી કરો + Materials… મટીરિયલ્સ… + Matte: મેટ: + Maximize મોટું કરો + Maximum મહત્તમ + Maximum Editability મહત્તમ સંપાદનક્ષમતા + Maximum angle મહત્તમ ખૂણો + Maximum: મહત્તમ: + Measure & Inspect માપ અને તપાસ + Measure & Inspect Tool માપ અને તપાસ ટૂલ + Measure Tool માપ ટૂલ + Measure Type by Glyph Bounds ગ્લિફ સીમા દ્વારા ટાઇપ માપો + Measurement Labels માપ લેબલ + Media મીડિયા + Medium મધ્યમ + Medium Dark મધ્યમ ઘેરો + Medium Gray મધ્યમ ગ્રે + Medium Light મધ્યમ હળવો + Menu Dialog મેનુ સંવાદ + Menu commands મેનુ આદેશો + Merge મર્જ કરો + Merge Graphic Styles ગ્રાફિક સ્ટાઇલ મર્જ કરો + Merge Graphic Styles… ગ્રાફિક સ્ટાઇલ મર્જ કરો… + Merge Layers લેયર મર્જ કરો + Merge Live Paint લાઇવ પેઇન્ટ મર્જ કરો + Merge Selected પસંદ કરેલું મર્જ કરો + Merge Shared Edge શેર કરેલી કિનાર મર્જ કરો + Merge Swatches સ્વેચ મર્જ કરો + Merge colors into one row રંગોને એક હરોળમાં મર્જ કરો + Merge only with selection માત્ર પસંદગી સાથે મર્જ કરો + Merged PRs મર્જ થયેલા PR + Mesh Point Color મેશ બિંદુનો રંગ + Mesh Tool મેશ ટૂલ + Message: સંદેશ: + Metadata: મેટાડેટા: + Metallic Gradients મેટાલિક ગ્રેડિયન્ટ + Meters મીટર + Method: પદ્ધતિ: + Mezzotint… મેઝોટિન્ટ… + Millimeters મિલીમીટર + Minify નાનું કરો + Minimize નાનું કરો + Minimum ન્યૂનતમ + Minimum angle ન્યૂનતમ ખૂણો + Minimum must not exceed Desired, nor Desired Maximum. ન્યૂનતમ, ઇચ્છિત કે ઇચ્છિત મહત્તમ કરતાં વધારે ન હોઈ શકે. + Minimum: ન્યૂનતમ: + Minus Back માઇનસ બૅક + Minus Front માઇનસ ફ્રન્ટ + Mirror મિરર + Mirror & Cut મિરર અને કટ + Mirror & Cut Tool મિરર અને કટ ટૂલ + Mirror Repeat મિરર રિપીટ + Missing ગુમ + Missing Fonts ગુમ ફૉન્ટ + Missing Fonts Dialog ગુમ ફૉન્ટ સંવાદ + Missing: {path} ગુમ: {path} + Miter માઇટર + Miter Join માઇટર જોઇન + Mixed મિશ્ર + Mixed Appearances મિશ્ર દેખાવ + Mixed line endings મિશ્ર લાઇન અંત + Mode: મોડ: + Model મૉડેલ + Models મૉડેલ્સ + Modern મૉડર્ન + Modified સુધારેલું + Modified since it was read: {path} વાંચ્યા પછી સુધારેલું: {path} + Modified: સુધારેલું: + Modify સુધારો + Mojikumi Set મોજીકુમી સેટ + Monitor RGB મૉનિટર RGB + Mono મોનો + Monochrome images મોનોક્રોમ ઇમેજ + Monospaced મોનોસ્પેસ્ડ + More વધુ + More Align options વધુ સંરેખણ વિકલ્પો + More Options વધુ વિકલ્પો + More Paragraph options વધુ ફકરા વિકલ્પો + More Pathfinder options વધુ પાથફાઇન્ડર વિકલ્પો + More Settings વધુ સેટિંગ્સ + Mosaic Tiles… મોઝેક ટાઇલ્સ… + Move ખસેડો + Move Anchor Point એન્કર બિંદુ ખસેડો + Move Anchors એન્કર ખસેડો + Move Appearance દેખાવ ખસેડો + Move Artboard આર્ટબોર્ડ ખસેડો + Move Artboard Up/Down આર્ટબોર્ડ ઉપર/નીચે ખસેડો + Move Artwork with Artboard આર્ટબોર્ડ સાથે આર્ટવર્ક ખસેડો + Move Brush બ્રશ ખસેડો + Move Direction Handle દિશા હૅન્ડલ ખસેડો + Move Document દસ્તાવેજ ખસેડો + Move Down નીચે ખસેડો + Move Effect ઇફેક્ટ ખસેડો + Move Envelope Mesh Point એન્વેલપ મેશ બિંદુ ખસેડો + Move Envelope Point એન્વેલપ બિંદુ ખસેડો + Move Graphic Style ગ્રાફિક સ્ટાઇલ ખસેડો + Move Guide ગાઇડ ખસેડો + Move Guide to Layer ગાઇડ લેયરમાં ખસેડો + Move Library Group લાઇબ્રેરી ગ્રૂપ ખસેડો + Move Library Item to Group લાઇબ્રેરી આઇટમ ગ્રૂપમાં ખસેડો + Move Locked and Hidden Artwork with Artboard લૉક અને છુપાયેલું આર્ટવર્ક આર્ટબોર્ડ સાથે ખસેડો + Move Mesh Point મેશ બિંદુ ખસેડો + Move Mesh Points મેશ બિંદુઓ ખસેડો + Move Panel પૅનલ ખસેડો + Move Plane પ્લેન ખસેડો + Move Plane and Copy Objects પ્લેન ખસેડો અને ઑબ્જેક્ટની નકલ કરો + Move Plane and Objects પ્લેન અને ઑબ્જેક્ટ ખસેડો + Move Plane to Match Object ઑબ્જેક્ટ સાથે મેળ ખાય તે રીતે પ્લેન ખસેડો + Move Rows હરોળ ખસેડો + Move Slice સ્લાઇસ ખસેડો + Move Slices સ્લાઇસ ખસેડો + Move Spine Point સ્પાઇન બિંદુ ખસેડો + Move Swatch સ્વેચ ખસેડો + Move Type Path Handle ટાઇપ પાથ હૅન્ડલ ખસેડો + Move Type on a Path પાથ પર ટાઇપ ખસેડો + Move Up ઉપર ખસેડો + Move in Perspective પર્સ્પેક્ટિવમાં ખસેડો + Move to Group ગ્રૂપમાં ખસેડો + Move… ખસેડો… + Multiple Files અનેક ફાઇલો + Multiple objects અનેક ઑબ્જેક્ટ + Multiply ગુણાકાર + My Themes મારી થીમ + NTSC Colors NTSC રંગો + Name નામ + Name (A–Z) નામ (A–Z) + Name: નામ: + Names the script builds as it runs: {count} સ્ક્રિપ્ટ ચાલતી વખતે બનાવે છે તે નામ: {count} + Navigate નેવિગેટ કરો + Navigator નેવિગેટર + Navigator Panel Options નેવિગેટર પૅનલ વિકલ્પો + Needs Attention Only માત્ર ધ્યાન માગતા + Needs a choice પસંદગી જરૂરી + Needs a key કીની જરૂર છે + Needs dialogs સંવાદોની જરૂર છે + Needs features only the reference app has માત્ર સંદર્ભ એપમાં હોય તેવી સુવિધાઓ જોઈએ + Needs files ફાઇલો જોઈએ + Needs menu commands મેનુ આદેશો જોઈએ + Needs nothing beyond plain JavaScript સાદા JavaScript સિવાય કશું જોઈતું નથી + Needs the desktop app ડેસ્કટૉપ એપ જોઈએ + Needs the document model દસ્તાવેજ મૉડેલ જોઈએ + Needs the script language's own objects or directives સ્ક્રિપ્ટ ભાષાના પોતાના ઑબ્જેક્ટ કે નિર્દેશો જોઈએ + Neon Glow… નિયોન ગ્લો… + Nesting too deep to follow અનુસરવા માટે નેસ્ટિંગ ખૂબ ઊંડું + Neutral Gradients ન્યુટ્રલ ગ્રેડિયન્ટ + New નવું + New Action નવી ઍક્શન + New Art Appearance નવો આર્ટ દેખાવ + New Art Has Basic Appearance નવા આર્ટમાં મૂળભૂત દેખાવ + New Artboard નવું આર્ટબોર્ડ + New Artboard Options… નવા આર્ટબોર્ડ વિકલ્પો… + New Brush નવો બ્રશ + New Brush… નવો બ્રશ… + New Character Style નવી કેરેક્ટર સ્ટાઇલ + New Character Style… નવી કેરેક્ટર સ્ટાઇલ… + New Color Group નવો રંગ ગ્રૂપ + New Color Group… નવો રંગ ગ્રૂપ… + New Data Set નવો ડેટા સેટ + New Data Set… નવો ડેટા સેટ… + New Design નવી ડિઝાઇન + New Document Presets નવા દસ્તાવેજ પ્રીસેટ + New Document from State સ્ટેટમાંથી નવો દસ્તાવેજ + New Graphic Style નવી ગ્રાફિક સ્ટાઇલ + New Graphic Style (Alt-click to name it) નવી ગ્રાફિક સ્ટાઇલ (નામ આપવા Alt-ક્લિક કરો) + New Graphic Style… નવી ગ્રાફિક સ્ટાઇલ… + New Group નવું ગ્રૂપ + New Guide નવી ગાઇડ + New Layer નવું લેયર + New Layer Options… નવા લેયર વિકલ્પો… + New Layer… નવું લેયર… + New Library Group નવું લાઇબ્રેરી ગ્રૂપ + New Name નવું નામ + New Opacity Masks Are Clipping નવા ઓપેસિટી માસ્ક ક્લિપિંગ છે + New Opacity Masks Are Inverted નવા ઓપેસિટી માસ્ક ઊલટા છે + New PDF Preset નવો PDF પ્રીસેટ + New Paragraph Style નવી ફકરા સ્ટાઇલ + New Paragraph Style… નવી ફકરા સ્ટાઇલ… + New Print Preset નવો પ્રિન્ટ પ્રીસેટ + New SVG Filter નવું SVG ફિલ્ટર + New Set નવો સેટ + New Sublayer નવું સબલેયર + New Sublayer… નવું સબલેયર… + New Swatch નવો સ્વેચ + New Swatch… નવો સ્વેચ… + New Symbol નવું સિમ્બોલ + New Symbols from Layers લેયરમાંથી નવા સિમ્બોલ + New Symbol… નવું સિમ્બોલ… + New Toolbar નવું ટૂલબાર + New Toolbar… નવું ટૂલબાર… + New Variable નવો વેરિએબલ + New Variable… નવો વેરિએબલ… + New View નવું દૃશ્ય + New View… નવું દૃશ્ય… + New Window નવી વિન્ડો + New Without Dialog સંવાદ વિના નવું + New Workspace નવી વર્કસ્પેસ + New Workspace… નવી વર્કસ્પેસ… + New file નવી ફાઇલ + New from Template… ટેમ્પ્લેટમાંથી નવું… + New theme from the current color હાલના રંગમાંથી નવી થીમ + New… નવું… + Next આગળ + Next Artboard આગળનું આર્ટબોર્ડ + Next Data Set આગળનો ડેટા સેટ + Next Document આગળનો દસ્તાવેજ + Next Object Above ઉપરનું આગળનું ઑબ્જેક્ટ + Next Object Below નીચેનું આગળનું ઑબ્જેક્ટ + Next Page આગળનું પેજ + Next Segment આગળનો સેગમેન્ટ + Next page આગળનું પેજ + No ના + No Break બ્રેક નહીં + No CSS CSS નથી + No Document કોઈ દસ્તાવેજ નથી + No Selection કોઈ પસંદગી નથી + No Shading શેડિંગ નથી + No Suggestions કોઈ સૂચનો નથી + No alternates for the selected character. પસંદ કરેલા અક્ષર માટે કોઈ વિકલ્પ નથી. + No artboards match. કોઈ આર્ટબોર્ડ મેળ ખાતું નથી. + No assets કોઈ ઍસેટ નથી + No assets yet. Collect art with Object › Collect for Export, or drag it into the Asset Export panel. હજી કોઈ ઍસેટ નથી. Object › Collect for Export વડે આર્ટ એકત્ર કરો, અથવા તેને Asset Export પૅનલમાં ખેંચો. + No colors to recolor (preserved colors are left out). ફરીથી રંગ આપવા કોઈ રંગ નથી (સાચવેલા રંગ બાકાત છે). + No contributor data was built into this copy. આ નકલમાં કોઈ યોગદાનકર્તા ડેટા સામેલ નથી. + No document કોઈ દસ્તાવેજ નથી + No document open કોઈ દસ્તાવેજ ખુલ્લો નથી + No document open. કોઈ દસ્તાવેજ ખુલ્લો નથી. + No equivalent સમકક્ષ નથી + No filter plug-ins installed કોઈ ફિલ્ટર પ્લગ-ઇન ઇન્સ્ટોલ નથી + No fonts are available. કોઈ ફૉન્ટ ઉપલબ્ધ નથી. + No gradient swatches કોઈ ગ્રેડિયન્ટ સ્વેચ નથી + No graphic styles કોઈ ગ્રાફિક સ્ટાઇલ નથી + No images કોઈ ઇમેજ નથી + No images match. કોઈ ઇમેજ મેળ ખાતી નથી. + No importer plug-ins installed કોઈ ઇમ્પોર્ટર પ્લગ-ઇન ઇન્સ્ટોલ નથી + No issues found. કોઈ સમસ્યા મળી નથી. + No items in this library match “{query}”. આ લાઇબ્રેરીમાં “{query}” સાથે મેળ ખાતી કોઈ આઇટમ નથી. + No model credits were built into this copy. આ નકલમાં કોઈ મૉડેલ ક્રેડિટ સામેલ નથી. + No options કોઈ વિકલ્પ નથી + No plug-ins are disabled. કોઈ પ્લગ-ઇન બંધ નથી. + No printer settings to open here અહીં ખોલવા માટે પ્રિન્ટર સેટિંગ્સ નથી + No results કોઈ પરિણામ નથી + No saved themes કોઈ સાચવેલી થીમ નથી + No selection કોઈ પસંદગી નથી + No settings found કોઈ સેટિંગ્સ મળી નથી + No text selected કોઈ લખાણ પસંદ નથી + Nonbreaking Hyphen નૉન-બ્રેકિંગ હાઇફન + Nonbreaking Space નૉન-બ્રેકિંગ સ્પેસ + None કંઈ નહીં + None. કંઈ નહીં. + Normal સામાન્ય + Normal Screen Mode સામાન્ય સ્ક્રીન મોડ + Norwegian નોર્વેજિયન + Not a script for the reference app સંદર્ભ એપ માટેની સ્ક્રિપ્ટ નથી + Not followed: outside the script's folder અનુસરાયું નથી: સ્ક્રિપ્ટના ફોલ્ડરની બહાર + Not found મળ્યું નથી + Not read વાંચ્યું નથી + Not read: a limit was reached વાંચ્યું નથી: મર્યાદા પહોંચી + Not valid UTF-8: unreadable bytes were replaced માન્ય UTF-8 નથી: ન વંચાતા બાઇટ બદલાયા + Note Paper… નોટ પેપર… + Note: નોંધ: + Nothing matches the Show filter. Show ફિલ્ટર સાથે કંઈ મેળ ખાતું નથી. + Nothing to print છાપવા માટે કંઈ નથી + Nudge નજ + Nudge in Perspective પર્સ્પેક્ટિવમાં નજ + Number Style: અંક શૈલી: + Number of Artboards: આર્ટબોર્ડની સંખ્યા: + Number of Colors: રંગોની સંખ્યા: + Number of Decimals: દશાંશની સંખ્યા: + Number of Recent Files to Display દર્શાવવાની તાજેતરની ફાઇલોની સંખ્યા + Number of Recent Fonts તાજેતરના ફૉન્ટની સંખ્યા + Number: નંબર: + Numbered List ક્રમાંકિત યાદી + Numbers અંકો + Numbers Without Units Are Points એકમ વગરના અંક પૉઇન્ટ ગણાય + Numerals અંકો + OK ઠીક + Object ઑબ્જેક્ટ + Object Highlighting ઑબ્જેક્ટ હાઇલાઇટિંગ + Object IDs: ઑબ્જેક્ટ ID: + Object Name ઑબ્જેક્ટનું નામ + Object Properties ઑબ્જેક્ટ ગુણધર્મો + Object Selection by Path Only માત્ર પાથ દ્વારા ઑબ્જેક્ટ પસંદગી + Object types: {names} ઑબ્જેક્ટ પ્રકાર: {names} + Objects ઑબ્જેક્ટ્સ + Objects on Path પાથ પરના ઑબ્જેક્ટ + Objects on Path Options પાથ પરના ઑબ્જેક્ટ વિકલ્પો + Objects on Path Tool પાથ પરના ઑબ્જેક્ટ ટૂલ + Objects using them keep their look but are no longer linked. તેમનો ઉપયોગ કરતા ઑબ્જેક્ટ તેમનો દેખાવ જાળવે છે પણ હવે લિંક નથી. + Objects with Color Effects રંગ અસર ધરાવતા ઑબ્જેક્ટ + Objects with Spot Colors સ્પૉટ રંગ ધરાવતા ઑબ્જેક્ટ + Ocean Ripple… ઓશન રિપલ… + Odia ઓડિયા + Off બંધ + Off-Axis Back ઑફ-ઍક્સિસ બૅક + Off-Axis Bottom ઑફ-ઍક્સિસ બૉટમ + Off-Axis Front ઑફ-ઍક્સિસ ફ્રન્ટ + Off-Axis Left ઑફ-ઍક્સિસ ડાબે + Off-Axis Right ઑફ-ઍક્સિસ જમણે + Off-Axis Top ઓફ-એક્સિસ ટોપ + Off: an editable copy of its art, embedded in this document. બંધ: આ દસ્તાવેજમાં સમાવિષ્ટ, તેની કલાની સંપાદનયોગ્ય નકલ. + Offset ઓફસેટ + Offset Path ઓફસેટ પાથ + Offset Path… ઓફસેટ પાથ… + Offset: ઓફસેટ: + Ogee ઓજી + Omit બાકાત રાખો + On ચાલુ + On Artboards આર્ટબોર્ડ પર + On Copy નકલ વખતે + On Paste ચોંટાડતી વખતે + On Screen સ્ક્રીન પર + On: one locked object showing the document, updated when it changes. ચાલુ: દસ્તાવેજ બતાવતી એક લૉક કરેલી ઑબ્જેક્ટ, જે બદલાય ત્યારે અપડેટ થાય છે. + One Point વન પોઇન્ટ + One Point Perspective વન પોઇન્ટ પરસ્પેક્ટિવ + One Point Perspective Preset 1 વન પોઇન્ટ પરસ્પેક્ટિવ પ્રીસેટ 1 + One Point Perspective Preset 2 વન પોઇન્ટ પરસ્પેક્ટિવ પ્રીસેટ 2 + One Point Perspective Preset 3 વન પોઇન્ટ પરસ્પેક્ટિવ પ્રીસેટ 3 + One Point Perspective Preset 4 વન પોઇન્ટ પરસ્પેક્ટિવ પ્રીસેટ 4 + One Point Perspective Preset 5 વન પોઇન્ટ પરસ્પેક્ટિવ પ્રીસેટ 5 + One file per artboard: -.{ext} દરેક આર્ટબોર્ડ માટે એક ફાઇલ: -.{ext} + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. એક કે વધુ સિમ્બોલ ઉપયોગમાં છે, અને તેમના ઇન્સ્ટન્સ વિસ્તૃત કરાય અથવા કાઢી નખાય ત્યાં સુધી તે કાઢી શકાશે નહીં. + One page per artboard. દરેક આર્ટબોર્ડ માટે એક પેજ. + Only Web Colors માત્ર વેબ રંગો + Opacity અપારદર્શકતા + Opacity & Mask Define Knockout Shape અપારદર્શકતા અને માસ્ક નૉકઆઉટ આકાર વ્યાખ્યાયિત કરે છે + Opacity Mask Info અપારદર્શકતા માસ્ક માહિતી + Opacity Mask Options અપારદર્શકતા માસ્ક વિકલ્પો + Opacity: અપારદર્શકતા: + Opacity: {value} અપારદર્શકતા: {value} + Opaque અપારદર્શક + Open ખોલો + Open Document દસ્તાવેજ ખોલો + Open Documents As Tabs દસ્તાવેજો ટેબ તરીકે ખોલો + Open Graphic Style Library ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી ખોલો + Open Location after Export નિકાસ પછી સ્થાન ખોલો + Open Paths ખુલ્લા પાથ + Open Recent File 1 તાજેતરની ફાઇલ 1 ખોલો + Open Recent File 10 તાજેતરની ફાઇલ 10 ખોલો + Open Recent File 11 તાજેતરની ફાઇલ 11 ખોલો + Open Recent File 12 તાજેતરની ફાઇલ 12 ખોલો + Open Recent File 13 તાજેતરની ફાઇલ 13 ખોલો + Open Recent File 14 તાજેતરની ફાઇલ 14 ખોલો + Open Recent File 15 તાજેતરની ફાઇલ 15 ખોલો + Open Recent File 16 તાજેતરની ફાઇલ 16 ખોલો + Open Recent File 17 તાજેતરની ફાઇલ 17 ખોલો + Open Recent File 18 તાજેતરની ફાઇલ 18 ખોલો + Open Recent File 19 તાજેતરની ફાઇલ 19 ખોલો + Open Recent File 2 તાજેતરની ફાઇલ 2 ખોલો + Open Recent File 20 તાજેતરની ફાઇલ 20 ખોલો + Open Recent File 21 તાજેતરની ફાઇલ 21 ખોલો + Open Recent File 22 તાજેતરની ફાઇલ 22 ખોલો + Open Recent File 23 તાજેતરની ફાઇલ 23 ખોલો + Open Recent File 24 તાજેતરની ફાઇલ 24 ખોલો + Open Recent File 25 તાજેતરની ફાઇલ 25 ખોલો + Open Recent File 26 તાજેતરની ફાઇલ 26 ખોલો + Open Recent File 27 તાજેતરની ફાઇલ 27 ખોલો + Open Recent File 28 તાજેતરની ફાઇલ 28 ખોલો + Open Recent File 29 તાજેતરની ફાઇલ 29 ખોલો + Open Recent File 3 તાજેતરની ફાઇલ 3 ખોલો + Open Recent File 30 તાજેતરની ફાઇલ 30 ખોલો + Open Recent File 4 તાજેતરની ફાઇલ 4 ખોલો + Open Recent File 5 તાજેતરની ફાઇલ 5 ખોલો + Open Recent File 6 તાજેતરની ફાઇલ 6 ખોલો + Open Recent File 7 તાજેતરની ફાઇલ 7 ખોલો + Open Recent File 8 તાજેતરની ફાઇલ 8 ખોલો + Open Recent File 9 તાજેતરની ફાઇલ 9 ખોલો + Open Recent Files તાજેતરની ફાઇલો ખોલો + Open Swatch Library સ્વેચ લાઇબ્રેરી ખોલો + Open With આ વડે ખોલો + Open a document to check it for issues. સમસ્યાઓ માટે તપાસવા દસ્તાવેજ ખોલો. + Open a document to draw parametric shapes. પેરામેટ્રિક આકારો દોરવા માટે દસ્તાવેજ ખોલો. + Open a document to draw with the Smart Pen. સ્માર્ટ પેન વડે દોરવા માટે દસ્તાવેજ ખોલો. + Open a document to edit its anchor points. તેના એન્કર પોઇન્ટ સંપાદિત કરવા દસ્તાવેજ ખોલો. + Open a document to edit its patterns. તેના પેટર્ન સંપાદિત કરવા દસ્તાવેજ ખોલો. + Open a document to navigate it. તેમાં નેવિગેટ કરવા દસ્તાવેજ ખોલો. + Open a document to preview its flattening. તેના ફ્લેટનિંગનું પૂર્વાવલોકન કરવા દસ્તાવેજ ખોલો. + Open a document to preview its pages તેના પેજનું પૂર્વાવલોકન કરવા દસ્તાવેજ ખોલો + Open a document to see its artboards. તેના આર્ટબોર્ડ જોવા દસ્તાવેજ ખોલો. + Open a document to see its graphic styles. તેની ગ્રાફિક સ્ટાઇલ જોવા દસ્તાવેજ ખોલો. + Open a document to see its history. તેનો ઇતિહાસ જોવા દસ્તાવેજ ખોલો. + Open a document to see its swatches. તેના સ્વેચ જોવા દસ્તાવેજ ખોલો. + Open password: ખોલવાનો પાસવર્ડ: + Open the URL in the web browser વેબ બ્રાઉઝરમાં URL ખોલો + Open this dialog with these settings next time આવતી વખતે આ સેટિંગ્સ સાથે આ ડાયલોગ ખોલો + Open this setting's help page આ સેટિંગનું મદદ પેજ ખોલો + OpenType OpenType + OpenType Features OpenType ફીચર્સ + Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. ખોલતી વખતે VectorCraft હજી વાંચી ન શકે તે ({what}) છોડી દેવાયું, તેથી તેને બદલવાથી તે કાયમ માટે ખોવાઈ જશે. તેને રાખવા માટે બીજા નામથી સાચવો. + Opens files in other apps અન્ય ઍપમાં ફાઇલો ખોલે છે + Opens network connections નેટવર્ક કનેક્શન ખોલે છે + Open… ખોલો… + Optical ઓપ્ટિકલ + Optical Margin Alignment ઓપ્ટિકલ માર્જિન એલાઇનમેન્ટ + Optimize for fast web view ઝડપી વેબ વ્યૂ માટે ઑપ્ટિમાઇઝ કરો + Optimized ઑપ્ટિમાઇઝ્ડ + Options વિકલ્પો + Options for Selection… પસંદગી માટેના વિકલ્પો… + Options: વિકલ્પો: + Options… વિકલ્પો… + Order: ક્રમ: + Ordinals ક્રમવાચક સંખ્યાઓ + Orientation અભિમુખતા + Orientation: અભિમુખતા: + Original મૂળ + Original: click to restore મૂળ: પુનઃસ્થાપિત કરવા ક્લિક કરો + Other અન્ય + Other Layers અન્ય લેયર + Other Library… અન્ય લાઇબ્રેરી… + Other Script… અન્ય સ્ક્રિપ્ટ… + Out Tangent Handles આઉટ ટેન્જન્ટ હેન્ડલ્સ + Out of fuel ઇંધણ ખતમ + Outer Corner Tile બહારની કોર્નર ટાઇલ + Outer Glow… બહારની ઝગમગાટ… + Outline આઉટલાઇન + Outline Object ઑબ્જેક્ટની આઉટલાઇન + Outline Others અન્યની આઉટલાઇન + Outline Stroke સ્ટ્રોકની આઉટલાઇન + Outline Text ટેક્સ્ટની આઉટલાઇન + Outlines આઉટલાઇન્સ + Outlines and Rules આઉટલાઇન અને નિયમો + Output All Blacks Accurately બધા બ્લેક ચોક્કસપણે આઉટપુટ કરો + Output All Blacks as Rich Black બધા બ્લેકને રિચ બ્લેક તરીકે આઉટપુટ કરો + Output Intent આઉટપુટ ઇન્ટેન્ટ + Output condition: આઉટપુટ સ્થિતિ: + Output intent profile: આઉટપુટ ઇન્ટેન્ટ પ્રોફાઇલ: + Output: આઉટપુટ: + Outside બહાર + Outside the document દસ્તાવેજની બહાર + Overlap ઓવરલેપ + Overlap: ઓવરલેપ: + Overlay ઓવરલે + Overprint ઓવરપ્રિન્ટ + Overprint Black ઓવરપ્રિન્ટ બ્લેક + Overprint Black… ઓવરપ્રિન્ટ બ્લેક… + Overprint Fill ઓવરપ્રિન્ટ ભરો + Overprint Preview ઓવરપ્રિન્ટ પૂર્વાવલોકન + Overprint Preview (separations) ઓવરપ્રિન્ટ પૂર્વાવલોકન (વિભાજન) + Overprint Stroke ઓવરપ્રિન્ટ સ્ટ્રોક + Overprint and Transparency Flattener ઓવરપ્રિન્ટ અને પારદર્શિતા ફ્લેટનર + Overprint and Transparency Flattener Options ઓવરપ્રિન્ટ અને પારદર્શિતા ફ્લેટનર વિકલ્પો + Overprint: ઓવરપ્રિન્ટ: + Overprints: ઓવરપ્રિન્ટ્સ: + Override Character Color અક્ષરનો રંગ ઓવરરાઇડ કરો + Overrides ઓવરરાઇડ્સ + PDF PDF + PDF 1.3 files have no transparency: the preset flattens it. PDF 1.3 ફાઇલોમાં પારદર્શિતા નથી: પ્રીસેટ તેને ફ્લેટ કરે છે. + PDF Info PDF માહિતી + PDF Preset PDF પ્રીસેટ + PDF Preset: PDF પ્રીસેટ: + PDF Presets PDF પ્રીસેટ્સ + PDF Presets… PDF પ્રીસેટ્સ… + PDF Settings PDF સેટિંગ્સ + PDF-compatible PDF-સુસંગત + PDF/A files carry their own output intent. PDF/A ફાઇલોમાં તેનો પોતાનો આઉટપુટ ઇન્ટેન્ટ હોય છે. + PDF/X-1a files are CMYK: every colour is converted to the destination (blank: the output intent's CMYK profile), untagged. PDF/X-1a ફાઇલો CMYK છે: દરેક રંગ ગંતવ્યમાં રૂપાંતરિત થાય છે (ખાલી: આઉટપુટ ઇન્ટેન્ટની CMYK પ્રોફાઇલ), ટૅગ વગરની. + PPI PPI + PRESET DETAILS પ્રીસેટની વિગતો + Package પેકેજ + Package collects a saved document with its linked files and fonts. પેકેજ સાચવેલા દસ્તાવેજને તેની લિંક કરેલી ફાઇલો અને ફોન્ટ સાથે એકત્ર કરે છે. + Package created પેકેજ બનાવ્યું + Package… પેકેજ… + Page પેજ + Page Isolated Blending પેજ આઇસોલેટેડ બ્લેન્ડિંગ + Page Knockout Group પેજ નૉકઆઉટ ગ્રૂપ + Pages પેજ + Paint Daubs… પેઇન્ટ ડૉબ્સ… + Paint Fills પેઇન્ટ ભરો + Paint Stops At: પેઇન્ટ અહીં અટકે છે: + Paint Strokes પેઇન્ટ સ્ટ્રોક + Paintbrush પેઇન્ટબ્રશ + Paintbrush Tool પેઇન્ટબ્રશ ટૂલ + Painting પેઇન્ટિંગ + Palette Knife… પેલેટ નાઇફ… + Panel Options… પેનલ વિકલ્પો… + Panel menu પેનલ મેનૂ + Panels પેનલ્સ + Panels Except Tools and Control ટૂલ્સ અને કંટ્રોલ સિવાયની પેનલ્સ + Paragraph ફકરો + Paragraph Style Options ફકરા સ્ટાઇલ વિકલ્પો + Paragraph Styles ફકરા સ્ટાઇલ + Paragraph Symbol ફકરા ચિહ્ન + Paragraph options ફકરા વિકલ્પો + Paragraph: ફકરો: + Parametric Shape પેરામેટ્રિક આકાર + Parametric Shape Tool પેરામેટ્રિક આકાર ટૂલ + Password પાસવર્ડ + Paste ચોંટાડો + Paste Design ડિઝાઇન ચોંટાડો + Paste Offset ઓફસેટ ચોંટાડો + Paste Remembers Layers ચોંટાડતી વખતે લેયર યાદ રાખો + Paste in Back પાછળ ચોંટાડો + Paste in Front આગળ ચોંટાડો + Paste in Place એ જ સ્થાને ચોંટાડો + Paste on All Artboards બધા આર્ટબોર્ડ પર ચોંટાડો + Paste without Formatting ફોર્મેટિંગ વગર ચોંટાડો + Pasted ચોંટાડ્યું + Pastels પેસ્ટલ + Patchwork… પેચવર્ક… + Path પાથ + Path Effects પાથ ઇફેક્ટ્સ + Path Eraser પાથ ઇરેઝર + Path Eraser Tool પાથ ઇરેઝર ટૂલ + Path Precision Tools પાથ ચોકસાઈ ટૂલ્સ + Path: પાથ: + Pathfinder પાથફાઇન્ડર + Pathfinder Options પાથફાઇન્ડર વિકલ્પો + Pathfinder Options… પાથફાઇન્ડર વિકલ્પો… + Pathfinders: પાથફાઇન્ડર્સ: + Paths પાથ + Pattern પેટર્ન + Pattern Borders પેટર્ન બોર્ડર + Pattern Brush પેટર્ન બ્રશ + Pattern Brush Options પેટર્ન બ્રશ વિકલ્પો + Pattern Editing પેટર્ન સંપાદન + Pattern Options પેટર્ન વિકલ્પો + Pattern options are available in pattern editing mode. પેટર્ન વિકલ્પો પેટર્ન સંપાદન મોડમાં ઉપલબ્ધ છે. + Patterns પેટર્ન + Pause For આ સમય માટે થોભાવો + Pen પેન + Pen Tool પેન ટૂલ + Pencil પેન્સિલ + Pencil Tool પેન્સિલ ટૂલ + Percent: ટકા: + Perceptual પર્સેપ્ચ્યુઅલ + Perceptual Scales પર્સેપ્ચ્યુઅલ સ્કેલ + Performance પ્રદર્શન + Permissions પરવાનગીઓ + Permissions password: પરવાનગી પાસવર્ડ: + Perspective પરસ્પેક્ટિવ + Perspective Distort પરસ્પેક્ટિવ વિકૃત + Perspective Grid પરસ્પેક્ટિવ ગ્રિડ + Perspective Grid Options પરસ્પેક્ટિવ ગ્રિડ વિકલ્પો + Perspective Grid Preset પરસ્પેક્ટિવ ગ્રિડ પ્રીસેટ + Perspective Grid Presets પરસ્પેક્ટિવ ગ્રિડ પ્રીસેટ્સ + Perspective Grid Presets… પરસ્પેક્ટિવ ગ્રિડ પ્રીસેટ્સ… + Perspective Grid Settings પરસ્પેક્ટિવ ગ્રિડ સેટિંગ્સ + Perspective Grid Tool પરસ્પેક્ટિવ ગ્રિડ ટૂલ + Perspective Plane Options… પરસ્પેક્ટિવ પ્લેન વિકલ્પો… + Perspective Selection Tool પરસ્પેક્ટિવ પસંદગી ટૂલ + Photocopy… ફોટોકોપી… + Picas પાઇકા + Pick Color From: આમાંથી રંગ પસંદ કરો: + Pie End Angle: પાઇ અંતિમ કોણ: + Pie Graph Tool પાઇ ગ્રાફ ટૂલ + Pie Start Angle: પાઇ પ્રારંભ કોણ: + Pin Bar Position પિન બાર સ્થિતિ + Pink ગુલાબી + Pivot: પિવટ: + Pixel Preview પિક્સેલ પૂર્વાવલોકન + Pixelate પિક્સેલેટ + Pixels પિક્સેલ + Place મૂકો + Place DXF DXF મૂકો + Place PDF PDF મૂકો + Place Symbol Instance સિમ્બોલ ઇન્સ્ટન્સ મૂકો + Place from Library લાઇબ્રેરીમાંથી મૂકો + Place the pages with the placement again પ્લેસમેન્ટ સાથે પેજ ફરી મૂકો + Placed File Info મૂકેલી ફાઇલની માહિતી + Placement Options પ્લેસમેન્ટ વિકલ્પો + Placement Options… પ્લેસમેન્ટ વિકલ્પો… + Placement: પ્લેસમેન્ટ: + Place… મૂકો… + Plane પ્લેન + Plaster… પ્લાસ્ટર… + Plastic પ્લાસ્ટિક + Plastic Wrap… પ્લાસ્ટિક રેપ… + Plates પ્લેટ્સ + Platform: પ્લેટફોર્મ: + Play Action ક્રિયા ચલાવો + Playback Options પ્લેબેક વિકલ્પો + Plug-in Info પ્લગ-ઇન માહિતી + Plug-ins પ્લગ-ઇન્સ + Plug-ins & Scratch Disks પ્લગ-ઇન્સ અને સ્ક્રેચ ડિસ્ક + Plug-in… પ્લગ-ઇન… + Point Text પોઇન્ટ ટેક્સ્ટ + Point Text Objects પોઇન્ટ ટેક્સ્ટ ઑબ્જેક્ટ્સ + Pointillize… પોઇન્ટિલાઇઝ… + Points પોઇન્ટ્સ + Points per segment પ્રતિ સેગમેન્ટ પોઇન્ટ + Points: પોઇન્ટ્સ: + Points: clicks add free points પોઇન્ટ્સ: ક્લિક મુક્ત પોઇન્ટ ઉમેરે છે + Polar Grid પોલર ગ્રિડ + Polar Grid Tool પોલર ગ્રિડ ટૂલ + Polar Grid Tool Options પોલર ગ્રિડ ટૂલ વિકલ્પો + Polish પોલિશ + Polygon બહુકોણ + Polygon Properties: બહુકોણના ગુણધર્મો: + Polygon Tool બહુકોણ ટૂલ + Portrait પોટ્રેટ + Portuguese પોર્ટુગીઝ + Position સ્થિતિ + Position: સ્થિતિ: + PostScript: PostScript: + Poster Edges… પોસ્ટર ધાર… + Power Saving (integrated) પાવર સેવિંગ (સંકલિત) + Precision ચોકસાઈ + Precision: ચોકસાઈ: + Preferences પસંદગીઓ + Preferences… પસંદગીઓ… + Prefix: ઉપસર્ગ: + Preparing the image… છબી તૈયાર કરી રહ્યું છે… + Presentation Mode પ્રેઝન્ટેશન મોડ + Preserve Alpha Transparency આલ્ફા પારદર્શિતા જાળવો + Preserve Appearance and Overprints દેખાવ અને ઓવરપ્રિન્ટ જાળવો + Preserve CMYK Numbers CMYK નંબર જાળવો + Preserve Editing Capabilities સંપાદન ક્ષમતાઓ જાળવો + Preserve Lightness તેજ જાળવો + Preserve Overprints and Spot Colors ઓવરપ્રિન્ટ અને સ્પોટ રંગ જાળવો + Preserve Paths પાથ જાળવો + Preserve Shape Using: આ વડે આકાર જાળવો: + Preserve Spot Colors સ્પોટ રંગ જાળવો + Preserve Text Appearance ટેક્સ્ટનો દેખાવ જાળવો + Preserve Text Editability ટેક્સ્ટ સંપાદનક્ષમતા જાળવો + Preserve editing capabilities સંપાદન ક્ષમતાઓ જાળવો + Preserve: જાળવો: + Preset Name પ્રીસેટનું નામ + Preset name: પ્રીસેટનું નામ: + Preset: પ્રીસેટ: + Presets પ્રીસેટ્સ + Presets you save with the Save Preset button appear here. સાચવો પ્રીસેટ બટન વડે તમે સાચવેલા પ્રીસેટ અહીં દેખાય છે. + Presets: પ્રીસેટ્સ: + Press દબાવો + Press Play Current Selection in the Actions panel to go on. આગળ વધવા માટે ક્રિયા પેનલમાં વર્તમાન પસંદગી ચલાવો દબાવો. + Press a key… કોઈ કી દબાવો… + Preview પૂર્વાવલોકન + Preview (1 inch on the page) પૂર્વાવલોકન (પેજ પર 1 ઇંચ) + Preview All Layers બધા લેયરનું પૂર્વાવલોકન + Preview Mode પૂર્વાવલોકન મોડ + Preview in Browser બ્રાઉઝરમાં પૂર્વાવલોકન + Preview the document as it is now અત્યારે જેમ છે તેમ દસ્તાવેજનું પૂર્વાવલોકન કરો + Previous પાછલું + Previous Artboard પાછલું આર્ટબોર્ડ + Previous Data Set પાછલો ડેટા સેટ + Previous Document પાછલો દસ્તાવેજ + Previous Page પાછલું પેજ + Previous page પાછલું પેજ + Primary પ્રાથમિક + Print છાપો + Print Ink Fixes પ્રિન્ટ ઇંક ફિક્સ + Print Ink Fixes… પ્રિન્ટ ઇંક ફિક્સ… + Print Layers: પ્રિન્ટ લેયર: + Print Preset પ્રિન્ટ પ્રીસેટ + Print Preset: પ્રિન્ટ પ્રીસેટ: + Print Presets પ્રિન્ટ પ્રીસેટ્સ + Print Presets… પ્રિન્ટ પ્રીસેટ્સ… + Print Preview પ્રિન્ટ પૂર્વાવલોકન + Print Setup પ્રિન્ટ સેટઅપ + Print Tiling પ્રિન્ટ ટાઇલિંગ + Print Tiling Pages પ્રિન્ટ ટાઇલિંગ પેજ + Print Tiling Tool પ્રિન્ટ ટાઇલિંગ ટૂલ + Print as Bitmap બિટમેપ તરીકે પ્રિન્ટ કરો + Printer પ્રિન્ટર + Printer Setup… પ્રિન્ટર સેટઅપ… + Printer mark type: પ્રિન્ટર માર્ક પ્રકાર: + Printer profile: પ્રિન્ટર પ્રોફાઇલ: + Printer: પ્રિન્ટર: + Printers પ્રિન્ટર્સ + Printing / Exporting છાપવું / નિકાસ + Printing allowed: છાપવાની મંજૂરી: + Printing and Proofing પ્રિન્ટિંગ અને પ્રૂફિંગ + Print… છાપો… + Problems સમસ્યાઓ + Process Color પ્રોસેસ રંગ + Profile inclusion: પ્રોફાઇલ સમાવેશ: + Profile: પ્રોફાઇલ: + Progressive પ્રોગ્રેસિવ + Projecting Cap પ્રોજેક્ટિંગ કેપ + Proof Colors પ્રૂફ રંગો + Proof Setup પ્રૂફ સેટઅપ + Properties ગુણધર્મો + Proportional Metrics પ્રમાણસર મેટ્રિક્સ + Pucker પકર + Pucker & Bloat… પકર અને બ્લોટ… + Pucker Tool પકર ટૂલ + Punctuation વિરામચિહ્નો + Punctuation Space વિરામચિહ્ન જગ્યા + Punjabi પંજાબી + Puppet Warp પપેટ વોર્પ + Puppet Warp Pins પપેટ વોર્પ પિન + Puppet Warp Tool પપેટ વોર્પ ટૂલ + Push In First પહેલા અંદર ધકેલો + Push Out First પહેલા બહાર ધકેલો + Push Out Only માત્ર બહાર ધકેલો + Put back in the toolbar ટૂલબારમાં પાછું મૂકો + Q Q + Quality {quality} ગુણવત્તા {quality} + Quality: ગુણવત્તા: + Quarter Space ચોથા ભાગની જગ્યા + Quick Actions ઝડપી ક્રિયાઓ + Quickly start a new file નવી ફાઇલ ઝડપથી શરૂ કરો + Quit VectorCraft VectorCraft બંધ કરો + RECENT તાજેતરનાં + RGB Color RGB રંગ + RGB Images RGB છબીઓ + RGB: RGB: + Radar Graph Tool રડાર ગ્રાફ ટૂલ + Radial રેડિયલ + Radial Blur… રેડિયલ બ્લર… + Radial Dividers: રેડિયલ વિભાજક: + Radial Glows રેડિયલ ઝગમગાટ + Radial Gradient રેડિયલ ગ્રેડિઅન્ટ + Radial Repeat રેડિયલ રિપીટ + Radius: ત્રિજ્યા: + Rainbow મેઘધનુષ્ય + Ramp: રેમ્પ: + Random રેન્ડમ + Randomize Colors રંગો રેન્ડમ કરો + Randomize Opacity અપારદર્શકતા રેન્ડમ કરો + Randomize Opacity… અપારદર્શકતા રેન્ડમ કરો… + Randomly change color order રંગોનો ક્રમ રેન્ડમ રીતે બદલો + Randomly change saturation and brightness સંતૃપ્તિ અને તેજ રેન્ડમ રીતે બદલો + Range: શ્રેણી: + Raster Effects રાસ્ટર ઇફેક્ટ્સ + Raster File Format: રાસ્ટર ફાઇલ ફોર્મેટ: + Raster Sample Size: રાસ્ટર નમૂના માપ: + Raster/Vector Balance: રાસ્ટર/વેક્ટર સંતુલન: + Rasterize રાસ્ટરાઇઝ કરો + Rasterize Unsupported Art અસમર્થિત કલાને રાસ્ટરાઇઝ કરો + Rasterize… રાસ્ટરાઇઝ કરો… + Rasters રાસ્ટર + Rating: રેટિંગ: + Ratio ગુણોત્તર + Rays કિરણો + Read Action Set File ક્રિયા સેટ ફાઇલ વાંચો + Read Shortcut Set File શોર્ટકટ સેટ ફાઇલ વાંચો + Read files you open in its format and make documents from them તમે ખોલો તે ફાઇલોને તેના ફોર્મેટમાં વાંચો અને તેમાંથી દસ્તાવેજ બનાવો + Read the objects it is given આપેલી ઑબ્જેક્ટ્સ વાંચો + Reading the file… ફાઇલ વાંચી રહ્યું છે… + Reads or sets environment variables એન્વાયર્નમેન્ટ વેરીએબલ વાંચે કે સેટ કરે છે + Ready તૈયાર + Real World: વાસ્તવિક વિશ્વ: + Real name વાસ્તવિક નામ + Real-time Drawing and Editing રિયલ-ટાઇમ ડ્રોઇંગ અને સંપાદન + Rearrange ફરીથી ગોઠવો + Rearrange All બધું ફરીથી ગોઠવો + Rearrange All Artboards બધા આર્ટબોર્ડ ફરીથી ગોઠવો + Rearrange All Artboards… બધા આર્ટબોર્ડ ફરીથી ગોઠવો… + Rearrange Artboards આર્ટબોર્ડ ફરીથી ગોઠવો + Recall Selection પસંદગી યાદ કરો + Recent Colors તાજેતરના રંગો + Recent Files તાજેતરની ફાઇલો + Recent Font 1 તાજેતરનો ફોન્ટ 1 + Recent Font 10 તાજેતરનો ફોન્ટ 10 + Recent Font 11 તાજેતરનો ફોન્ટ 11 + Recent Font 12 તાજેતરનો ફોન્ટ 12 + Recent Font 13 તાજેતરનો ફોન્ટ 13 + Recent Font 14 તાજેતરનો ફોન્ટ 14 + Recent Font 15 તાજેતરનો ફોન્ટ 15 + Recent Font 2 તાજેતરનો ફોન્ટ 2 + Recent Font 3 તાજેતરનો ફોન્ટ 3 + Recent Font 4 તાજેતરનો ફોન્ટ 4 + Recent Font 5 તાજેતરનો ફોન્ટ 5 + Recent Font 6 તાજેતરનો ફોન્ટ 6 + Recent Font 7 તાજેતરનો ફોન્ટ 7 + Recent Font 8 તાજેતરનો ફોન્ટ 8 + Recent Font 9 તાજેતરનો ફોન્ટ 9 + Recent Fonts તાજેતરના ફોન્ટ્સ + Recent URLs તાજેતરના URL + Recolor રંગ બદલો + Recolor Artwork કલાકૃતિનો રંગ બદલો + Recolor Artwork… કલાકૃતિનો રંગ બદલો… + Recolor this row આ પંક્તિનો રંગ બદલો + Recolor with Preset પ્રીસેટ વડે રંગ બદલો + Record રેકોર્ડ + Record Stop રેકોર્ડ બંધ કરો + Recover Documents દસ્તાવેજો પુનઃપ્રાપ્ત કરો + Recovery Data પુનઃપ્રાપ્તિ ડેટા + Rectangle લંબચોરસ + Rectangle Cut લંબચોરસ કટ + Rectangle Cut Tool લંબચોરસ કટ ટૂલ + Rectangle Properties: લંબચોરસ પ્રોપર્ટીઝ: + Rectangle Tool લંબચોરસ ટૂલ + Rectangle… લંબચોરસ… + Rectangular Grid લંબચોરસ ગ્રિડ + Rectangular Grid Tool લંબચોરસ ગ્રિડ ટૂલ + Rectangular Grid Tool Options લંબચોરસ ગ્રિડ ટૂલ વિકલ્પો + Red લાલ + Redefine Character Style અક્ષર સ્ટાઇલ ફરીથી વ્યાખ્યાયિત કરો + Redefine Graphic Style ગ્રાફિક સ્ટાઇલ ફરીથી વ્યાખ્યાયિત કરો + Redefine Graphic Style “{name}” ગ્રાફિક સ્ટાઇલ “{name}” ફરીથી વ્યાખ્યાયિત કરો + Redefine Paragraph Style ફકરા સ્ટાઇલ ફરીથી વ્યાખ્યાયિત કરો + Redefine Symbol સિમ્બોલ ફરીથી વ્યાખ્યાયિત કરો + Redefine {name} {name} ફરીથી વ્યાખ્યાયિત કરો + Redo ફરીથી કરો + Redo {name} {name} ફરીથી કરો + Reduce Colors રંગો ઘટાડો + Reduce to Basic Appearance મૂળભૂત દેખાવમાં ઘટાડો + Reduction: ઘટાડો: + Reference Point સંદર્ભ બિંદુ + Reflect પ્રતિબિંબિત કરો + Reflect Tool પ્રતિબિંબ ટૂલ + Reflect X X પ્રતિબિંબ + Reflect Y Y પ્રતિબિંબ + Reflect… પ્રતિબિંબિત કરો… + Refresh તાજું કરો + Refresh Deferred Effects સ્થગિત ઇફેક્ટ્સ તાજી કરો + Refresh Font List ફોન્ટ યાદી તાજી કરો + Registered Trademark Symbol રજિસ્ટર્ડ ટ્રેડમાર્ક સિમ્બોલ + Registration: નોંધણી: + Registry name: રજિસ્ટ્રી નામ: + Regular સામાન્ય + Regular expression not closed નિયમિત અભિવ્યક્તિ બંધ નથી + Rejoin Path Pieces પાથના ટુકડા ફરી જોડો + Rejoin Path Pieces… પાથના ટુકડા ફરી જોડો… + Relative સાપેક્ષ + Release મુક્ત કરો + Release Blend બ્લેન્ડ મુક્ત કરો + Release Clipping Mask ક્લિપિંગ માસ્ક મુક્ત કરો + Release Compound Path કમ્પાઉન્ડ પાથ મુક્ત કરો + Release Compound Shape કમ્પાઉન્ડ શેપ મુક્ત કરો + Release Envelope એન્વેલપ મુક્ત કરો + Release Guides ગાઇડ મુક્ત કરો + Release Image Trace ઇમેજ ટ્રેસ મુક્ત કરો + Release Intertwine ઇન્ટરટ્વાઇન મુક્ત કરો + Release Live Paint લાઇવ પેઇન્ટ મુક્ત કરો + Release Opacity Mask ઓપેસિટી માસ્ક મુક્ત કરો + Release Repeat રિપીટ મુક્ત કરો + Release Selection પસંદગી મુક્ત કરો + Release Shaper Group શેપર ગ્રૂપ મુક્ત કરો + Release Slice સ્લાઇસ મુક્ત કરો + Release Text Wrap ટેક્સ્ટ રેપ મુક્ત કરો + Release Threaded Text થ્રેડેડ ટેક્સ્ટ મુક્ત કરો + Release Type on a Path પાથ પર ટાઇપ મુક્ત કરો + Release Type on a Path… પાથ પર ટાઇપ મુક્ત કરો… + Release to Layers (Build) લેયર્સમાં મુક્ત કરો (બિલ્ડ) + Release to Layers (Sequence) લેયર્સમાં મુક્ત કરો (સિક્વન્સ) + Release with Perspective પર્સ્પેક્ટિવ સાથે મુક્ત કરો + Relink ફરી લિંક કરો + Relink to Folder… ફોલ્ડરમાં ફરી લિંક કરો… + Relink… ફરી લિંક કરો… + Reload Plug-ins પ્લગ-ઇન્સ ફરી લોડ કરો + Remember Import Matches ઇમ્પોર્ટ મેચ યાદ રાખો + Remember My Choices મારી પસંદગીઓ યાદ રાખો + Remove દૂર કરો + Remove Anchor Points એન્કર પોઇન્ટ દૂર કરો + Remove Asset એસેટ દૂર કરો + Remove Breaks બ્રેક દૂર કરો + Remove Brush Stroke બ્રશ સ્ટ્રોક દૂર કરો + Remove Color Profile… કલર પ્રોફાઇલ દૂર કરો… + Remove Corners ખૂણા દૂર કરો + Remove Effect ઇફેક્ટ દૂર કરો + Remove Effects ઇફેક્ટ્સ દૂર કરો + Remove Gaps ગેપ દૂર કરો + Remove Guide ગાઇડ દૂર કરો + Remove Item આઇટમ દૂર કરો + Remove JavaScript File JavaScript ફાઇલ દૂર કરો + Remove Line Breaks લાઇન બ્રેક દૂર કરો + Remove Paragraph Breaks ફકરા બ્રેક દૂર કરો + Remove Plug-in પ્લગ-ઇન દૂર કરો + Remove Points, Keep Shape પોઇન્ટ દૂર કરો, આકાર જાળવો + Remove Redundant Points વધારાના પોઇન્ટ દૂર કરો + Remove Selected Assets પસંદ કરેલી એસેટ્સ દૂર કરો + Remove Selected Entry પસંદ કરેલી એન્ટ્રી દૂર કરો + Remove Strokes સ્ટ્રોક દૂર કરો + Remove Threading થ્રેડિંગ દૂર કરો + Remove blank lines between paragraphs ફકરાઓ વચ્ચેની ખાલી લાઇનો દૂર કરો + Remove from Shaper Group શેપર ગ્રૂપમાંથી દૂર કરો + Remove “{keyword}” “{keyword}” દૂર કરો + Rename નામ બદલો + Rename Action ક્રિયાનું નામ બદલો + Rename Artboards આર્ટબોર્ડ્સનું નામ બદલો + Rename Asset એસેટનું નામ બદલો + Rename Character Style અક્ષર સ્ટાઇલનું નામ બદલો + Rename Data Set ડેટા સેટનું નામ બદલો + Rename Data Set… ડેટા સેટનું નામ બદલો… + Rename Design ડિઝાઇનનું નામ બદલો + Rename Graphic Style ગ્રાફિક સ્ટાઇલનું નામ બદલો + Rename Library લાઇબ્રેરીનું નામ બદલો + Rename Library Group લાઇબ્રેરી ગ્રૂપનું નામ બદલો + Rename Paragraph Style ફકરા સ્ટાઇલનું નામ બદલો + Rename Preset પ્રીસેટનું નામ બદલો + Rename Style સ્ટાઇલનું નામ બદલો + Rename Toolbar ટૂલબારનું નામ બદલો + Rename Variable વેરિએબલનું નામ બદલો + Rename View વ્યૂનું નામ બદલો + Rename Workspace વર્કસ્પેસનું નામ બદલો + Render Threads (-1 = Automatic) રેન્ડર થ્રેડ્સ (-1 = આપમેળે) + Rendering intent: રેન્ડરિંગ ઇરાદો: + Reorder ક્રમ બદલો + Reorder Appearance દેખાવનો ક્રમ બદલો + Reorder Appearance Item દેખાવ આઇટમનો ક્રમ બદલો + Reorder Artboards આર્ટબોર્ડ્સનો ક્રમ બદલો + Repeat પુનરાવર્તન કરો + Repeat Options પુનરાવર્તન વિકલ્પો + Repeat Pathfinder પાથફાઇન્ડર પુનરાવર્તન કરો + Repeat {name} {name} પુનરાવર્તન કરો + Repeating પુનરાવર્તન + Replace બદલો + Replace Font ફોન્ટ બદલો + Replace Missing Fonts ગુમ ફોન્ટ બદલો + Replace Missing Fonts… ગુમ ફોન્ટ બદલો… + Replace Spine સ્પાઇન બદલો + Replace Symbol સિમ્બોલ બદલો + Replace With Font ફોન્ટ વડે બદલો + Replace With Font From આમાંથી ફોન્ટ વડે બદલો + Replace it with another file, or ignore it: its images keep showing the preview saved with the document. તેને બીજી ફાઇલથી બદલો, અથવા અવગણો: તેની છબીઓ દસ્તાવેજ સાથે સાચવેલું પૂર્વાવલોકન બતાવતી રહેશે. + Replace with Top Object ટોચની ઑબ્જેક્ટ વડે બદલો + Replace with Top Object… ટોચની ઑબ્જેક્ટ વડે બદલો… + Replace “{name}”, the file this document was opened from? “{name}” બદલીએ, જે ફાઇલમાંથી આ દસ્તાવેજ ખોલવામાં આવ્યો હતો તે? + Replace: બદલો: + Replaces the saved preset of that name. એ નામનો સાચવેલો પ્રીસેટ બદલાશે. + Replace… બદલો… + Report copied રિપોર્ટ નકલ થયો + Require a password to open the document દસ્તાવેજ ખોલવા પાસવર્ડ જરૂરી + Reselect ફરી પસંદ કરો + Reset રીસેટ કરો + Reset Bar Position બાર સ્થિતિ રીસેટ કરો + Reset Bounding Box બાઉન્ડિંગ બોક્સ રીસેટ કરો + Reset Envelope Mesh એન્વેલપ મેશ રીસેટ કરો + Reset Envelope Shape એન્વેલપ આકાર રીસેટ કરો + Reset Essentials એસેન્શિયલ્સ રીસેટ કરો + Reset Horizontal Scale આડો સ્કેલ રીસેટ કરો + Reset Keyboard Shortcuts કીબોર્ડ શૉર્ટકટ રીસેટ કરો + Reset OpenType Features OpenType ફીચર્સ રીસેટ કરો + Reset Panel પેનલ રીસેટ કરો + Reset Preferences પસંદગીઓ રીસેટ કરો + Reset Profiles પ્રોફાઇલ્સ રીસેટ કરો + Reset Rotate View વ્યૂ ફેરવવાનું રીસેટ કરો + Reset Ruler Origin રૂલર ઓરિજિન રીસેટ કરો + Reset Symbol Instance સિમ્બોલ ઇન્સ્ટન્સ રીસેટ કરો + Reset Text Transform ટેક્સ્ટ ટ્રાન્સફોર્મ રીસેટ કરો + Reset Text Transform and Scale ટેક્સ્ટ ટ્રાન્સફોર્મ અને સ્કેલ રીસેટ કરો + Reset Tracking and Kerning ટ્રેકિંગ અને કર્નિંગ રીસેટ કરો + Reset Transformation ટ્રાન્સફોર્મેશન રીસેટ કરો + Reset all બધું રીસેટ કરો + Reset all 3D settings to their defaults. બધી 3D સેટિંગ્સ ડિફોલ્ટ પર રીસેટ કરો. + Reset all Revolve settings to their defaults. બધી રિવોલ્વ સેટિંગ્સ ડિફોલ્ટ પર રીસેટ કરો. + Reset all preferences to their defaults? બધી પસંદગીઓ ડિફોલ્ટ પર રીસેટ કરીએ? + Reset the rows to the artwork's colors પંક્તિઓ આર્ટવર્કના રંગો પર રીસેટ કરો + Reset to Inverse ઊલટા પર રીસેટ કરો + Reset to Linear લિનિયર પર રીસેટ કરો + Reset to White, Black સફેદ, કાળા પર રીસેટ કરો + Reset to default ડિફોલ્ટ પર રીસેટ કરો + Reset with Mesh મેશ વડે રીસેટ કરો + Reset with Mesh… મેશ વડે રીસેટ કરો… + Reset with Warp વોર્પ વડે રીસેટ કરો + Reset with Warp… વોર્પ વડે રીસેટ કરો… + Reset {name} {name} રીસેટ કરો + Reshape રીશેપ + Reshape Segment સેગમેન્ટ રીશેપ કરો + Reshape Spine સ્પાઇન રીશેપ કરો + Reshape Spine Segment સ્પાઇન સેગમેન્ટ રીશેપ કરો + Reshape Tool રીશેપ ટૂલ + Reshape Type Area ટાઇપ એરિયા રીશેપ કરો + Reshape Type Path ટાઇપ પાથ રીશેપ કરો + Resize માપ બદલો + Resize Artboard આર્ટબોર્ડનું માપ બદલો + Resize Slice સ્લાઇસનું માપ બદલો + Resize Type Area ટાઇપ એરિયાનું માપ બદલો + Resolution: રિઝોલ્યુશન: + Responsive રિસ્પોન્સિવ + Restore પુનઃસ્થાપિત કરો + Restore Recovered Documents પુનઃપ્રાપ્ત દસ્તાવેજો પુનઃસ્થાપિત કરો + Restore every preference to its default (applied on OK) દરેક પસંદગી તેના ડિફોલ્ટ પર પુનઃસ્થાપિત કરો (ઠીક પર લાગુ થશે) + Restored documents open unsaved: save them to keep them. પુનઃસ્થાપિત દસ્તાવેજો અસાચવેલા ખુલશે: તેમને રાખવા માટે સાચવો. + Restrict printing, editing and other tasks છાપકામ, સંપાદન અને અન્ય કાર્યો પર પ્રતિબંધ મૂકો + Reticulation… રેટિક્યુલેશન… + Retract Handles હેન્ડલ પાછા ખેંચો + Retract Handles… હેન્ડલ પાછા ખેંચો… + Reverse Direction દિશા ઊલટાવો + Reverse Front to Back આગળથી પાછળ ઊલટાવો + Reverse Gradient ગ્રેડિયન્ટ ઊલટાવો + Reverse Order ક્રમ ઊલટાવો + Reverse Path Direction પાથની દિશા ઊલટાવો + Reverse Path Direction Off પાથની દિશા ઊલટાવવાનું બંધ + Reverse Path Direction On પાથની દિશા ઊલટાવવાનું ચાલુ + Reverse Spine સ્પાઇન ઊલટાવો + Reverse the order ક્રમ ઊલટાવો + Revert મૂળ સ્થિતિમાં લાવો + Revert to the saved version of “{name}”? “{name}”નું સાચવેલું સંસ્કરણ પાછું લાવીએ? + Revolve રિવોલ્વ + Revolve angle રિવોલ્વ ખૂણો + Revolve… રિવોલ્વ… + Right જમણે + Right Grid: જમણી ગ્રિડ: + Right Indent જમણો ઇન્ડેન્ટ + Right Plane જમણું પ્લેન + Right to Left જમણેથી ડાબે + Right-Justified Tab જમણે-સંરેખિત ટૅબ + Right-to-Left Paragraph Direction જમણેથી ડાબે ફકરાની દિશા + Right: જમણે: + Rings રિંગ્સ + Rise… રાઇઝ… + Roman રોમન + Roman Baseline રોમન બેઝલાઇન + Roman Hanging Punctuation રોમન હેંગિંગ વિરામચિહ્નો + Rotate ફેરવો + Rotate Tool ફેરવો ટૂલ + Rotate View વ્યૂ ફેરવો + Rotate View Tool વ્યૂ ફેરવો ટૂલ + Rotate View to Selection વ્યૂ પસંદગી પ્રમાણે ફેરવો + Rotate: ફેરવો: + Rotate… ફેરવો… + Rotation પરિભ્રમણ + Rotation X પરિભ્રમણ X + Rotation Y પરિભ્રમણ Y + Rotation Z પરિભ્રમણ Z + Rotation relative to: આના સાપેક્ષે પરિભ્રમણ: + Rotation: પરિભ્રમણ: + Rough Pastels… રફ પેસ્ટલ્સ… + Roughen… રફ બનાવો… + Round ગોળ + Round Cap ગોળ કેપ + Round Corners… ગોળ ખૂણા… + Round Join ગોળ જોઇન + Rounded ગોળાકાર + Rounded Rectangle ગોળાકાર લંબચોરસ + Rounded Rectangle Tool ગોળાકાર લંબચોરસ ટૂલ + Rounded Rectangle… ગોળાકાર લંબચોરસ… + Roundness: ગોળાઈ: + Row Size: પંક્તિનું માપ: + Rows: પંક્તિઓ: + Ruby રૂબી + Ruby… રૂબી… + Rule: નિયમ: + Ruler Origin રૂલર ઓરિજિન + Rulers રૂલર્સ + Rulers & Grids રૂલર્સ અને ગ્રિડ + Run ચલાવો + Run Plug-in પ્લગ-ઇન ચલાવો + Run Script સ્ક્રિપ્ટ ચલાવો + Run these commands on the document: {ids} દસ્તાવેજ પર આ આદેશો ચલાવો: {ids} + Runs code the checker can't read ચકાસણીકર્તા વાંચી ન શકે તેવો કોડ ચલાવે છે + Russian રશિયન + SAVED સાચવેલું + SVG Filter SVG ફિલ્ટર + SVG Filters SVG ફિલ્ટર્સ + SVG Interactivity SVG ઇન્ટરેક્ટિવિટી + SVG Options SVG વિકલ્પો + SVG Profile: SVG પ્રોફાઇલ: + Safe Mode સેફ મોડ + Safe Mode… સેફ મોડ… + Same સમાન + Same file સમાન ફાઇલ + Same item as a later row પછીની પંક્તિ જેવી જ આઇટમ + Same size સમાન માપ + Sample Color નમૂના રંગ + Sample Color from Screen સ્ક્રીનમાંથી નમૂના રંગ + Sans Serif / Gothic સાન્સ સેરિફ / ગોથિક + Sanskrit સંસ્કૃત + Saturate સંતૃપ્ત કરો + Saturate… સંતૃપ્ત કરો… + Saturation સંતૃપ્તિ + Saturation and brightness સંતૃપ્તિ અને તેજ + Save સાચવો + Save Actions… ક્રિયાઓ સાચવો… + Save As આ રીતે સાચવો + Save As Preset… પ્રીસેટ તરીકે સાચવો… + Save As… આ રીતે સાચવો… + Save Brush Library બ્રશ લાઇબ્રેરી સાચવો + Save Brush Library… બ્રશ લાઇબ્રેરી સાચવો… + Save Brushes… બ્રશ સાચવો… + Save Changes ફેરફારો સાચવો + Save Colors as Swatches રંગો સ્વેચ તરીકે સાચવો + Save Document દસ્તાવેજ સાચવો + Save Document Info… દસ્તાવેજની માહિતી સાચવો… + Save Document Preset દસ્તાવેજ પ્રીસેટ સાચવો + Save Export Preset એક્સપોર્ટ પ્રીસેટ સાચવો + Save Gradient Preset ગ્રેડિયન્ટ પ્રીસેટ સાચવો + Save Gradient Preset… ગ્રેડિયન્ટ પ્રીસેટ સાચવો… + Save Graphic Style Library ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી સાચવો + Save Graphic Style Library… ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી સાચવો… + Save Grid as Preset ગ્રિડ પ્રીસેટ તરીકે સાચવો + Save Grid as Preset… ગ્રિડ પ્રીસેટ તરીકે સાચવો… + Save Image Trace Preset ઇમેજ ટ્રેસ પ્રીસેટ સાચવો + Save PDF PDF સાચવો + Save PDF Dialog PDF સંવાદ સાચવો + Save PDF Preset PDF પ્રીસેટ સાચવો + Save Perspective Grid Preset પર્સ્પેક્ટિવ ગ્રિડ પ્રીસેટ સાચવો + Save Preset પ્રીસેટ સાચવો + Save Preset… પ્રીસેટ સાચવો… + Save Print Preset પ્રિન્ટ પ્રીસેટ સાચવો + Save Recovery Data પુનઃપ્રાપ્તિ ડેટા સાચવો + Save Save for Web Preset વેબ માટેનો પ્રીસેટ સાચવો + Save Selection પસંદગી સાચવો + Save Selection… પસંદગી સાચવો… + Save Swatch Library સ્વેચ લાઇબ્રેરી સાચવો + Save Swatch Library… સ્વેચ લાઇબ્રેરી સાચવો… + Save Symbol Library સિમ્બોલ લાઇબ્રેરી સાચવો + Save Symbol Library… સિમ્બોલ લાઇબ્રેરી સાચવો… + Save Symbols… સિમ્બોલ સાચવો… + Save Theme થીમ સાચવો + Save To: આ સ્થળે સાચવો: + Save Transparency Flattener Preset પારદર્શિતા ફ્લેટનર પ્રીસેટ સાચવો + Save Transparency Flattener Preset… પારદર્શિતા ફ્લેટનર પ્રીસેટ સાચવો… + Save Variable Library… વેરિએબલ લાઇબ્રેરી સાચવો… + Save a Copy નકલ સાચવો + Save a Copy… નકલ સાચવો… + Save a Timestamped Copy ટાઇમસ્ટેમ્પવાળી નકલ સાચવો + Save as New Preset… નવો પ્રીસેટ તરીકે સાચવો… + Save as PDF… PDF તરીકે સાચવો… + Save as Template ટેમ્પલેટ તરીકે સાચવો + Save as Template… ટેમ્પલેટ તરીકે સાચવો… + Save as preset: પ્રીસેટ તરીકે સાચવો: + Save changes to {name} {name}માં ફેરફારો સાચવો + Save color group to Swatch panel રંગ ગ્રૂપ સ્વેચ પેનલમાં સાચવો + Save each artboard to a separate file દરેક આર્ટબોર્ડ અલગ ફાઇલમાં સાચવો + Save for Office Documents Office દસ્તાવેજો માટે સાચવો + Save for Office Documents… Office દસ્તાવેજો માટે સાચવો… + Save for Web વેબ માટે સાચવો + Save for Web (Legacy)… વેબ માટે સાચવો (લેગસી)… + Save for Web Presets વેબ માટે સાચવો પ્રીસેટ્સ + Save for Web Preview વેબ માટે સાચવો પૂર્વાવલોકન + Save for Web Settings વેબ માટે સાચવો સેટિંગ્સ + Save selected colors as swatches પસંદ કરેલા રંગો સ્વેચ તરીકે સાચવો + Save the applied preferences to a JSON file લાગુ પસંદગીઓ JSON ફાઇલમાં સાચવો + Save the current layout under this name વર્તમાન લેઆઉટ આ નામે સાચવો + Save the document first? પહેલા દસ્તાવેજ સાચવીએ? + Save the selected preset to a file પસંદ કરેલો પ્રીસેટ ફાઇલમાં સાચવો + Save the settings as a preset સેટિંગ્સ પ્રીસેટ તરીકે સાચવો + Save theme to My Themes થીમ "મારી થીમ્સ"માં સાચવો + Save these settings as a preset આ સેટિંગ્સ પ્રીસેટ તરીકે સાચવો + Save these settings as a print preset આ સેટિંગ્સ પ્રિન્ટ પ્રીસેટ તરીકે સાચવો + Save to Swatches સ્વેચમાં સાચવો + Saved સાચવ્યું + Saved Selection 1 સાચવેલી પસંદગી 1 + Saved Selection 10 સાચવેલી પસંદગી 10 + Saved Selection 11 સાચવેલી પસંદગી 11 + Saved Selection 12 સાચવેલી પસંદગી 12 + Saved Selection 13 સાચવેલી પસંદગી 13 + Saved Selection 14 સાચવેલી પસંદગી 14 + Saved Selection 15 સાચવેલી પસંદગી 15 + Saved Selection 16 સાચવેલી પસંદગી 16 + Saved Selection 17 સાચવેલી પસંદગી 17 + Saved Selection 18 સાચવેલી પસંદગી 18 + Saved Selection 19 સાચવેલી પસંદગી 19 + Saved Selection 2 સાચવેલી પસંદગી 2 + Saved Selection 20 સાચવેલી પસંદગી 20 + Saved Selection 21 સાચવેલી પસંદગી 21 + Saved Selection 22 સાચવેલી પસંદગી 22 + Saved Selection 23 સાચવેલી પસંદગી 23 + Saved Selection 24 સાચવેલી પસંદગી 24 + Saved Selection 25 સાચવેલી પસંદગી 25 + Saved Selection 3 સાચવેલી પસંદગી 3 + Saved Selection 4 સાચવેલી પસંદગી 4 + Saved Selection 5 સાચવેલી પસંદગી 5 + Saved Selection 6 સાચવેલી પસંદગી 6 + Saved Selection 7 સાચવેલી પસંદગી 7 + Saved Selection 8 સાચવેલી પસંદગી 8 + Saved Selection 9 સાચવેલી પસંદગી 9 + Saved Selections સાચવેલી પસંદગીઓ + Saved View 1 સાચવેલો વ્યૂ 1 + Saved View 10 સાચવેલો વ્યૂ 10 + Saved View 11 સાચવેલો વ્યૂ 11 + Saved View 12 સાચવેલો વ્યૂ 12 + Saved View 13 સાચવેલો વ્યૂ 13 + Saved View 14 સાચવેલો વ્યૂ 14 + Saved View 15 સાચવેલો વ્યૂ 15 + Saved View 16 સાચવેલો વ્યૂ 16 + Saved View 17 સાચવેલો વ્યૂ 17 + Saved View 18 સાચવેલો વ્યૂ 18 + Saved View 19 સાચવેલો વ્યૂ 19 + Saved View 2 સાચવેલો વ્યૂ 2 + Saved View 20 સાચવેલો વ્યૂ 20 + Saved View 21 સાચવેલો વ્યૂ 21 + Saved View 22 સાચવેલો વ્યૂ 22 + Saved View 23 સાચવેલો વ્યૂ 23 + Saved View 24 સાચવેલો વ્યૂ 24 + Saved View 25 સાચવેલો વ્યૂ 25 + Saved View 3 સાચવેલો વ્યૂ 3 + Saved View 4 સાચવેલો વ્યૂ 4 + Saved View 5 સાચવેલો વ્યૂ 5 + Saved View 6 સાચવેલો વ્યૂ 6 + Saved View 7 સાચવેલો વ્યૂ 7 + Saved View 8 સાચવેલો વ્યૂ 8 + Saved View 9 સાચવેલો વ્યૂ 9 + Saved Views સાચવેલા વ્યૂઝ + Saves the current bars, toolbar and panel layout. વર્તમાન બાર, ટૂલબાર અને પેનલ લેઆઉટ સાચવે છે. + Save… સાચવો… + Scale સ્કેલ + Scale Artwork with Artboard આર્ટબોર્ડ સાથે આર્ટવર્ક સ્કેલ કરો + Scale Corners ખૂણા સ્કેલ કરો + Scale Cursor Proportional to UI UI પ્રમાણે કર્સર સ્કેલ કરો + Scale Design ડિઝાઇન સ્કેલ કરો + Scale Lineweights લાઇનવેઇટ સ્કેલ કરો + Scale Presets સ્કેલ પ્રીસેટ્સ + Scale Proportionately પ્રમાણસર સ્કેલ કરો + Scale Strokes & Effects સ્ટ્રોક અને ઇફેક્ટ્સ સ્કેલ કરો + Scale Text Attributes ટેક્સ્ટ ગુણધર્મો સ્કેલ કરો + Scale Tool સ્કેલ ટૂલ + Scale at Center કેન્દ્રથી સ્કેલ કરો + Scale in Perspective પર્સ્પેક્ટિવમાં સ્કેલ કરો + Scale to Fit Artboard આર્ટબોર્ડમાં બંધબેસતું સ્કેલ કરો + Scale: સ્કેલ: + Scale… સ્કેલ… + Scaling સ્કેલિંગ + Scaling: સ્કેલિંગ: + Scallop સ્કેલપ + Scallop Tool સ્કેલપ ટૂલ + Scans: સ્કેન: + Scatter Brush સ્કેટર બ્રશ + Scatter Brush Options સ્કેટર બ્રશ વિકલ્પો + Scatter Graph Tool સ્કેટર ગ્રાફ ટૂલ + Scatter Shapes સ્કેટર આકારો + Scatter: સ્કેટર: + Scissors Tool કાતર ટૂલ + Scratch Disks સ્ક્રેચ ડિસ્ક + Screen સ્ક્રીન + Screen Mode સ્ક્રીન મોડ + Screens સ્ક્રીન્સ + Scribble… સ્ક્રિબલ… + Script / Brush સ્ક્રિપ્ટ / બ્રશ + Script Alert સ્ક્રિપ્ટ ચેતવણી + Script Console સ્ક્રિપ્ટ કન્સોલ + Scripts સ્ક્રિપ્ટ્સ + Scroll Sideways with Cmd/Ctrl + Wheel Cmd/Ctrl + વ્હીલ વડે બાજુમાં સ્ક્રોલ કરો + Scrub Numeric Fields by Dragging ખેંચીને આંકડાકીય ફીલ્ડ્સ સ્ક્રબ કરો + Search શોધો + Search All બધું શોધો + Search Commands… આદેશો શોધો… + Search Fonts Folder ફોન્ટ ફોલ્ડર શોધો + Search Preferences પસંદગીઓ શોધો + Search commands and tools આદેશો અને ટૂલ્સ શોધો + Search commands and tools… આદેશો અને ટૂલ્સ શોધો… + Search images છબીઓ શોધો + Searching… શોધી રહ્યા છીએ… + Secondary ગૌણ + Section Symbol વિભાગ ચિહ્ન + Seed: સીડ: + Segmented color wheel વિભાજિત રંગ ચક્ર + Segments સેગમેન્ટ્સ + Select પસંદ કરો + Select All બધું પસંદ કરો + Select All Artboards બધા આર્ટબોર્ડ પસંદ કરો + Select All Assets બધી એસેટ્સ પસંદ કરો + Select All Bound Objects બધી બાઉન્ડ ઑબ્જેક્ટ્સ પસંદ કરો + Select All Instances બધા ઇન્સ્ટન્સ પસંદ કરો + Select All Unused બધું ન વપરાયેલું પસંદ કરો + Select Anchors એન્કર પસંદ કરો + Select Appearance Item દેખાવ આઇટમ પસંદ કરો + Select Art આર્ટ પસંદ કરો + Select Bound Object બાઉન્ડ ઑબ્જેક્ટ પસંદ કરો + Select Data Set ડેટા સેટ પસંદ કરો + Select Embedded એમ્બેડ કરેલું પસંદ કરો + Select Fills ભરણ પસંદ કરો + Select Freeform Point ફ્રીફોર્મ પોઇન્ટ પસંદ કરો + Select Gradient Stop ગ્રેડિયન્ટ સ્ટોપ પસંદ કરો + Select Guides ગાઇડ પસંદ કરો + Select Linked લિંક કરેલું પસંદ કરો + Select Missing ગુમ થયેલું પસંદ કરો + Select Modified સુધારેલું પસંદ કરો + Select Objects ઑબ્જેક્ટ્સ પસંદ કરો + Select Same Tint % સમાન ટિન્ટ % પસંદ કરો + Select Segments સેગમેન્ટ પસંદ કરો + Select Shaper Face શેપર ફેસ પસંદ કરો + Select Slices સ્લાઇસ પસંદ કરો + Select Strokes સ્ટ્રોક પસંદ કરો + Select Symbol Instance Part સિમ્બોલ ઇન્સ્ટન્સનો ભાગ પસંદ કરો + Select Text by Font ફોન્ટ મુજબ ટેક્સ્ટ પસંદ કરો + Select Tool ટૂલ પસંદ કરો + Select Unused વપરાયેલ નથી તે પસંદ કરો + Select a character with the Type tool to see its alternates. તેના વિકલ્પી અક્ષરો જોવા માટે ટાઇપ ટૂલ વડે એક અક્ષર પસંદ કરો. + Select a parametric shape to edit it. With none selected, these set the next shape. સંપાદિત કરવા માટે પેરામેટ્રિક શેપ પસંદ કરો. કંઈ પસંદ ન હોય ત્યારે, આ આગલો શેપ સેટ કરે છે. + Select a text object ટેક્સ્ટ ઑબ્જેક્ટ પસંદ કરો + Select a text object to edit its character attributes. અક્ષર ગુણધર્મો સંપાદિત કરવા માટે ટેક્સ્ટ ઑબ્જેક્ટ પસંદ કરો. + Select a text object to edit its paragraph attributes. ફકરા ગુણધર્મો સંપાદિત કરવા માટે ટેક્સ્ટ ઑબ્જેક્ટ પસંદ કરો. + Select an image to see its Link Info. તેની લિંક માહિતી જોવા માટે ઇમેજ પસંદ કરો. + Select art and choose Object › Pattern › Make. આર્ટ પસંદ કરો અને ઑબ્જેક્ટ › પૅટર્ન › બનાવો પસંદ કરો. + Select art to make a scatter, art or pattern brush. સ્કેટર, આર્ટ અથવા પૅટર્ન બ્રશ બનાવવા માટે આર્ટ પસંદ કરો. + Select artwork to apply effects અસરો લાગુ કરવા માટે આર્ટવર્ક પસંદ કરો + Select by Artboards આર્ટબોર્ડ મુજબ પસંદ કરો + Select by Artboards… આર્ટબોર્ડ મુજબ પસંદ કરો… + Select by Color રંગ મુજબ પસંદ કરો + Select by Color Kind રંગ પ્રકાર મુજબ પસંદ કરો + Select by Link Status લિંક સ્થિતિ મુજબ પસંદ કરો + Select by Link Status… લિંક સ્થિતિ મુજબ પસંદ કરો… + Select objects to adjust their colors. તેમના રંગ ગોઠવવા માટે ઑબ્જેક્ટ પસંદ કરો. + Select objects to find the swatches nearest their colors. તેમના રંગની સૌથી નજીકના સ્વેચ શોધવા માટે ઑબ્જેક્ટ પસંદ કરો. + Select objects to see their CSS, or Generate CSS for the whole document. તેમની CSS જોવા માટે ઑબ્જેક્ટ પસંદ કરો, અથવા આખા દસ્તાવેજ માટે CSS બનાવો. + Select objects, then pick an object type to give them a 3D effect. ઑબ્જેક્ટ પસંદ કરો, પછી તેમને 3D અસર આપવા માટે ઑબ્જેક્ટ પ્રકાર પસંદ કરો. + Select styled art and click New Graphic Style. સ્ટાઇલવાળી આર્ટ પસંદ કરો અને નવી ગ્રાફિક સ્ટાઇલ ક્લિક કરો. + Select text to set its OpenType features. તેની OpenType સુવિધાઓ સેટ કરવા માટે ટેક્સ્ટ પસંદ કરો. + Select the art (and, on top of it, the mask object) આર્ટ (અને તેની ઉપર માસ્ક ઑબ્જેક્ટ) પસંદ કરો + Select two or more objects, or a group બે કે વધુ ઑબ્જેક્ટ, અથવા ગ્રૂપ પસંદ કરો + Select: પસંદ કરો: + Selected Artwork પસંદ કરેલ આર્ટવર્ક + Selected Brushes Only માત્ર પસંદ કરેલા બ્રશ + Selected Styles Only માત્ર પસંદ કરેલી સ્ટાઇલ + Selected Swatches પસંદ કરેલા સ્વેચ + Selected Swatches Only માત્ર પસંદ કરેલા સ્વેચ + Selected Symbols Only માત્ર પસંદ કરેલા સિમ્બોલ + Selected: {n}, Total number of files to export: {m} પસંદ કરેલ: {n}, નિકાસ કરવાની કુલ ફાઇલો: {m} + Selection પસંદગી + Selection & Anchor Display પસંદગી અને એન્કર ડિસ્પ્લે + Selection Only માત્ર પસંદગી + Selection Tool પસંદગી ટૂલ + Selection Tools પસંદગી ટૂલ્સ + Selection: પસંદગી: + Send Backward પાછળ મોકલો + Send to Back સૌથી પાછળ મોકલો + Send to Current Layer વર્તમાન લેયરમાં મોકલો + Sends messages to other apps અન્ય એપ્સને સંદેશા મોકલે છે + Sentence case વાક્ય કેસ + Separate colors into rows રંગોને પંક્તિઓમાં અલગ કરો + Separate several keywords with commas. અનેક કીવર્ડ અલ્પવિરામથી અલગ કરો. + Separations Preview સેપરેશન પ્રીવ્યૂ + Separations list the document's inks here: one page per ink that prints. અહીં સેપરેશન દસ્તાવેજની શાહી યાદી આપે છે: છાપવામાં આવતી દરેક શાહી માટે એક પાનું. + Serbian: Cyrillic સર્બિયન: સિરિલિક + Serbian: Latin સર્બિયન: લેટિન + Serialize Document દસ્તાવેજ ક્રમબદ્ધ કરો + Serif / Mincho સેરીફ / મિન્ચો + Services સેવાઓ + Set Alignment, Keep Position ગોઠવણી સેટ કરો, સ્થિતિ જાળવો + Set Anchor એન્કર સેટ કરો + Set Anchors એન્કર સેટ કરો + Set Bounds બાઉન્ડ્સ સેટ કરો + Set Corners કોર્નર સેટ કરો + Set Corners… કોર્નર સેટ કરો… + Set Current Layer વર્તમાન લેયર સેટ કરો + Set Font Height Reference ફોન્ટ ઊંચાઈ સંદર્ભ સેટ કરો + Set History Limit… ઇતિહાસ મર્યાદા સેટ કરો… + Set Keyboard Shortcut કીબોર્ડ શોર્ટકટ સેટ કરો + Set Mesh Point Color મેશ પોઇન્ટ રંગ સેટ કરો + Set Options વિકલ્પો સેટ કરો + Set Preference પસંદગી સેટ કરો + Set Size of Each દરેકનું કદ સેટ કરો + Set Size of Each… દરેકનું કદ સેટ કરો… + Set Slice Rectangle સ્લાઇસ લંબચોરસ સેટ કરો + Set Tab Stops ટૅબ સ્ટૉપ સેટ કરો + Set Text ટેક્સ્ટ સેટ કરો + Set Zoom ઝૂમ સેટ કરો + Set as Default ડિફૉલ્ટ તરીકે સેટ કરો + Set base color to the current color બેઝ રંગ વર્તમાન રંગ પર સેટ કરો + Set the selected color to the current color પસંદ કરેલ રંગ વર્તમાન રંગ પર સેટ કરો + Set {n} {n} સેટ કરો + Settings સેટિંગ્સ + Setup… સેટઅપ… + Shading શેડિંગ + Shading Color શેડિંગ રંગ + Shadows and Glows પડછાયા અને ગ્લો + Shape શેપ + Shape Builder શેપ બિલ્ડર + Shape Builder Delete શેપ બિલ્ડર કાઢી નાખો + Shape Builder Regions શેપ બિલ્ડર વિસ્તારો + Shape Builder Tool શેપ બિલ્ડર ટૂલ + Shape Effects શેપ અસરો + Shape Mode શેપ મોડ + Shape Mode Buttons Make Compound Shapes શેપ મોડ બટન્સ કમ્પાઉન્ડ શેપ બનાવે છે + Shape Modes: શેપ મોડ્સ: + Shape Properties શેપ ગુણધર્મો + Shape Properties: શેપ ગુણધર્મો: + Shape: શેપ: + Shaper શેપર + Shaper Tool શેપર ટૂલ + Shapes શેપ + Shared Edge Merge Tool શેર્ડ એજ મર્જ ટૂલ + Sharpen શાર્પન + Shear શિયર + Shear Tool શિયર ટૂલ + Shear… શિયર… + Shell Lower… શેલ નીચે… + Shell Upper… શેલ ઉપર… + Shift Shift + Shift to Color… રંગ તરફ શિફ્ટ કરો… + Shift: શિફ્ટ: + Shifts the tints of global and spot colors; other colors stay. ગ્લોબલ અને સ્પૉટ રંગના ટિન્ટ શિફ્ટ કરે છે; અન્ય રંગો યથાવત રહે છે. + Short ટૂંકું + Shortcut શોર્ટકટ + Shortcuts શોર્ટકટ + Show બતાવો + Show Active Plane Widget સક્રિય પ્લેન વિજેટ બતાવો + Show All બધું બતાવો + Show All Hidden Attributes બધા છુપાયેલા ગુણધર્મો બતાવો + Show All Layers બધા લેયર બતાવો + Show All Plates બધી પ્લેટ બતાવો + Show Artboards આર્ટબોર્ડ બતાવો + Show Bounding Box બાઉન્ડિંગ બૉક્સ બતાવો + Show Brush Size બ્રશ કદ બતાવો + Show Center કેન્દ્ર બતાવો + Show Center Mark કેન્દ્ર ચિહ્ન બતાવો + Show Code કોડ બતાવો + Show Corner Widget કોર્નર વિજેટ બતાવો + Show Cross Hairs ક્રોસ હેર બતાવો + Show East Asian Options પૂર્વ એશિયન વિકલ્પો બતાવો + Show Edges કિનારીઓ બતાવો + Show External Script Warning બાહ્ય સ્ક્રિપ્ટ ચેતવણી બતાવો + Show Find Field શોધ ફીલ્ડ બતાવો + Show Font Height Options ફોન્ટ ઊંચાઈ વિકલ્પો બતાવો + Show Font Names in English ફોન્ટ નામો અંગ્રેજીમાં બતાવો + Show Gradient Annotator ગ્રેડિઅન્ટ એનોટેટર બતાવો + Show Grid ગ્રિડ બતાવો + Show Grid (⌘') ગ્રિડ બતાવો (⌘') + Show Guides ગાઇડ બતાવો + Show Hidden Characters છુપાયેલા અક્ષરો બતાવો + Show Images in Outline Mode આઉટલાઇન મોડમાં ઇમેજ બતાવો + Show Indic Options ઇન્ડિક વિકલ્પો બતાવો + Show Issue સમસ્યા બતાવો + Show Layers Only માત્ર લેયર બતાવો + Show Library લાઇબ્રેરી બતાવો + Show Link Info લિંક માહિતી બતાવો + Show List View સૂચિ દૃશ્ય બતાવો + Show Live Paint Gaps લાઇવ પેઇન્ટ ગેપ બતાવો + Show Note નોંધ બતાવો + Show Options વિકલ્પો બતાવો + Show Original Path મૂળ પાથ બતાવો + Show Package પૅકેજ બતાવો + Show Panel પેનલ બતાવો + Show Perspective Grid પર્સ્પેક્ટિવ ગ્રિડ બતાવો + Show Pixel Grid (Above 600% Zoom) પિક્સેલ ગ્રિડ બતાવો (600% ઝૂમથી ઉપર) + Show Presets પ્રીસેટ બતાવો + Show Print Tiling પ્રિન્ટ ટાઇલિંગ બતાવો + Show Properties Panel ગુણધર્મો પેનલ બતાવો + Show Rulers રૂલર બતાવો + Show Rulers (⌘R) રૂલર બતાવો (⌘R) + Show Slice Numbers સ્લાઇસ નંબર બતાવો + Show Slices સ્લાઇસ બતાવો + Show Snap to Glyph Options ગ્લિફ પર સ્નેપ વિકલ્પો બતાવો + Show Swatch Bounds સ્વેચ બાઉન્ડ્સ બતાવો + Show Swatch Kinds સ્વેચ પ્રકાર બતાવો + Show Template ટેમ્પ્લેટ બતાવો + Show Text Threads ટેક્સ્ટ થ્રેડ બતાવો + Show The Home Screen When No Documents Are Open કોઈ દસ્તાવેજ ખુલ્લો ન હોય ત્યારે હોમ સ્ક્રીન બતાવો + Show Thumbnail થંબનેઇલ બતાવો + Show Thumbnail View થંબનેઇલ દૃશ્ય બતાવો + Show Thumbnails થંબનેઇલ બતાવો + Show Tile Edge ટાઇલ કિનારી બતાવો + Show Tints/Shades ટિન્ટ/શેડ બતાવો + Show Tool Group Labels ટૂલ ગ્રૂપ લેબલ બતાવો + Show Tool Tips ટૂલ ટિપ્સ બતાવો + Show Toolbar ટૂલબાર બતાવો + Show Transparency Grid પારદર્શિતા ગ્રિડ બતાવો + Show Video Rulers વિડિયો રૂલર બતાવો + Show Video Safe Areas વિડિયો સેફ એરિયા બતાવો + Show Vivid/Muted વાઇબ્રન્ટ/મ્યૂટ બતાવો + Show Warm/Cool ગરમ/ઠંડા બતાવો + Show all plates બધી પ્લેટ બતાવો + Show axis અક્ષ બતાવો + Show color bars રંગ બાર બતાવો + Show handles when multiple anchors are selected બહુવિધ એન્કર પસંદ હોય ત્યારે હૅન્ડલ બતાવો + Show in Folder ફોલ્ડરમાં બતાવો + Show its progress, see Cancel and write log lines તેની પ્રગતિ બતાવવી, રદ કરવાની વિનંતી જોવી અને લૉગ લાઇનો લખવી + Show segmented color wheel સેગમેન્ટેડ રંગ ચક્ર બતાવો + Show smooth color wheel સ્મૂધ રંગ ચક્ર બતાવો + Show/hide plate (Alt-click: only this plate) પ્લેટ બતાવો/છુપાવો (Alt-ક્લિક: માત્ર આ પ્લેટ) + Show: બતાવો: + Shown with the optimised preview. ઑપ્ટિમાઇઝ્ડ પ્રીવ્યૂ સાથે બતાવેલ. + Side 1: બાજુ 1: + Side 2: બાજુ 2: + Side Length: બાજુની લંબાઈ: + Side Tile સાઇડ ટાઇલ + Sides: બાજુઓ: + Simple સરળ + Simplify સરળ બનાવો + Simplify Anchors (Keep Shape) એન્કર સરળ બનાવો (શેપ જાળવો) + Simplify Anchors (Keep Shape)… એન્કર સરળ બનાવો (શેપ જાળવો)… + Simplify Anchors… એન્કર સરળ બનાવો… + Simplify: સરળ બનાવો: + Simplify… સરળ બનાવો… + Simulate Colored Paper રંગીન કાગળનું અનુકરણ કરો + Simulate Paper કાગળનું અનુકરણ કરો + Single Corner Radius એક કોર્નર ત્રિજ્યા + Single File એક ફાઇલ + Single Left Quotation Mark ડાબું એકલ અવતરણ ચિહ્ન + Single Quotes: એકલ અવતરણ ચિહ્નો: + Single Right Quotation Mark જમણું એકલ અવતરણ ચિહ્ન + Single Word Justification: એક શબ્દ જસ્ટિફિકેશન: + Single-line Composer સિંગલ-લાઇન કમ્પોઝર + Sixth Space છઠ્ઠી જગ્યા + Size કદ + Size Tile to Art ટાઇલનું કદ આર્ટ પ્રમાણે કરો + Size/Leading કદ/લીડિંગ + Size: કદ: + Size: {size} Tracking: {tracking} કદ: {size} ટ્રેકિંગ: {tracking} + Sketch સ્કેચ + Skew સ્કીવ + Skew: સ્કીવ: + Skin Tone Ramps સ્કિન ટોન રૅમ્પ + Skip Blank Artboards ખાલી આર્ટબોર્ડ છોડો + Skip Objects with Spot Colors સ્પૉટ રંગવાળા ઑબ્જેક્ટ છોડો + Skip Spaces જગ્યાઓ છોડો + Sky Gradients સ્કાય ગ્રેડિઅન્ટ + Slice સ્લાઇસ + Slice Options સ્લાઇસ વિકલ્પો + Slice Options… સ્લાઇસ વિકલ્પો… + Slice Selection Tool સ્લાઇસ પસંદગી ટૂલ + Slice Tool સ્લાઇસ ટૂલ + Slice Type: સ્લાઇસ પ્રકાર: + Slices સ્લાઇસ + Slices: સ્લાઇસ: + Slide Anchor Along Path પાથ સાથે એન્કર સ્લાઇડ કરો + Sliding સ્લાઇડિંગ + Slope: ઢાળ: + Small નાનું + Small Caps સ્મોલ કૅપ્સ + Small Caps: સ્મોલ કૅપ્સ: + Smaller ↔ Larger નાનું ↔ મોટું + Smart Blur… સ્માર્ટ બ્લર… + Smart Guides સ્માર્ટ ગાઇડ + Smart Guides (View → Smart Guides) સ્માર્ટ ગાઇડ (વ્યૂ → સ્માર્ટ ગાઇડ) + Smart Guides (⌘U) સ્માર્ટ ગાઇડ (⌘U) + Smart Pen સ્માર્ટ પેન + Smart Pen Panel સ્માર્ટ પેન પેનલ + Smart Pen Tool સ્માર્ટ પેન ટૂલ + Smart Punctuation… સ્માર્ટ વિરામચિહ્નો… + Smooth સ્મૂધ + Smooth Anchors… એન્કર સ્મૂધ કરો… + Smooth Tool સ્મૂધ ટૂલ + Smooth… સ્મૂધ કરો… + Smudge Stick… સ્મજ સ્ટિક… + Snap Options સ્નેપ વિકલ્પો + Snap to Glyph ગ્લિફ પર સ્નેપ + Snap to Glyph Options ગ્લિફ પર સ્નેપ વિકલ્પો + Snap to Grid ગ્રિડ પર સ્નેપ + Snap to Last Location છેલ્લા સ્થાન પર સ્નેપ + Snap to Perpendicular લંબ પર સ્નેપ + Snap to Pixel પિક્સેલ પર સ્નેપ + Snap to Point પોઇન્ટ પર સ્નેપ + Snap to Tangent સ્પર્શક પર સ્નેપ + Snapping સ્નેપિંગ + Snapping Tolerance સ્નેપિંગ સહનશીલતા + Soft સૉફ્ટ + Soft Light સૉફ્ટ લાઇટ + Soft Mix સૉફ્ટ મિક્સ + Softness નરમાઈ + Solid સૉલિડ + Some lists were cut short at their limit. કેટલીક યાદીઓ તેમની મર્યાદા પર ટૂંકાવવામાં આવી હતી. + Some scripts were not listed કેટલીક સ્ક્રિપ્ટ યાદીમાં નહોતી + Sort ક્રમ લગાવો + Sort By: આ મુજબ ક્રમ લગાવો: + Sort Objects ઑબ્જેક્ટ ક્રમ લગાવો + Sort Objects… ઑબ્જેક્ટ ક્રમ લગાવો… + Sort Swatches સ્વેચ ક્રમ લગાવો + Sort by Kind પ્રકાર મુજબ ક્રમ લગાવો + Sort by Name નામ મુજબ ક્રમ લગાવો + Source Folder: સ્રોત ફોલ્ડર: + Source code on GitHub GitHub પર સ્રોત કોડ + Space After Paragraph ફકરા પછી જગ્યા + Space Before Paragraph ફકરા પહેલાં જગ્યા + Spaces જગ્યાઓ + Spacing Guides અંતર ગાઇડ + Spacing: અંતર: + Spanish સ્પેનિશ + Sparkle સ્પાર્કલ + Spatter… સ્પૅટર… + Specify: ઉલ્લેખ કરો: + Spectrum Gradients સ્પેક્ટ્રમ ગ્રેડિઅન્ટ + Spelling જોડણી + Spelling Suggestions જોડણી સૂચનો + Spiral સ્પાઇરલ + Spiral Tool સ્પાઇરલ ટૂલ + Split Freeform Line ફ્રીફોર્મ રેખા વિભાજિત કરો + Split Into Grid ગ્રિડમાં વિભાજિત કરો + Split Into Grid… ગ્રિડમાં વિભાજિત કરો… + Split Path પાથ વિભાજિત કરો + Split Text at Cursor કર્સર પર ટેક્સ્ટ વિભાજિત કરો + Split at Anchors… એન્કર પર વિભાજિત કરો… + Split at Crossings ક્રોસિંગ પર વિભાજિત કરો + Sponge… સ્પૉન્જ… + Spot Color સ્પૉટ રંગ + Spot Colors સ્પૉટ રંગો + Spot Colors… સ્પૉટ રંગો… + Sprayed Strokes… સ્પ્રે સ્ટ્રોક… + Spread: સ્પ્રેડ: + Square ચોરસ + Squeeze… સ્ક્વિઝ… + Stacked Bar Graph Tool સ્ટૅક્ડ બાર ગ્રાફ ટૂલ + Stacked Column Graph Tool સ્ટૅક્ડ કૉલમ ગ્રાફ ટૂલ + Stained Glass… સ્ટેઇન્ડ ગ્લાસ… + Stair Step સીડી પગથિયું + Stamp… સ્ટૅમ્પ… + Standard માનક + Standard Ligatures માનક લિગેચર + Standard Vertical Roman Alignment માનક વર્ટિકલ રોમન ગોઠવણી + Standard: માનક: + Star સ્ટાર + Star Tool સ્ટાર ટૂલ + Start Global Edit ગ્લોબલ સંપાદન શરૂ કરો + Start Lists From Typed Markers ટાઇપ કરેલા માર્કરથી યાદીઓ શરૂ કરો + Start Normally સામાન્ય રીતે શરૂ કરો + Start Tile શરૂઆતની ટાઇલ + Start arrowhead શરૂઆતનું એરોહેડ + Start from VectorCraft Defaults VectorCraft ડિફૉલ્ટથી શરૂ કરો + Start in Safe Mode સેફ મોડમાં શરૂ કરો + Start: શરૂ: + Static Symbol સ્ટેટિક સિમ્બોલ + Status સ્થિતિ + Status Effects સ્ટેટસ અસરો + Step પગલું + Step Backward એક પગલું પાછળ + Step Forward એક પગલું આગળ + Step Type પગલાનો પ્રકાર + Step by Step પગલાં પ્રમાણે + Steps: પગલાં: + Stop બંધ કરો + Stop Editing Opacity Mask ઓપેસિટી માસ્ક સંપાદન બંધ કરો + Stop Global Edit ગ્લોબલ સંપાદન બંધ કરો + Stop Playing/Recording પ્લેબેક/રેકોર્ડિંગ બંધ કરો + Stop Script સ્ક્રિપ્ટ બંધ કરો + Stop editing the opacity mask ઓપેસિટી માસ્કનું સંપાદન બંધ કરો + Stop the script and undo what it did? Then quit again. સ્ક્રિપ્ટ બંધ કરીને તેણે કરેલું પૂર્વવત્ કરીએ? પછી ફરી બહાર નીકળો. + Stopped બંધ થયું + Straight સીધું + Straight Line સીધી રેખા + Straight or Curved Segment સીધો અથવા વક્ર સેગમેન્ટ + Stray Points છૂટા પોઇન્ટ + Stretch Between Guides ગાઇડ વચ્ચે ખેંચો + Stretch to Fit Stroke Length સ્ટ્રોક લંબાઈ પ્રમાણે ખેંચો + Stretch to fit બંધબેસાડવા ખેંચો + Strikethrough સ્ટ્રાઇકથ્રૂ + String not closed સ્ટ્રિંગ બંધ નથી + Stroke સ્ટ્રોક + Stroke (X), double-click for the Color Picker સ્ટ્રોક (X), કલર પિકર માટે ડબલ-ક્લિક કરો + Stroke Color સ્ટ્રોક રંગ + Stroke Options સ્ટ્રોક વિકલ્પો + Stroke Tint સ્ટ્રોક ટિન્ટ + Stroke Weight સ્ટ્રોક જાડાઈ + Stroke Weight Down સ્ટ્રોક જાડાઈ ઘટાડો + Stroke Weight Up સ્ટ્રોક જાડાઈ વધારો + Stroke When Editable સંપાદનયોગ્ય હોય ત્યારે સ્ટ્રોક + Stroke options સ્ટ્રોક વિકલ્પો + Stroke: સ્ટ્રોક: + Stroke: click for swatches, Shift-click for the color mixer સ્ટ્રોક: સ્વેચ માટે ક્લિક કરો, કલર મિક્સર માટે Shift-ક્લિક કરો + Strokes use one or more of these brushes. Take the brushes off those strokes and delete the brushes? આ સ્ટ્રોક એક કે વધુ બ્રશ વાપરે છે. તે સ્ટ્રોક પરથી બ્રશ દૂર કરીને બ્રશ કાઢી નાખીએ? + Strong મજબૂત + Style સ્ટાઇલ + Style Name: સ્ટાઇલ નામ: + Style: સ્ટાઇલ: + Styling: સ્ટાઇલિંગ: + Stylistic સ્ટાઇલિસ્ટિક + Stylistic Alternates સ્ટાઇલિસ્ટિક વિકલ્પી + Stylistic Sets સ્ટાઇલિસ્ટિક સેટ + Stylize સ્ટાઇલાઇઝ + Subdivisions ઉપવિભાગો + Subscript સબસ્ક્રિપ્ટ + Subscript: સબસ્ક્રિપ્ટ: + Subset fonts below: નીચેના ફોન્ટ સબસેટ કરો: + Subtract બાદ કરો + Suffix સફિક્સ + Sumi-e… સુમી-ઈ… + Superscript સુપરસ્ક્રિપ્ટ + Superscript: સુપરસ્ક્રિપ્ટ: + Surface સપાટી + Swap Fill and Stroke ફિલ અને સ્ટ્રોક બદલો + Swap Fill and Stroke (Shift+X) ફિલ અને સ્ટ્રોક બદલો (Shift+X) + Swap Objects ઑબ્જેક્ટ બદલો + Swap Objects and Sizes ઑબ્જેક્ટ અને કદ બદલો + Swap start and end arrowheads શરૂઆત અને અંતના એરોહેડ બદલો + Swash સ્વૅશ + Swatch Conflict સ્વેચ વિરોધાભાસ + Swatch Conflicts સ્વેચ વિરોધાભાસ + Swatch Libraries સ્વેચ લાઇબ્રેરી + Swatch Library સ્વેચ લાઇબ્રેરી + Swatch Name: સ્વેચ નામ: + Swatch Options સ્વેચ વિકલ્પો + Swatch Options… સ્વેચ વિકલ્પો… + Swatch Tiles સ્વેચ ટાઇલ + Swatch exchange files keep solid colors only. સ્વેચ એક્સચેન્જ ફાઇલોમાં માત્ર સૉલિડ રંગ રહે છે. + Swatches સ્વેચ + Swedish સ્વીડિશ + Switch Orientation દિશા બદલો + Switch Units એકમો બદલો + Switch workspace વર્કસ્પેસ બદલો + Switch x/y x/y બદલો + Symbol Instance સિમ્બોલ ઇન્સ્ટન્સ + Symbol Instance Appearance સિમ્બોલ ઇન્સ્ટન્સ દેખાવ + Symbol Libraries સિમ્બોલ લાઇબ્રેરી + Symbol Library સિમ્બોલ લાઇબ્રેરી + Symbol Options સિમ્બોલ વિકલ્પો + Symbol Options… સિમ્બોલ વિકલ્પો… + Symbol Screener સિમ્બોલ સ્ક્રીનર + Symbol Screener Tool સિમ્બોલ સ્ક્રીનર ટૂલ + Symbol Scruncher સિમ્બોલ સ્ક્રન્ચર + Symbol Scruncher Tool સિમ્બોલ સ્ક્રન્ચર ટૂલ + Symbol Set Density: સિમ્બોલ સેટ ઘનતા: + Symbol Shifter સિમ્બોલ શિફ્ટર + Symbol Shifter Tool સિમ્બોલ શિફ્ટર ટૂલ + Symbol Sizer સિમ્બોલ સાઇઝર + Symbol Sizer Tool સિમ્બોલ સાઇઝર ટૂલ + Symbol Spinner સિમ્બોલ સ્પિનર + Symbol Spinner Tool સિમ્બોલ સ્પિનર ટૂલ + Symbol Sprayer સિમ્બોલ સ્પ્રેયર + Symbol Sprayer Tool સિમ્બોલ સ્પ્રેયર ટૂલ + Symbol Stainer સિમ્બોલ સ્ટેઇનર + Symbol Stainer Tool સિમ્બોલ સ્ટેઇનર ટૂલ + Symbol Styler સિમ્બોલ સ્ટાઇલર + Symbol Styler Tool સિમ્બોલ સ્ટાઇલર ટૂલ + Symbol Type: સિમ્બોલ પ્રકાર: + Symbolism Tool સિમ્બોલિઝમ ટૂલ + Symbolism Tools Options સિમ્બોલિઝમ ટૂલ્સ વિકલ્પો + Symbols સિમ્બોલ + Syntax newer than ES3 ES3 કરતાં નવું સિન્ટેક્સ + System Layout સિસ્ટમ લેઆઉટ + System Title Bar સિસ્ટમ ટાઇટલ બાર + System Title Bar takes precedence over this option while it is on. ચાલુ હોય ત્યારે સિસ્ટમ ટાઇટલ બાર આ વિકલ્પ પર પ્રાધાન્ય ધરાવે છે. + Tab ટૅબ + Tab Stops ટૅબ સ્ટૉપ + Table ટેબલ + Tabs ટૅબ + Tamil તમિલ + Taper ટેપર + Target લક્ષ્ય + Target Contents લક્ષ્ય સામગ્રી + Target: લક્ષ્ય: + Tate-chu-yoko તેટે-ચુ-યોકો + Tate-chu-yoko Settings તેટે-ચુ-યોકો સેટિંગ્સ + Tate-chu-yoko Settings… તેટે-ચુ-યોકો સેટિંગ્સ… + Telugu તેલુગુ + Temperature તાપમાન + Temperature/Tint… તાપમાન/ટિન્ટ… + Template ટેમ્પ્લેટ + Template literal not closed ટેમ્પ્લેટ લિટરલ બંધ નથી + Templates Folder ટેમ્પ્લેટ ફોલ્ડર + Text ટેક્સ્ટ + Text Displayed in Cell: સેલમાં દેખાતો ટેક્સ્ટ: + Text Export Options ટેક્સ્ટ નિકાસ વિકલ્પો + Text Fill & Stroke Color ટેક્સ્ટ ફિલ અને સ્ટ્રોક રંગ + Text Fill Color ટેક્સ્ટ ફિલ રંગ + Text Flow ટેક્સ્ટ ફ્લો + Text Import Options ટેક્સ્ટ આયાત વિકલ્પો + Text Orientation ટેક્સ્ટ દિશા + Text Stroke Color ટેક્સ્ટ સ્ટ્રોક રંગ + Text Tweaker ટેક્સ્ટ ટ્વીકર + Text Tweaker… ટેક્સ્ટ ટ્વીકર… + Text Wrap ટેક્સ્ટ રૅપ + Text Wrap Options ટેક્સ્ટ રૅપ વિકલ્પો + Text Wrap Options… ટેક્સ્ટ રૅપ વિકલ્પો… + Text: ટેક્સ્ટ: + Texture ટેક્સ્ચર + Texturizer… ટેક્સ્ચરાઇઝર… + Thai થાઈ + The Print Tiling tool placed the pages. પ્રિન્ટ ટાઇલિંગ ટૂલે પૃષ્ઠો ગોઠવ્યાં. + The Scripts folder cannot be read સ્ક્રિપ્ટ્સ ફોલ્ડર વાંચી શકાતું નથી + The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. જ્યારે VECTORCRAFT_TITLEBAR એન્વાયર્નમેન્ટ વેરિએબલ સેટ હોય ત્યારે તે ટાઇટલ બાર પસંદ કરે છે. + The active document changed while the screen color sampler was open સ્ક્રીન રંગ સેમ્પલર ખુલ્લું હતું ત્યારે સક્રિય દસ્તાવેજ બદલાયો + The art keeps its own colors. આર્ટમાં પોતાના રંગો રહે છે. + The bounds of the visible art. દેખાતી આર્ટની સીમાઓ. + The cell shows the type object's text as HTML. સેલ ટાઇપ ઑબ્જેક્ટનો ટેક્સ્ટ HTML તરીકે બતાવે છે. + The defaults are applied when you click OK in Preferences. Cancel there keeps your current preferences. તમે સેટિંગ્સમાં ઠીક ક્લિક કરો ત્યારે ડિફોલ્ટ લાગુ થાય છે. ત્યાં રદ કરો તમારી હાલની પસંદગીઓ જાળવી રાખે છે. + The dialog was closed before its file dialog answered ફાઇલ ડાયલૉગનો જવાબ મળે તે પહેલાં ડાયલૉગ બંધ થયો + The document has no colour swatches. દસ્તાવેજમાં કોઈ રંગ સ્વેચ નથી. + The document was closed while its file dialog was open ફાઇલ ડાયલૉગ ખુલ્લું હતું ત્યારે દસ્તાવેજ બંધ થયો + The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. પ્રિન્ટર પહોંચી ન શકે તે કાગળની કિનારી: આર્ટ તેની અંદર જાય છે, અને પૂર્વાવલોકનમાં તે ડૅશ સાથે દેખાય છે. + The file is too large to read in full, so only its first part is listed. ફાઇલ પૂરી વાંચવા માટે ખૂબ મોટી છે, તેથી તેનો માત્ર પહેલો ભાગ દર્શાવ્યો છે. + The key color becomes the stroke color. The other colors shift their hue by the same amount and keep their saturation and brightness. મુખ્ય રંગ સ્ટ્રોક રંગ બને છે. બીજા રંગોનો હ્યુ એટલો જ બદલાય છે અને તેમની સંતૃપ્તિ તથા તેજ જળવાય છે. + The last launch of VectorCraft stopped while the plug-in {name} was loading. It may not be what stopped it. છેલ્લી વખત VectorCraft શરૂ થતાં પ્લગ-ઇન {name} લોડ થતું હતું ત્યારે બંધ પડ્યું. તે કારણ ન પણ હોય. + The name in the save panel was “{typed}”; VectorCraft added the file type's extension. Replacing the file overwrites its contents. સાચવો પેનલમાં નામ “{typed}” હતું; VectorCraft એ ફાઇલ પ્રકારનું એક્સ્ટેન્શન ઉમેર્યું. ફાઇલ બદલવાથી તેની સામગ્રી ઓવરરાઇટ થશે. + The page as it prints છાપતી વખતે પૃષ્ઠ જેવું દેખાય છે + The plug-in is computing… પ્લગ-ઇન ગણતરી કરી રહ્યું છે… + The plug-in writes the whole document. પ્લગ-ઇન આખો દસ્તાવેજ લખે છે. + The samples show this brush on a blue stroke. નમૂનાઓ આ બ્રશને વાદળી સ્ટ્રોક પર બતાવે છે. + The search stopped at its limit: search a smaller folder. શોધ તેની મર્યાદાએ અટકી: નાનું ફોલ્ડર શોધો. + The selected character's font is not available. પસંદ કરેલા અક્ષરનો ફૉન્ટ ઉપલબ્ધ નથી. + The selected objects have no CSS of their own (see below). પસંદ કરેલી ઑબ્જેક્ટ્સની પોતાની CSS નથી (નીચે જુઓ). + The system's settings of this printer આ પ્રિન્ટરની સિસ્ટમ સેટિંગ્સ + These fonts aren't available. Their text shows in another font. આ ફૉન્ટ ઉપલબ્ધ નથી. તેમનો ટેક્સ્ટ બીજા ફૉન્ટમાં દેખાય છે. + Thin Space પાતળી જગ્યા + Third Space ત્રીજી જગ્યા + Third Vanishing Point: ત્રીજો વેનિશિંગ પૉઇન્ટ: + This document already has a swatch named “{name}” with another color. આ દસ્તાવેજમાં “{name}” નામનો સ્વેચ બીજા રંગ સાથે પહેલેથી છે. + This file is not the plug-in installed before with this ID. It gets the settings and cache kept for that ID. આ ફાઇલ આ ID સાથે અગાઉ ઇન્સ્ટોલ થયેલું પ્લગ-ઇન નથી. તેને તે ID માટે રાખેલી સેટિંગ્સ અને કૅશ મળે છે. + This library is empty આ લાઇબ્રેરી ખાલી છે + This panel is on the roadmap (see the parity plan). આ પૅનલ રોડમૅપ પર છે (પૅરિટી યોજના જુઓ). + This script has added a great many objects. Continue, or stop it and undo what it did? આ સ્ક્રિપ્ટે ઘણી બધી ઑબ્જેક્ટ્સ ઉમેરી છે. ચાલુ રાખીએ, કે તેને બંધ કરીને તેણે જે કર્યું તે પૂર્વવત્ કરીએ? + This system has no screen color sampler આ સિસ્ટમમાં સ્ક્રીન રંગ સેમ્પલર નથી + Thread Text થ્રેડ ટેક્સ્ટ + Threaded Text થ્રેડેડ ટેક્સ્ટ + Threads થ્રેડ્સ + Three Point ત્રણ બિંદુ + Three Point Perspective ત્રણ બિંદુ પર્સ્પેક્ટિવ + Three Point Perspective Preset 1 ત્રણ બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 1 + Three Point Perspective Preset 2 ત્રણ બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 2 + Three Point Perspective Preset 3 ત્રણ બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 3 + Three Point Perspective Preset 4 ત્રણ બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 4 + Three Point Perspective Preset 5 ત્રણ બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 5 + Thumbnails: થંબનેલ: + Tile ટાઇલ + Tile Edge Color ટાઇલ કિનારીનો રંગ + Tile Edge Color… ટાઇલ કિનારીનો રંગ… + Tile Range: ટાઇલ શ્રેણી: + Tile Type: ટાઇલ પ્રકાર: + Tiles are numbered across, then down; leave the range empty for every tile. ટાઇલ આડી રીતે, પછી ઊભી રીતે ક્રમાંકિત છે; દરેક ટાઇલ માટે શ્રેણી ખાલી છોડો. + Tint ટિન્ટ + Tints ટિન્ટ્સ + Tints and Shades ટિન્ટ્સ અને શેડ્સ + Tips ટિપ્સ + Title Case ટાઇટલ કેસ + Titling Alternates ટાઇટલિંગ વૈકલ્પિક અક્ષરો + To: સુધી: + Toggle Fill/Stroke Focus ભરો/સ્ટ્રોક ફોકસ ટૉગલ કરો + Toggle Line Composer લાઇન કમ્પોઝર ટૉગલ કરો + Toggle Selection પસંદગી ટૉગલ કરો + Toggle single/double column એક/બે કૉલમ ટૉગલ કરો + Tolerance સહનશીલતા + Tolerance: સહનશીલતા: + Tone 1 ટોન 1 + Tone 2 ટોન 2 + Tone 3 ટોન 3 + Tone 4 ટોન 4 + Too large to check તપાસવા માટે ખૂબ મોટું + Tool Option ટૂલ વિકલ્પ + Tool Options… ટૂલ વિકલ્પો… + Toolbar: Add Tool ટૂલબાર: ટૂલ ઉમેરો + Toolbar: Advanced / Basic ટૂલબાર: એડવાન્સ્ડ / બેઝિક + Toolbar: Remove Tool ટૂલબાર: ટૂલ કાઢી નાખો + Toolbar: Reset ટૂલબાર: રીસેટ કરો + Toolbar: Show Controls ટૂલબાર: નિયંત્રણો બતાવો + Toolbar: Single/Double Column ટૂલબાર: એક/બે કૉલમ + Toolbars ટૂલબાર + Tools ટૂલ્સ + Tools take a single key (optionally with Shift) ટૂલ્સ એક જ કી લે છે (વૈકલ્પિક રીતે Shift સાથે) + Top ઉપર + Top Left ઉપર ડાબે + Top Left: ઉપર ડાબે: + Top Right ઉપર જમણે + Top Right: ઉપર જમણે: + Top to Bottom ઉપરથી નીચે + Top-Justified Tab ઉપર-સંરેખિત ટૅબ + Top-to-Top Leading ઉપરથી ઉપર લીડિંગ + Top: ઉપર: + Torn Edges… ફાટેલી કિનારીઓ… + Total Width: કુલ પહોળાઈ: + Total: કુલ: + Touch ટચ + Touch Type ટચ ટાઇપ + Touch Type Tool ટચ ટાઇપ ટૂલ + Touching સ્પર્શતું + Tracing ટ્રેસિંગ + Tracing the colors… રંગોનું ટ્રેસિંગ… + Tracking ટ્રેકિંગ + Trackpad Gesture to Rotate View વ્યૂ ફેરવવા માટે ટ્રૅકપૅડ જેસ્ચર + Trademark Symbol ટ્રેડમાર્ક ચિહ્ન + Transform ટ્રાન્સફોર્મ + Transform Again ફરી ટ્રાન્સફોર્મ કરો + Transform Both બંનેને ટ્રાન્સફોર્મ કરો + Transform Each દરેકને ટ્રાન્સફોર્મ કરો + Transform Each… દરેકને ટ્રાન્સફોર્મ કરો… + Transform Object Only માત્ર ઑબ્જેક્ટ ટ્રાન્સફોર્મ કરો + Transform Objects ઑબ્જેક્ટ્સ ટ્રાન્સફોર્મ કરો + Transform Panel ટ્રાન્સફોર્મ પૅનલ + Transform Pattern Only માત્ર પૅટર્ન ટ્રાન્સફોર્મ કરો + Transform Pattern Tiles પૅટર્ન ટાઇલ્સ ટ્રાન્સફોર્મ કરો + Transform Patterns પૅટર્ન ટ્રાન્સફોર્મ કરો + Transform Tools ટ્રાન્સફોર્મ ટૂલ્સ + Transform in Perspective પર્સ્પેક્ટિવમાં ટ્રાન્સફોર્મ કરો + Transform options ટ્રાન્સફોર્મ વિકલ્પો + Transform… ટ્રાન્સફોર્મ… + Transparency પારદર્શિતા + Transparency Flattener Presets પારદર્શિતા ફ્લેટનર પ્રીસેટ્સ + Transparency Flattener Presets… પારદર્શિતા ફ્લેટનર પ્રીસેટ્સ… + Transparency Grid પારદર્શિતા ગ્રિડ + Transparency Info પારદર્શિતા માહિતી + Transparency Options પારદર્શિતા વિકલ્પો + Transparency is flattened with the preset. Type is written as outlines; gradients as bands of colour. પ્રીસેટ સાથે પારદર્શિતા ફ્લેટન થાય છે. ટાઇપ આઉટલાઇન તરીકે લખાય છે; ગ્રેડિયન્ટ રંગની પટ્ટીઓ તરીકે. + Transparency is flattened with the preset. Type is written as outlines; gradients stay smooth. પ્રીસેટ સાથે પારદર્શિતા ફ્લેટન થાય છે. ટાઇપ આઉટલાઇન તરીકે લખાય છે; ગ્રેડિયન્ટ સરળ રહે છે. + Transparent પારદર્શક + Transparent Background પારદર્શક પૃષ્ઠભૂમિ + Transparent Images: પારદર્શક છબીઓ: + Transpose ટ્રાન્સપોઝ + Transverse (the page a quarter turn on the paper) ટ્રાન્સવર્સ (પૃષ્ઠ કાગળ પર ચોથા ભાગનો વળાંક) + Trap… ટ્રૅપ… + Trim ટ્રિમ + Trim View ટ્રિમ વ્યૂ + Trim covered geometry on Expand Appearance. Live preview keeps the full surface. Transparent paints and surfaces exceeding visibility limits retain all faces. Expand Appearance પર ઢંકાયેલી ભૂમિતિ ટ્રિમ કરો. લાઇવ પૂર્વાવલોકન આખી સપાટી જાળવે છે. પારદર્શક પેઇન્ટ્સ અને દૃશ્યતા મર્યાદા વટાવતી સપાટીઓ બધી ફેસ જાળવે છે. + Trim mark weight: ટ્રિમ માર્કની જાડાઈ: + True colour needs version 2004 or later: the nearest of 256 colours is written. ટ્રૂ કલર માટે સંસ્કરણ 2004 કે તેથી નવું જોઈએ: 256 રંગોમાંથી નજીકનો લખાય છે. + Turkish તુર્કી + Turn off Data Recovery for complex documents જટિલ દસ્તાવેજો માટે ડેટા રિકવરી બંધ કરો + Tweak… ટ્વીક… + Twirl ટ્વિર્લ + Twirl Rate: ટ્વિર્લ દર: + Twirl Tool ટ્વિર્લ ટૂલ + Twist ટ્વિસ્ટ + Twist… ટ્વિસ્ટ… + Two Point બે બિંદુ + Two Point Perspective બે બિંદુ પર્સ્પેક્ટિવ + Two Point Perspective Preset 1 બે બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 1 + Two Point Perspective Preset 2 બે બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 2 + Two Point Perspective Preset 3 બે બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 3 + Two Point Perspective Preset 4 બે બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 4 + Two Point Perspective Preset 5 બે બિંદુ પર્સ્પેક્ટિવ પ્રીસેટ 5 + Type ટાઇપ + Type Object Selection by Path Only માત્ર પાથ દ્વારા ટાઇપ ઑબ્જેક્ટ પસંદગી + Type Orientation ટાઇપ દિશા + Type Tool ટાઇપ ટૂલ + Type Tools ટાઇપ ટૂલ્સ + Type becomes outlines, and strokes a CAD line can't draw become filled shapes. ટાઇપ આઉટલાઇન બને છે, અને CAD લાઇન દોરી ન શકે તેવા સ્ટ્રોક ભરેલા આકાર બને છે. + Type on a Path પાથ પર ટાઇપ + Type on a Path Options પાથ પર ટાઇપ વિકલ્પો + Type on a Path Options… પાથ પર ટાઇપ વિકલ્પો… + Type on a Path Tool પાથ પર ટાઇપ ટૂલ + Type stays text, and every stroke is a line with its lineweight and dashes. ટાઇપ ટેક્સ્ટ જ રહે છે, અને દરેક સ્ટ્રોક તેની લાઇનની જાડાઈ અને ડૅશ સાથેની લાઇન છે. + Type: ટાઇપ: + Type… ટાઇપ… + Typing ટાઇપિંગ + Typography ટાઇપોગ્રાફી + UI Brightness UI તેજ + UI Frames UI ફ્રેમ્સ + UI Scaling UI સ્કેલિંગ + UPPERCASE અપરકેસ + URL: URL: + Unbind Variable વેરિએબલ અનબાઇન્ડ કરો + Underline અધોરેખા + Underpainting… અંડરપેઇન્ટિંગ… + Undo પૂર્વવત્ કરો + Undo {name} {name} પૂર્વવત્ કરો + Unembed એમ્બેડ દૂર કરો + Unembed… એમ્બેડ દૂર કરો… + Ungroup અનગ્રુપ કરો + Ungroup Color Group રંગ ગ્રુપ અનગ્રુપ કરો + Uniform યુનિફોર્મ + Uniformly Scaled સમાન રીતે સ્કેલ કરેલ + Unite યુનાઇટ + Units એકમો + Units: એકમો: + Units: {units} એકમો: {units} + Unknown directive અજાણ્યો નિર્દેશ + Unlink Opacity Mask ઓપેસિટી માસ્કનું લિંક દૂર કરો + Unlink harmony colors હાર્મની રંગોનું લિંક દૂર કરો + Unlink the mask માસ્કનું લિંક દૂર કરો + Unlock અનલૉક કરો + Unlock All બધું અનલૉક કરો + Unlock All Layers બધા લેયર અનલૉક કરો + Unlock Artboards આર્ટબોર્ડ અનલૉક કરો + Unlock Grid ગ્રિડ અનલૉક કરો + Unlock Guides ગાઇડ અનલૉક કરો + Unlock Selected પસંદ કરેલું અનલૉક કરો + Unlock Variables વેરિએબલ અનલૉક કરો + Unnamed Objects નામ વગરની ઑબ્જેક્ટ્સ + Unprintable margin: અછાપી હાંસિયો: + Unselected Artwork પસંદ ન કરેલી આર્ટવર્ક + Unsharp Mask… અનશાર્પ માસ્ક… + Unshift Unshift + Up/Down: ઉપર/નીચે: + Update Changed Links બદલાયેલી લિંક્સ અપડેટ કરો + Update Data Set ડેટા સેટ અપડેટ કરો + Update Dimensions પરિમાણ અપડેટ કરો + Update Link લિંક અપડેટ કરો + Update Links લિંક્સ અપડેટ કરો + Update Modified Links સુધારેલી લિંક્સ અપડેટ કરો + Update Selection પસંદગી અપડેટ કરો + Update While Dragging ખસેડતી વખતે અપડેટ કરો + Urdu ઉર્દૂ + Use વાપરો + Use Artboards આર્ટબોર્ડ વાપરો + Use Black Point Compensation બ્લેક પૉઇન્ટ કમ્પેન્સેશન વાપરો + Use Compression કમ્પ્રેશન વાપરો + Use Document Raster Effects Resolution દસ્તાવેજ રાસ્ટર અસર રિઝોલ્યુશન વાપરો + Use Even-Odd Fill Rule ઇવન-ઓડ ભરો નિયમ વાપરો + Use Japanese Crop Marks જાપાનીઝ ક્રોપ માર્ક્સ વાપરો + Use Library Item લાઇબ્રેરી આઇટમ વાપરો + Use Low Resolution Proxy for Linked EPS લિંક થયેલ EPS માટે ઓછા રિઝોલ્યુશનનો પ્રોક્સી વાપરો + Use Non-Zero Winding Fill Rule નૉન-ઝીરો વાઇન્ડિંગ ભરો નિયમ વાપરો + Use Outside Rectangle As Frame બહારના લંબચોરસને ફ્રેમ તરીકે વાપરો + Use Precise Cursors ચોક્કસ કર્સર વાપરો + Use Pressure Pen પ્રેશર પેન વાપરો + Use Preview Bounds પૂર્વાવલોકન સીમા વાપરો + Use Registration Point for Symbol સિમ્બોલ માટે રજિસ્ટ્રેશન બિંદુ વાપરો + Use Square for Previews પૂર્વાવલોકન માટે ચોરસ વાપરો + Use System Defaults for Edit Original ઓરિજિનલ એડિટ કરવા માટે સિસ્ટમ ડિફોલ્ટ વાપરો + Use Text for Previews પૂર્વાવલોકન માટે ટેક્સ્ટ વાપરો + Use Typographer's Quotes ટાઇપોગ્રાફર ક્વોટ્સ વાપરો + Use document bleed settings દસ્તાવેજની બ્લીડ સેટિંગ્સ વાપરો + Use the Selected Text's Font પસંદ કરેલા ટેક્સ્ટનો ફૉન્ટ વાપરો + User Brush Library 1 વપરાશકર્તા બ્રશ લાઇબ્રેરી 1 + User Brush Library 10 વપરાશકર્તા બ્રશ લાઇબ્રેરી 10 + User Brush Library 2 વપરાશકર્તા બ્રશ લાઇબ્રેરી 2 + User Brush Library 3 વપરાશકર્તા બ્રશ લાઇબ્રેરી 3 + User Brush Library 4 વપરાશકર્તા બ્રશ લાઇબ્રેરી 4 + User Brush Library 5 વપરાશકર્તા બ્રશ લાઇબ્રેરી 5 + User Brush Library 6 વપરાશકર્તા બ્રશ લાઇબ્રેરી 6 + User Brush Library 7 વપરાશકર્તા બ્રશ લાઇબ્રેરી 7 + User Brush Library 8 વપરાશકર્તા બ્રશ લાઇબ્રેરી 8 + User Brush Library 9 વપરાશકર્તા બ્રશ લાઇબ્રેરી 9 + User Defined વપરાશકર્તા વ્યાખ્યાયિત + User Defined Libraries વપરાશકર્તા વ્યાખ્યાયિત લાઇબ્રેરીઓ + User Graphic Style Library 1 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 1 + User Graphic Style Library 10 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 10 + User Graphic Style Library 2 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 2 + User Graphic Style Library 3 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 3 + User Graphic Style Library 4 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 4 + User Graphic Style Library 5 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 5 + User Graphic Style Library 6 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 6 + User Graphic Style Library 7 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 7 + User Graphic Style Library 8 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 8 + User Graphic Style Library 9 વપરાશકર્તા ગ્રાફિક સ્ટાઇલ લાઇબ્રેરી 9 + User Interface યુઝર ઇન્ટરફેસ + User Swatch Library 1 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 1 + User Swatch Library 10 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 10 + User Swatch Library 2 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 2 + User Swatch Library 3 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 3 + User Swatch Library 4 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 4 + User Swatch Library 5 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 5 + User Swatch Library 6 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 6 + User Swatch Library 7 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 7 + User Swatch Library 8 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 8 + User Swatch Library 9 વપરાશકર્તા સ્વેચ લાઇબ્રેરી 9 + User Symbol Library 1 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 1 + User Symbol Library 10 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 10 + User Symbol Library 2 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 2 + User Symbol Library 3 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 3 + User Symbol Library 4 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 4 + User Symbol Library 5 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 5 + User Symbol Library 6 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 6 + User Symbol Library 7 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 7 + User Symbol Library 8 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 8 + User Symbol Library 9 વપરાશકર્તા સિમ્બોલ લાઇબ્રેરી 9 + Username વપરાશકર્તાનામ + Uses every data set of the open document. ખુલ્લા દસ્તાવેજના દરેક ડેટા સેટનો ઉપયોગ કરે છે. + V Spacing: V અંતર: + V: V: + Variable વેરિએબલ + Variable Font વેરિએબલ ફૉન્ટ + Variable Options… વેરિએબલ વિકલ્પો… + Variable Width Profile વેરિએબલ પહોળાઈ પ્રોફાઇલ + Variables વેરિએબલ્સ + Variation: વેરિએશન: + Vector Effects વેક્ટર અસરો + Vector Halftone વેક્ટર હાફટોન + Vector Halftone… વેક્ટર હાફટોન… + Vector illustration — fast, open, scriptable. વેક્ટર ઇલસ્ટ્રેશન — ઝડપી, ખુલ્લું, સ્ક્રિપ્ટેબલ. + VectorCraft VectorCraft + VectorCraft Help… VectorCraft મદદ… + VectorCraft Title Bar on Every Desktop દરેક ડેસ્કટૉપ પર VectorCraft ટાઇટલ બાર + VectorCraft Updates VectorCraft અપડેટ્સ + VectorCraft copies the fonts to {folder}. VectorCraft ફૉન્ટ્સ {folder} માં કૉપિ કરે છે. + VectorCraft didn't quit normally last time. These documents had unsaved changes: છેલ્લી વખત VectorCraft સામાન્ય રીતે બંધ થયું નહોતું. આ દસ્તાવેજોમાં સાચવ્યા વગરના ફેરફારો હતા: + VectorCraft documents: one locked object, updated when the file changes. VectorCraft દસ્તાવેજો: એક લૉક ઑબ્જેક્ટ, ફાઇલ બદલાય ત્યારે અપડેટ થાય છે. + VectorCraft is in Safe Mode: no plug-ins load or run until it starts normally. VectorCraft સેફ મોડમાં છે: સામાન્ય રીતે શરૂ ન થાય ત્યાં સુધી કોઈ પ્લગ-ઇન લોડ કે ચાલતા નથી. + VectorCraft on GitHub GitHub પર VectorCraft + VectorCraft on getartcraft.com getartcraft.com પર VectorCraft + VectorCraft read this script without running it. VectorCraft એ સ્ક્રિપ્ટ ચલાવ્યા વગર વાંચી. + VectorCraft {release} and later (version {n}) VectorCraft {release} અને પછીનાં (સંસ્કરણ {n}) + Vectors વેક્ટર્સ + Version સંસ્કરણ + Version {version} — open-source vector illustration in pure Rust. સંસ્કરણ {version} — શુદ્ધ Rust માં ઓપન-સોર્સ વેક્ટર ઇલસ્ટ્રેશન. + Version: સંસ્કરણ: + Version: {version} સંસ્કરણ: {version} + Vertical ઊભું + Vertical Align Bottom ઊભું સંરેખણ: નીચે + Vertical Align Center ઊભું સંરેખણ: મધ્ય + Vertical Align Top ઊભું સંરેખણ: ઉપર + Vertical Area Type Tool ઊભું એરિયા ટાઇપ ટૂલ + Vertical Distribute Bottom ઊભું વિતરણ: નીચે + Vertical Distribute Center ઊભું વિતરણ: મધ્ય + Vertical Distribute Space ઊભું વિતરણ: જગ્યા + Vertical Distribute Top ઊભું વિતરણ: ઉપર + Vertical Dividers: ઊભા વિભાજકો: + Vertical Guide ઊભી ગાઇડ + Vertical Scale ઊભું સ્કેલ + Vertical Scale % ઊભું સ્કેલ % + Vertical Type Tool ઊભું ટાઇપ ટૂલ + Vertical Type on a Path Tool પાથ પર ઊભું ટાઇપ ટૂલ + Vertical: ઊભું: + Vertically Scaled ઊભી રીતે સ્કેલ કરેલ + Video વિડિયો + Video Ruler Pixel Aspect Ratio: વિડિયો રૂલર પિક્સેલ ગુણોત્તર: + View વ્યૂ + View Artboard Only માત્ર આર્ટબોર્ડ જુઓ + View Opacity Mask ઓપેસિટી માસ્ક જુઓ + View PDF after saving સાચવ્યા પછી PDF જુઓ + Viewing Angle: જોવાનો ખૂણો: + Viewing Distance: જોવાનું અંતર: + Violet વાયોલેટ + Visibility દૃશ્યતા + Volume વૉલ્યૂમ + W: W: + Warnings ચેતવણીઓ + Warp વૉર્પ + Warp Options વૉર્પ વિકલ્પો + Warp Tool વૉર્પ ટૂલ + Water Paper… વૉટર પેપર… + Watercolor… વૉટરકલર… + Wave… વેવ… + Web વેબ + Web Safe 216 વેબ સેફ 216 + Web Shift વેબ શિફ્ટ + Web Snap: વેબ સ્નૅપ: + Weight જાડાઈ + Weight: જાડાઈ: + Welcome to VectorCraft VectorCraft માં સ્વાગત છે + When pasting text ટેક્સ્ટ ચોંટાડતી વખતે + White સફેદ + Wide Blurry પહોળું ઝાંખું + Wide Sharp પહોળું તીક્ષ્ણ + Widget Position: વિજેટ સ્થાન: + Width પહોળાઈ + Width Point પહોળાઈ બિંદુ + Width Point Edit પહોળાઈ બિંદુ સંપાદન + Width Point Edit… પહોળાઈ બિંદુ સંપાદન… + Width Profile પહોળાઈ પ્રોફાઇલ + Width Profiles પહોળાઈ પ્રોફાઇલ્સ + Width Tool પહોળાઈ ટૂલ + Width and Height પહોળાઈ અને ઊંચાઈ + Width: પહોળાઈ: + Window વિન્ડો + Wireframe વાયરફ્રેમ + Word Spacing: શબ્દ અંતર: + Words Longer Than: આનાથી લાંબા શબ્દો: + Working CMYK કાર્યકારી CMYK + Workspace વર્કસ્પેસ + Wrinkle કરચલી + Wrinkle Tool કરચલી ટૂલ + Write Layers લેયર લખો + Write the document as a file in its own format દસ્તાવેજને તેના પોતાના ફૉર્મેટમાં ફાઇલ તરીકે લખો + Writes, moves or deletes files ફાઇલો લખે, ખસેડે કે કાઢી નાખે છે + X11 (XWayland) X11 (XWayland) + X11 (XWayland) When a Pen Is Attached પેન જોડાયેલ હોય ત્યારે X11 (XWayland) + XML ID XML ID + XML literal not closed XML લિટરલ બંધ નથી + Yards યાર્ડ + Yes હા + Your browser downloads the files (several as one .zip). તમારું બ્રાઉઝર ફાઇલો ડાઉનલોડ કરે છે (ઘણી એક .zip તરીકે). + Your changes will be lost if you don't save them. તમે સાચવશો નહીં તો તમારા ફેરફારો ખોવાઈ જશે. + Zig Zag… ઝિગ ઝૅગ… + Zoom ઝૂમ + Zoom In ઝૂમ ઇન + Zoom Out ઝૂમ આઉટ + Zoom Tool ઝૂમ ટૂલ + Zoom to Selection પસંદગી પર ઝૂમ કરો + Zoom with Mouse Wheel માઉસ વ્હીલથી ઝૂમ કરો + [1P-High View] [1P-ઉચ્ચ વ્યૂ] + [1P-Low View] [1P-નીચો વ્યૂ] + [1P-Normal View] [1P-સામાન્ય વ્યૂ] + [2P-High View] [2P-ઉચ્ચ વ્યૂ] + [2P-Low View] [2P-નીચો વ્યૂ] + [2P-Normal View] [2P-સામાન્ય વ્યૂ] + [3P-Low View] [3P-નીચો વ્યૂ] + [3P-Normal View] [3P-સામાન્ય વ્યૂ] + [Default] is protected: New… starts an editable copy. [Default] સુરક્ષિત છે: નવું… સંપાદનયોગ્ય નકલ શરૂ કરે છે. + [Unnamed] [નામ વગરનું] + above ઉપર + dash ડૅશ + e.g. 1-3, 5 દા.ત. 1-3, 5 + fonts not copied: {fonts} ફૉન્ટ્સ કૉપિ થયા નથી: {fonts} + from the format's public description ફૉર્મેટના જાહેર વર્ણન પરથી + from your earlier imports તમારા અગાઉના આયાતોમાંથી + gap અંતર + glyph {id} ગ્લિફ {id} + hyphens હાઇફન + letters અક્ષરો + line {line} લાઇન {line} + lines {lines} લાઇન {lines} + lowercase નાના અક્ષરો + no data set to edit સંપાદન કરવા માટે કોઈ ડેટા સેટ નથી + no data set to rename નામ બદલવા માટે કોઈ ડેટા સેટ નથી + no document કોઈ દસ્તાવેજ નથી + no variable to rename નામ બદલવા માટે કોઈ વેરિએબલ નથી + not known yet હજી જાણીતું નથી + of characters used વપરાયેલા અક્ષરોમાંથી + of {count} {count} માંથી + or more spaces with a tab અથવા વધુ જગ્યાઓ ટૅબ સાથે + per slice પ્રતિ સ્લાઇસ + slices across, evenly spaced આડી સ્લાઇસ, સરખા અંતરે + slices down, evenly spaced ઊભી સ્લાઇસ, સરખા અંતરે + spot સ્પૉટ + the preset પ્રીસેટ + tile {number} ટાઇલ {number} + to થી + x-Height x-ઊંચાઈ + {count} of {total} artboards {total} માંથી {count} આર્ટબોર્ડ + {effect} is already applied {effect} પહેલેથી લાગુ છે + {file}, line {line} {file}, લાઇન {line} + {folders} and {files} searched {folders} ફોલ્ડર અને {files} ફાઇલ શોધાયા + {format} Options {format} વિકલ્પો + {kind} Gradient {kind} ગ્રેડિયન્ટ + {kind}, {model} {kind}, {model} + {name1} {value1}, {name2} {value2} {name1} {value1}, {name2} {value2} + {name} (spot) {name} (સ્પૉટ) + {pages} in all, {count} per copy કુલ {pages}, પ્રતિ નકલ {count} + {page} of {count} {count} માંથી {page} + {people} contributors · {commits} commits {people} યોગદાનકર્તાઓ · {commits} કમિટ્સ + {percent}% opacity {percent}% ઓપેસિટી + {ready} of {total} ready {total} માંથી {ready} તૈયાર + {secs} sec {secs} સેક + {shortcut} with the tool sizes the brush {shortcut} સાથે ટૂલ બ્રશનું કદ નક્કી કરે છે + {title} (selection) {title} (પસંદગી) + {tool} Options {tool} વિકલ્પો + {tool} Tool Options {tool} ટૂલ વિકલ્પો + {warning}: click to correct to the closest color ({hex}) {warning}: નજીકના રંગ પર સુધારવા ક્લિક કરો ({hex}) + “{file}” is protected by a password. “{file}” પાસવર્ડથી સુરક્ષિત છે. + “{name}” already exists. Replace it? “{name}” પહેલેથી છે. તેને બદલીએ? +antiAlias Sharp શાર્પ + Arabic અરબી + Farsi ફારસી + Devanagari દેવનાગરી + Gurmukhi ગુરમુખી +@plural Result: {n}|Results: {n} પરિણામ: {n}|પરિણામો: {n} +@msg the document sets a bleed that this PDF/X file leaves out: turn on Use document bleed settings દસ્તાવેજ એવી બ્લીડ સેટ કરે છે જે આ PDF/X ફાઇલ છોડી દે છે: "દસ્તાવેજની બ્લીડ સેટિંગ્સ વાપરો" ચાલુ કરો +@msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} સાચવ્યું, {n} નોંધ સાથે: {first}|{path} સાચવ્યું, {n} નોંધો સાથે: {first} +@msgplural Packaged {n} file ({note}) in {folder}|Packaged {n} files ({note}) in {folder} {folder} માં {n} ફાઇલ ({note}) પેકેજ થઈ|{folder} માં {n} ફાઇલો ({note}) પેકેજ થઈ +@msgplural Packaged {n} file {note}|Packaged {n} files {note} {n} ફાઇલ પેકેજ થઈ {note}|{n} ફાઇલો પેકેજ થઈ {note} +@msgplural Packaged {n} file in {folder}|Packaged {n} files in {folder} {folder} માં {n} ફાઇલ પેકેજ થઈ|{folder} માં {n} ફાઇલો પેકેજ થઈ +@msgplural Packaged {n} file|Packaged {n} files {n} ફાઇલ પેકેજ થઈ|{n} ફાઇલો પેકેજ થઈ +@plural Packaged {n} file {note} in {folder}|Packaged {n} files {note} in {folder} {folder} માં {n} ફાઇલ {note} પેકેજ થઈ|{folder} માં {n} ફાઇલો {note} પેકેજ થઈ +@plural Packaged {n} file {note}|Packaged {n} files {note} {n} ફાઇલ પેકેજ થઈ {note}|{n} ફાઇલો પેકેજ થઈ {note} +@plural Packaged {n} file in {folder}|Packaged {n} files in {folder} {folder} માં {n} ફાઇલ પેકેજ થઈ|{folder} માં {n} ફાઇલો પેકેજ થઈ +@plural Packaged {n} file|Packaged {n} files {n} ફાઇલ પેકેજ થઈ|{n} ફાઇલો પેકેજ થઈ +@msg (fonts not copied: {fonts}) (ફૉન્ટ્સ કૉપિ થયા નથી: {fonts}) +@msgplural ({n} linked file not found)|({n} linked files not found) ({n} લિંક કરેલી ફાઇલ મળી નથી)|({n} લિંક કરેલી ફાઇલો મળી નથી) +@msgplural {n} linked file not found|{n} linked files not found {n} લિંક કરેલી ફાઇલ મળી નથી|{n} લિંક કરેલી ફાઇલો મળી નથી +@plural {n} linked file not found|{n} linked files not found {n} લિંક કરેલી ફાઇલ મળી નથી|{n} લિંક કરેલી ફાઇલો મળી નથી +@msg variables that aren't text were left out: {kinds} ટેક્સ્ટ ન હોય તેવા વેરિએબલ છોડી દેવાયા: {kinds} +@msg a new document won't read these variables back with their kind: {names} નવો દસ્તાવેજ આ વેરિએબલ્સને તેમના પ્રકાર સાથે પાછા વાંચશે નહીં: {names} +@msg a new document refuses this file because of these variables: {names} આ વેરિએબલ્સને કારણે નવો દસ્તાવેજ આ ફાઇલ સ્વીકારશે નહીં: {names} +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them `{name}` માટેનું હેડર `{cell}` વેરિએબલ `{other}` તરીકે વંચાશે: બેમાંથી એકનું નામ બદલો +@msg the header `{header}` for `{name}` would read back as the variable `{other}`: rename one of them `{name}` માટેનું હેડર `{header}` વેરિએબલ `{other}` તરીકે વંચાશે: બેમાંથી એકનું નામ બદલો +@msg ({note}) ({more}) ({note}) ({more}) +@msg {note}) ({more} {note} ({more}) +@msg invalid parameters for `{cmd}`: {msg} `{cmd}` માટે અમાન્ય પેરામીટર: {msg} +@msg fonts not copied: {fonts} ફૉન્ટ્સ કૉપિ થયા નથી: {fonts} + Energy Beams ઊર્જા કિરણો + Laser Bolt લેસર બોલ્ટ + Plasma Arc પ્લાઝ્મા આર્ક + Charge Wave ચાર્જ તરંગ + Spark Trail તણખાનો પથ + Skill Icons કૌશલ્ય આઇકન + Attack હુમલો + Defend બચાવ + Heal ઉપચાર + Buff બફ + Ult અલ્ટ + Write pages longer than 200 in at full size in points 200 in થી લાંબા પેજ પૂરા કદમાં, પોઇન્ટ્સમાં લખો + Dynamic Text ડાયનેમિક ટેક્સ્ટ +@msg type on a path in a thread cannot use Dynamic Text: {frames} થ્રેડમાં પાથ પરનું ટાઇપ ડાયનેમિક ટેક્સ્ટ વાપરી શકતું નથી: {frames} +@msg id {value} is a ruler guide, not an object id {value} રૂલર ગાઇડ છે, ઑબ્જેક્ટ નથી +@msg a ruler guide goes only directly in a layer or sublayer રૂલર ગાઇડ ફક્ત સીધા લેયર અથવા સબલેયરમાં જ જાય છે + time unavailable સમય ઉપલબ્ધ નથી + {ms} ms {ms} ms + {time}, {calls} {time}, {calls} + slowest: {commands} સૌથી ધીમા: {commands} + {dropped}, {cut} {dropped}, {cut} +@plural {n} host call|{n} host calls {n} હોસ્ટ કૉલ|{n} હોસ્ટ કૉલ +@plural {n} earlier line dropped|{n} earlier lines dropped {n} અગાઉની લાઇન છોડી|{n} અગાઉની લાઇનો છોડી +@plural {n} line cut|{n} lines cut {n} લાઇન કાપી|{n} લાઇનો કાપી +# Interface labels added since the original native review; contributor proofreading completed 2026-11-14. + Convert to Indexed Color ઇન્ડેક્સ્ડ કલરમાં ફેરવો + Convert to Indexed Color… ઇન્ડેક્સ્ડ કલરમાં ફેરવો… + Image Tools ઇમેજ ટૂલ્સ + Remove Background… પૃષ્ઠભૂમિ દૂર કરો… + Remove Background પૃષ્ઠભૂમિ દૂર કરો + Background પૃષ્ઠભૂમિ + Edge Color કિનારીનો રંગ + Picked Colors પસંદ કરેલા રંગો + Corner Color ખૂણાનો રંગ + Contiguous સળંગ + Each Channel દરેક ચેનલ + Color Mask… કલર માસ્ક… + Color Mask કલર માસ્ક + Remove Color Mask કલર માસ્ક દૂર કરો + Apply Color Mask કલર માસ્ક લાગુ કરો + Delete the selected items and their artwork? પસંદ કરેલી આઇટમ્સ અને તેમની આર્ટવર્ક કાઢી નાખવી છે? + Delete this layer and its artwork? આ લેયર અને તેની આર્ટવર્ક કાઢી નાખવી છે? + Preserve જાળવો + TIFF (Black & White) TIFF (કાળું અને સફેદ) + TIFF (8-bit Color) TIFF (8-bit રંગ) + High Resolution ઉચ્ચ રિઝોલ્યુશન + Medium Resolution મધ્યમ રિઝોલ્યુશન + Low Resolution નીચું રિઝોલ્યુશન + Language Level 2 લેંગ્વેજ લેવલ 2 + Language Level 3 લેંગ્વેજ લેવલ 3 + Select one character with the Type tool first. પહેલા ટાઇપ ટૂલ વડે એક અક્ષર પસંદ કરો. + {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} એ {font}નો ગ્લિફ છે, પસંદ કરેલા લખાણના ફોન્ટ ({text_font})નો નથી. + {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} એ {base} (U+{base_code})નો {place} ગ્લિફ છે; તમારી પસંદગી {sel} (U+{code}) છે. + at the start of a word શબ્દની શરૂઆતમાં + in the middle of a word શબ્દની વચ્ચે + at the end of a word શબ્દના અંતે + standing alone સ્વતંત્ર + {glyph} is not an alternate of {sel} (U+{code}) as it stands in its word. શબ્દમાં તેના સ્થાન મુજબ {glyph} એ {sel} (U+{code})નો વૈકલ્પિક ગ્લિફ નથી. + Double-click to insert {base} (U+{code}) instead: a presentation form does not join, the letter does. તેના બદલે {base} (U+{code}) દાખલ કરવા ડબલ-ક્લિક કરો: પ્રેઝન્ટેશન ફોર્મ જોડાતું નથી, અક્ષર જોડાય છે. + Inserted {base} (U+{code}) instead of a presentation form: the font joins it to its neighbors by position. પ્રેઝન્ટેશન ફોર્મના બદલે {base} (U+{code}) દાખલ કર્યો: ફોન્ટ તેને સ્થાન પ્રમાણે આસપાસના અક્ષરો સાથે જોડે છે. + No OpenType feature draws this glyph here, so it replaces the character's glyph and may not join its neighbors. અહીં કોઈ OpenType ફીચર આ ગ્લિફ દોરતું નથી, તેથી તે અક્ષરના ગ્લિફને બદલે છે અને કદાચ આસપાસના અક્ષરો સાથે જોડાશે નહીં. + Show samples on white સફેદ પૃષ્ઠભૂમિ પર નમૂનાઓ બતાવો + Switch Selection and Drawing Modes પસંદગી અને ડ્રોઇંગ મોડ્સ બદલો + Revolve (Classic)… રિવોલ્વ (ક્લાસિક)… + Rotate (Classic)… રોટેટ (ક્લાસિક)… + Painted and Sketched રંગેલું અને સ્કેચ કરેલું + Washed Blue ધોવાયેલો વાદળી + Bristle Ochre બ્રિસલ ગેરુ + Pastel Heather પેસ્ટલ હેધર + Charcoal Shade ચારકોલ છાંયો + Ink Contour શાહીનો કોન્ટૂર + Wet Ink ભીની શાહી + Fine Pen બારીક પેન + Detach અલગ કરો + Detach Object from Path ઑબ્જેક્ટને પાથથી અલગ કરો + Include Variable Data વેરિએબલ ડેટા સામેલ કરો + Pad પેડ + Repeats રિપીટ્સ + YakumonoHankaku YakumonoHankaku + GyomatsuYakumonoHankaku GyomatsuYakumonoHankaku + GyomatsuYakumonoZenkaku GyomatsuYakumonoZenkaku + YakumonoZenkaku YakumonoZenkaku + Blend Easing બ્લેન્ડ ઇઝિંગ + missing from the Brushes panel બ્રશ પેનલમાં નથી + Show DCS Transparency Interactions DCS ટ્રાન્સપરન્સીની પરસ્પર અસરો બતાવો + Transparent, or overlaps transparent objects: check its separations પારદર્શક છે અથવા પારદર્શક ઑબ્જેક્ટ્સ પર આવે છે: તેના કલર સેપરેશન્સ તપાસો + Dynamic Text Presets ડાયનેમિક ટેક્સ્ટ પ્રીસેટ્સ + Arch કમાન + Organic Fades ઑર્ગેનિક ફેડ્સ + Holographic હોલોગ્રાફિક + Preview: પ્રીવ્યૂ: + Insert Aki Left ડાબે અકી ઉમેરો + Insert Aki Right જમણે અકી ઉમેરો + Conic Gradient કોનિક ગ્રેડિયન્ટ + Diamond Gradient ડાયમંડ ગ્રેડિયન્ટ + Include Placed Images પ્લેસ કરેલી ઇમેજો સામેલ કરો + Include Fill Hatches ફિલ હેચિસ સામેલ કરો + Use Monitor Profile for Canvas કેનવાસ માટે મોનિટર પ્રોફાઇલ વાપરો + The canvas is drawn in sRGB. કેનવાસ sRGBમાં દોરાય છે. + Canvas display profile: કેનવાસ ડિસ્પ્લે પ્રોફાઇલ: + The system matches the window to the display's colors. સિસ્ટમ વિન્ડોના રંગો ડિસ્પ્લેના રંગો સાથે મેળવે છે. + No usable display profile was found: the canvas is drawn in sRGB. ઉપયોગી ડિસ્પ્લે પ્રોફાઇલ મળી નથી: કેનવાસ sRGBમાં દોરાય છે. + Create Spine સ્પાઇન બનાવો + Create Spine: સ્પાઇન બનાવો: + Always હંમેશાં + Never ક્યારેય નહીં + Arrowheads એરોહેડ્સ + Edit Arrowheads એરોહેડ્સ સંપાદિત કરો + Edit Arrowheads… એરોહેડ્સ સંપાદિત કરો… + Reload Arrowheads એરોહેડ્સ ફરી લોડ કરો + Color Theme Picker કલર થીમ પિકર + Click or drag across the art to pick its colors; Shift adds more આર્ટના રંગો પસંદ કરવા તેના પર ક્લિક કરો અથવા ડ્રેગ કરો; Shiftથી વધુ ઉમેરાય છે + Prominent Colors મુખ્ય રંગો + Save All Colors બધા રંગો સાચવો + Save Prominent Colors મુખ્ય રંગો સાચવો + Korean Line Break Anywhere કોરિયન લાઇન ગમે ત્યાં તોડો + Bunri-Kinshi બુનરી-કિન્શી + East Asian Composition Settings પૂર્વ એશિયાઈ કમ્પોઝિશન સેટિંગ્સ + Korean Composition Settings કોરિયન કમ્પોઝિશન સેટિંગ્સ + Show Bounding Box While Moving and Drawing ખસેડતી અને દોરતી વખતે બાઉન્ડિંગ બોક્સ બતાવો + Textures ટેક્સ્ચર્સ + Fine Grain બારીક દાણા + Canvas Weave કેનવાસ વણાટ + Burlap Sack શણનો કોથળો + Cracked Glaze તિરાડવાળું ગ્લેઝ + Mosaic Floor મોઝેક ફ્લોર + Quilted Squares ગાદીદાર ચોરસ + Leaded Glass લેડેડ ગ્લાસ + Damp Paper ભીનો કાગળ + Show Recent Colors તાજેતરના રંગો બતાવો + Toggle RGB Percent RGB ટકાવારી ચાલુ/બંધ કરો + Spot Color Objects સ્પોટ કલર ઑબ્જેક્ટ્સ + Pattern Objects પેટર્ન ઑબ્જેક્ટ્સ + Gradient Objects ગ્રેડિયન્ટ ઑબ્જેક્ટ્સ + Font Details ફોન્ટની વિગતો + Linked Images લિંક કરેલી ઇમેજો + Embedded Images એમ્બેડ કરેલી ઇમેજો + Compound Paths કમ્પાઉન્ડ પાથ્સ + Text Objects ટેક્સ્ટ ઑબ્જેક્ટ્સ + Placed Documents પ્લેસ કરેલા દસ્તાવેજો + Symbol Instances સિમ્બોલ ઇન્સ્ટન્સિસ + Gradient Meshes ગ્રેડિયન્ટ મેશિસ + Blends બ્લેન્ડ્સ + Envelopes એન્વલોપ્સ + Intertwine Groups ઇન્ટરટ્વાઇન ગ્રુપ્સ + Opacity Masks ઓપેસિટી માસ્ક્સ + Objects with Effects ઇફેક્ટ્સવાળા ઑબ્જેક્ટ્સ + Pattern Swatches પેટર્ન સ્વેચિસ + Tate-chu-yoko Up/Down ટાટે-ચુ-યોકો ઉપર/નીચે + Tate-chu-yoko Left/Right ટાટે-ચુ-યોકો ડાબે/જમણે + Add Tool… ટૂલ ઉમેરો… + Remove Tool ટૂલ દૂર કરો + Center on Top ઉપર કેન્દ્રિત કરો + Center on Bottom નીચે કેન્દ્રિત કરો + Working RGB (don't tag) વર્કિંગ RGB (ટૅગ ન કરો) + Dock to Left ડાબે ડૉક કરો + Dock to Right જમણે ડૉક કરો + Dock Toolbar ટૂલબાર ડૉક કરો + Float Toolbar ટૂલબાર ફ્લોટ કરો + Control Bar Options કન્ટ્રોલ બાર વિકલ્પો + Tool Options ટૂલ વિકલ્પો + Object Options ઑબ્જેક્ટ વિકલ્પો + Image Options ઇમેજ વિકલ્પો + Vertical Alignment ઊભું એલાઇનમેન્ટ + Align and Distribute સંરેખિત કરો અને વહેંચો + Document Options દસ્તાવેજ વિકલ્પો + Rename Library Item લાઇબ્રેરી આઇટમનું નામ બદલો + Add to Character Styles કેરેક્ટર સ્ટાઇલ્સમાં ઉમેરો + Shadows પડછાયા + Distance from Object ઑબ્જેક્ટથી અંતર + Shadow Bounds પડછાયાની સીમા + Behind Object ઑબ્જેક્ટની પાછળ + Below Object ઑબ્જેક્ટની નીચે + Optical Effects ઑપ્ટિકલ ઇફેક્ટ્સ + Grit Textures ખરબચડા ટેક્સ્ચર્સ + Hue Clockwise હ્યૂ ઘડિયાળની દિશામાં + Hue Counterclockwise હ્યૂ ઘડિયાળની વિરુદ્ધ દિશામાં + Automation asked to install this plug-in. To install it, choose Object › Plug-ins › Install Plug-in… yourself. ઑટોમેશને આ પ્લગ-ઇન ઇન્સ્ટૉલ કરવા કહ્યું છે. ઇન્સ્ટૉલ કરવા માટે જાતે Object › Plug-ins › Install Plug-in… પસંદ કરો. + Move Action Step ઍક્શનનું પગલું ખસેડો + Insert Menu Item… મેનુ આઇટમ ઉમેરો… + Insert Menu Item મેનુ આઇટમ ઉમેરો + Insert Select Path પાથ પસંદ કરવાનું પગલું ઉમેરો + Select Path પાથ પસંદ કરો + Select Object… ઑબ્જેક્ટ પસંદ કરો… + Select Object ઑબ્જેક્ટ પસંદ કરો + Set Selection પસંદગી સેટ કરો + Case Sensitive મોટા-નાના અક્ષરનો ભેદ રાખો + Whole Word આખો શબ્દ + Select Object: {note} ઑબ્જેક્ટ પસંદ કરો: {note} + Select by Note નોંધ દ્વારા પસંદ કરો + Menu Item: મેનુ આઇટમ: + No menu item matches. કોઈ મેનુ આઇટમ મેળ ખાતી નથી. + Several menu items match. Choose one from the list. ઘણી મેનુ આઇટમ્સ મેળ ખાય છે. યાદીમાંથી એક પસંદ કરો. + Items that open a dialog, and File, Window and Help menu items, are not listed. ડાયલોગ ખોલતી આઇટમ્સ અને File, Window તથા Help મેનુની આઇટમ્સ યાદીમાં નથી. + Ink and Wash શાહી અને વૉશ + Tapered Ink ટેપર થયેલી શાહી + Brush Pen બ્રશ પેન + Soft Wash સોફ્ટ વૉશ + Pooling Wash એકઠો થતો વૉશ + Bristle Brushes બ્રિસલ બ્રશ + Fine Liner ફાઇન લાઇનર + Pointed Flat અણીદાર ફ્લેટ + Stubby Round ટૂંકો ગોળ + Dry Scrub સૂકું સ્ક્રબ + Soft Dome સોફ્ટ ડોમ + Filbert ફિલ્બર્ટ + Angled Round ખૂણાવાળો ગોળ + Angled Flat ખૂણાવાળો સપાટ + Fan Blender ફેન બ્લેન્ડર + Grass Fan ગ્રાસ ફેન + Dry Media સૂકાં માધ્યમો + Chalk Grain ચૉકના દાણા + Graphite Pencil ગ્રેફાઇટ પેન્સિલ + Wax Crayon વેક્સ ક્રેયોન + Charcoal Dust ચારકોલનો ભૂકો + Notched Ribbon ખાંચાવાળી રિબન + Double Line બેવડી રેખા + Wave તરંગ + Teardrop આંસુનો આકાર + Striped Ribbon પટ્ટાવાળી રિબન + Fine Nib બારીક નિબ + Broad Nib પહોળી નિબ + Chisel છીણી + Round Pen ગોળ પેન + Pressure Pen પ્રેશર પેન + Beads મણકા + Dash Dot ડૅશ ડૉટ + Scallops કાંગરાવાળી કિનારી + Stars તારા + Triangles ત્રિકોણ + Sparkles ચમકારા + Pixel Snapping Options પિક્સેલ સ્નેપિંગ વિકલ્પો + Pixel Snapping Options… પિક્સેલ સ્નેપિંગ વિકલ્પો… + Snap to Pixel While Drawing દોરતી વખતે પિક્સેલ પર સ્નેપ કરો + Snap to Pixel While Moving ખસેડતી વખતે પિક્સેલ પર સ્નેપ કરો + Snap to Pixel While Scaling સ્કેલ કરતી વખતે પિક્સેલ પર સ્નેપ કરો + These options take effect when View › Snap to Pixel is on. View › Snap to Pixel ચાલુ હોય ત્યારે આ વિકલ્પો લાગુ થાય છે. + Basic Paragraph Formats પેરાગ્રાફના મૂળભૂત ફૉર્મેટ્સ + Composition કમ્પોઝિશન + Place Brush Art બ્રશ આર્ટ પ્લેસ કરો + Replace Brush Art બ્રશ આર્ટ બદલો + Strokes use this brush. Apply the new art to those strokes, or leave them as they are and add a copy of the brush with the new art? સ્ટ્રોક્સ આ બ્રશ વાપરે છે. તે સ્ટ્રોક્સ પર નવી આર્ટ લાગુ કરવી છે કે તેમને જેમ છે તેમ રાખીને નવી આર્ટવાળી બ્રશની નકલ ઉમેરવી છે? + Apply to Strokes સ્ટ્રોક્સ પર લાગુ કરો + Leave Strokes સ્ટ્રોક્સ જેમ છે તેમ રાખો + Copy Troubleshooting Info સમસ્યા નિવારણની માહિતી કૉપી કરો + Align New Objects to Pixel Grid નવા ઑબ્જેક્ટ્સને પિક્સેલ ગ્રિડ સાથે સંરેખિત કરો + Not Aligned to Pixel Grid પિક્સેલ ગ્રિડ સાથે સંરેખિત નથી + Learn શીખો + Explore tutorials ટ્યુટોરિયલ્સ શોધો + For you તમારા માટે + Skills કૌશલ્યો + Get started શરૂ કરો + Quick skill guide કૌશલ્યની ઝડપી માર્ગદર્શિકા + Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. મુખ્ય ટૂલ્સ માટે કૌશલ્ય પ્રમાણે ગોઠવેલા ટૂંકા પ્રાયોગિક ટ્યુટોરિયલ્સ. તેના ટ્યુટોરિયલ્સ જોવા કૌશલ્ય પસંદ કરો. + List Tutorials ટ્યુટોરિયલ્સની યાદી બતાવો + Show Tutorial ટ્યુટોરિયલ બતાવો + Show Tool Location ટૂલનું સ્થાન બતાવો + Show where this tool is આ ટૂલ ક્યાં છે તે બતાવો + View more વધુ જુઓ + Finish tutorial ટ્યુટોરિયલ પૂર્ણ કરો + Next Up હવે પછી + You finished the last tutorial in this skill. તમે આ કૌશલ્યનો છેલ્લો ટ્યુટોરિયલ પૂર્ણ કર્યો છે. + Back to skills કૌશલ્યો પર પાછા જાઓ + Open Tutorial Sample ટ્યુટોરિયલનો નમૂનો ખોલો + Finish Tutorial ટ્યુટોરિયલ પૂર્ણ કરો + Next Tutorial આગળનો ટ્યુટોરિયલ + Back to Skills કૌશલ્યો પર પાછા જાઓ + Cutting Tools કટિંગ ટૂલ્સ + Stencil Bridges… સ્ટેન્સિલ બ્રિજિસ… + Stencil Bridges સ્ટેન્સિલ બ્રિજિસ + Shortest Crossing સૌથી ટૂંકો ક્રોસિંગ + Direction દિશા + Weed Box and Lines… વીડ બોક્સ અને રેખાઓ… + Weed Box and Lines વીડ બોક્સ અને રેખાઓ + Margin માર્જિન + Box બોક્સ + Swatch સ્વેચ +# Current-main coverage additions; contributor proofreading completed 2026-11-14. + built in બિલ્ટ-ઇન + embedding not allowed એમ્બેડ કરવાની મંજૂરી નથી + embedding for editing સંપાદન માટે એમ્બેડ કરવાની મંજૂરી + embedding for preview and print પ્રીવ્યૂ અને પ્રિન્ટ માટે એમ્બેડ કરવાની મંજૂરી + embedding allowed એમ્બેડ કરવાની મંજૂરી છે + {file}; {embedding} {file}; {embedding} + substituted: shown in {family} {style} ({detail}) બદલાયેલ: {family} {style}માં બતાવ્યું ({detail}) + missing: shown in {family} {style} ગુમ: {family} {style}માં બતાવ્યું + missing ગુમ + {width} × {height} px {width} × {height} px + {size} — {file} {size} — {file} + {n} ppi {n} ppi + {kind}, {instances} {kind}, {instances} + {kind}, {rows} × {cols} {kind}, {rows} × {cols} + {grid}, {instances} {grid}, {instances} + Linear gradient લીનિયર ગ્રેડિયન્ટ + Radial gradient રેડિયલ ગ્રેડિયન્ટ + Conic gradient કોનિક ગ્રેડિયન્ટ + Diamond gradient ડાયમંડ ગ્રેડિયન્ટ + Freeform gradient ફ્રીફોર્મ ગ્રેડિયન્ટ +@plural {n} instance|{n} instances {n} ઇન્સ્ટન્સ|{n} ઇન્સ્ટન્સિસ +@plural {n} row|{n} rows {n} પંક્તિ|{n} પંક્તિઓ +@plural {n} column|{n} columns {n} કૉલમ|{n} કૉલમ + Warichu વારિચુ + Warichu Settings… વારિચુ સેટિંગ્સ… + Warichu Settings વારિચુ સેટિંગ્સ + Lines: રેખાઓ: + Line Gap: રેખાઓ વચ્ચેનું અંતર: + Trim at Crossings Tool છેદબિંદુઓ પર કાપવાનું ટૂલ + Trim at Crossings છેદબિંદુઓ પર કાપો + Banner Hems and Grommets… બેનરની વાળેલી કિનારીઓ અને ગ્રોમેટ્સ… + Banner Hems and Grommets બેનરની વાળેલી કિનારીઓ અને ગ્રોમેટ્સ + Hem વાળેલી કિનારી + Grommets ગ્રોમેટ્સ + Grommet Inset ગ્રોમેટનું અંદરની બાજુનું અંતર + Diameter વ્યાસ + Add as Single Graphic એક ગ્રાફિક તરીકે ઉમેરો + Bow ધનુષાકાર + Expand as Wireframes વાયરફ્રેમ્સ તરીકે વિસ્તારો + Add art with Add as Single Graphic, or drop art here. એક ગ્રાફિક તરીકે ઉમેરો વડે આર્ટ ઉમેરો, અથવા આર્ટ અહીં મૂકો. + Align On: આના પર સંરેખિત કરો: + Delete Tab ટૅબ કાઢી નાખો + Face સપાટી + Graphics map onto the Plane, Extrude and Revolve object types. ગ્રાફિક્સને Plane, Extrude અને Revolve પ્રકારના ઑબ્જેક્ટ્સ પર મેપ કરી શકાય છે. + Justify with last line aligned center છેલ્લી લાઇન મધ્યમાં રાખીને જસ્ટિફાઇ કરો + Justify with last line aligned right છેલ્લી લાઇન જમણે રાખીને જસ્ટિફાઇ કરો + Leader: લીડર: + Line Gap રેખાઓ વચ્ચેનું અંતર + Manage Profiles પ્રોફાઇલ્સનું સંચાલન કરો + Mapped graphics મેપ કરેલા ગ્રાફિક્સ + Materials મટિરિયલ્સ + NEW DOCUMENT PROFILES નવા દસ્તાવેજની પ્રોફાઇલ્સ + Opacity Mask ઓપેસિટી માસ્ક + Pick a graphic above to map it onto the object. ઑબ્જેક્ટ પર મેપ કરવા ઉપરથી ગ્રાફિક પસંદ કરો. + Remove Graphic ગ્રાફિક દૂર કરો + Replace the selected 3D objects with the edges of their faces, as stroked paths. પસંદ કરેલા 3D ઑબ્જેક્ટ્સને તેમની સપાટીઓની કિનારીઓના સ્ટ્રોકવાળા પાથ્સથી બદલો. + Select a 3D object to map the graphic onto it. ગ્રાફિક મેપ કરવા માટે 3D ઑબ્જેક્ટ પસંદ કરો. + The selected objects have different graphics. પસંદ કરેલા ઑબ્જેક્ટ્સમાં જુદાં ગ્રાફિક્સ છે. + Word Gap શબ્દો વચ્ચેનું અંતર + X: X: + Y: Y: + @{login}: {prs}, {commits}, +{added} / −{deleted} lines (Δ {delta}), +{binary_added} / −{binary_deleted} binary assets, {first} – {last} @{login}: {prs}, {commits}, +{added} / −{deleted} લાઇન્સ (Δ {delta}), +{binary_added} / −{binary_deleted} બાઇનરી ઍસેટ્સ, {first} – {last} + PRs PRs + Preset {number} પ્રીસેટ {number} +@plural {n} PR|{n} PRs {n} PR|{n} PRs +@plural {n} commit|{n} commits {n} કમિટ|{n} કમિટ્સ +anchor Points: પોઇન્ટ્સ: +diagnostics render રેન્ડર +diagnostics ui ઇન્ટરફેસ +file-filter All placeable files પ્લેસ કરી શકાય તેવી બધી ફાઇલો +file-filter All readable files વાંચી શકાય તેવી બધી ફાઇલો +file-filter Export presets એક્સપોર્ટ પ્રીસેટ્સ +file-filter Flattener presets ફ્લેટનર પ્રીસેટ્સ +file-filter PDF presets PDF પ્રીસેટ્સ +file-filter PDF-compatible .ai PDF-સુસંગત .ai +file-filter Print presets પ્રિન્ટ પ્રીસેટ્સ +file-filter SVG Compressed સંકુચિત SVG +file-filter Swatch libraries સ્વેચ લાઇબ્રેરીઓ +file-filter Templates ટેમ્પ્લેટ્સ +file-filter VectorCraft Template VectorCraft ટેમ્પ્લેટ +gradient-kind Radial રેડિયલ +lighting Light લાઇટ +mark-style Japanese જાપાનીઝ +swatch Amber એમ્બર +swatch Aqua એક્વા +swatch Azure આસમાની +swatch Black કાળો +swatch Blue વાદળી +swatch Bright Blue તેજસ્વી વાદળી +swatch Bright Green તેજસ્વી લીલો +swatch Bright Red તેજસ્વી લાલ +swatch Bright Violet તેજસ્વી વાયોલેટ +swatch Bright Yellow તેજસ્વી પીળો +swatch Brights તેજસ્વી +swatch Brown ભૂરો +swatch Chocolate ચૉકલેટ +swatch Coffee કોફી +swatch Copper કોપર +swatch Cyan સાયન +swatch Cyan Green સાયન લીલો +swatch Emerald એમરાલ્ડ +swatch Espresso એસ્પ્રેસો +swatch Grays ગ્રે +swatch Green લીલો +swatch Indigo ઈન્ડિગો +swatch Lime લાઇમ +swatch Magenta મેજેન્ટા +swatch Ocean સમુદ્રી +swatch Orange નારંગી +swatch Orange Red નારંગી લાલ +swatch Pink ગુલાબી +swatch Purple જાંબલી +swatch Radial White, Black રેડિયલ સફેદ, કાળો +swatch Red લાલ +swatch Rose રોઝ +swatch Rust રસ્ટ +swatch Sand રેતિયો +swatch Sunset સૂર્યાસ્ત +swatch Tan આછો ભૂરો +swatch Teal ટીલ +swatch Violet વાયોલેટ +swatch Walnut અખરોટી +swatch White સફેદ +swatch White, Black સફેદ, કાળો +swatch Yellow પીળો +swatch Yellow Green પીળો લીલો +swatch [None] [કંઈ નહીં] +swatch [Registration] [રજિસ્ટ્રેશન] + Burst Ray વિસ્ફોટક કિરણ + Dash Trail ડૅશની લડી + Speed Line ગતિ રેખા + Zigzag ઝિગઝૅગ + Repeat Objects પુનરાવર્તિત ઑબ્જેક્ટ્સ +@msg `{path}` must be one of {_1}, not {got} `{path}` એ {_1}માંથી એક હોવું જોઈએ, {got} નહીં + Path Length પાથની લંબાઈ diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 3de589c7a6..0e93cf2f87 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -1140,6 +1140,8 @@ antiAlias Sharp शार्प Crop Image इमेज क्रॉप करें Image Tools इमेज टूल Remove Background… पृष्ठभूमि हटाएँ… + Draw (Pencil) ड्रा (पेंसिल) + Place Image इमेज प्लेस करें Remove Background पृष्ठभूमि हटाएँ Background पृष्ठभूमि Edge Color किनारे का रंग @@ -5943,6 +5945,7 @@ antiAlias Sharp शार्प @msg `{key}` must be a number `{key}` एक संख्या होना चाहिए @msg `{key}` must be a number {_1}..{_2} `{key}` {_1}..{_2} की एक संख्या होना चाहिए @msg `{key}` must be a number not below 0 `{key}` 0 से कम नहीं होने वाली संख्या होना चाहिए +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 0 से कम नहीं होने वाली चार संख्याएँ होनी चाहिए: [tl, tr, br, bl] @msg `{key}` must be a number, not {v} `{key}` एक संख्या होना चाहिए, {v} नहीं @msg `{key}` must be a profile name or null `{key}` को प्रोफ़ाइल नाम या null होना चाहिए @msg `{key}` must be a string `{key}` एक स्ट्रिंग होना चाहिए diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index bf001a37b1..fe2c9497a3 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -906,6 +906,8 @@ font-technology Variable Változó Crop Image Kép körülvágása Image Tools Képeszközök Remove Background… Háttér eltávolítása… + Draw (Pencil) Rajzolás (Ceruza) + Place Image Kép elhelyezése Remove Background Háttér eltávolítása Background Háttér Edge Color Szélszín @@ -4915,6 +4917,7 @@ imageTrace Outline Körvonal @msg `select` must be true or false `select`: true vagy false kell @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` és `maxAnchors`: egész szám kell @msg `{key}` must be a number not below 0 `{key}`: legalább 0 értékű szám kell +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}`: négy legalább 0 értékű szám kell: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`: [x, y, width, height] kell, négy véges szám, a szélesség és a magasság nem negatív @msg `{min_key}` is greater than `{max_key}` `{min_key}` nagyobb, mint `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` adj meg legalább egyet ezek közül: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index beaf28da91..208877f673 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -194,6 +194,8 @@ Crop Image Pangkas gambar Image Tools Alat Gambar Remove Background… Hapus latar belakang… + Draw (Pencil) Menggambar (pensil) + Place Image Tempatkan gambar Remove Background Hapus latar belakang Background Latar belakang Edge Color Warna tepi diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index d2ab7d4510..9951a32c1b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -705,6 +705,8 @@ font-technology Variable Variabili Crop Image Ritaglia immagine Image Tools Strumenti immagine Remove Background… Rimuovi sfondo… + Draw (Pencil) Disegna (Matita) + Place Image Inserisci immagine Remove Background Rimuovi sfondo Background Sfondo Edge Color Colore del bordo @@ -4542,6 +4544,7 @@ axis Both Entrambi @msg `select` must be true or false `select` deve essere true o false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devono essere numeri interi @msg `{key}` must be a number not below 0 `{key}` deve essere un numero non inferiore a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` deve essere quattro numeri non inferiori a 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve essere [x, y, width, height]: quattro numeri finiti, con larghezza e altezza non negative @msg `{min_key}` is greater than `{max_key}` `{min_key}` è maggiore di `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica almeno uno tra `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index d753e7b031..c5b1444879 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -243,6 +243,8 @@ digits Malayalam マラヤーラム数字 Crop Image 画像の切り抜き Image Tools 画像ツール Remove Background… 背景を削除… + Draw (Pencil) 描画 (鉛筆) + Place Image 画像を配置 Remove Background 背景を削除 Background 背景 Edge Color 端の色 @@ -5012,6 +5014,7 @@ swatch Bright Violet ビビッドバイオレット @msg `select` must be true or false `select` には true または false を指定してください @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` と `maxAnchors` は整数にしてください @msg `{key}` must be a number not below 0 `{key}` には 0 以上の数値を指定してください +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` には 0 以上の数値を 4 つ指定してください: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` は [x, y, width, height] の形式で指定してください: 有限の数値 4 つで、幅と高さは負の値にできません @msg `{min_key}` is greater than `{max_key}` `{min_key}` が `{max_key}` より大きくなっています @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 次のうち少なくとも 1 つを指定してください: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 96afacb8bc..008f496ac9 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -166,6 +166,8 @@ Crop Image გამოსახულების ჩამოჭრა Image Tools გამოსახულების ხელსაწყოები Remove Background… ფონის მოშორება… + Draw (Pencil) ხატვა (ფანქარი) + Place Image გამოსახულების განთავსება Remove Background ფონის მოშორება Background ფონი Edge Color კიდის ფერი @@ -4187,6 +4189,7 @@ font-technology Variable ცვლადი @msg `select` must be true or false `select` უნდა იყოს true ან false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` და `maxAnchors` უნდა იყოს მთელი რიცხვები @msg `{key}` must be a number not below 0 `{key}` უნდა იყოს რიცხვი, არანაკლებ 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` უნდა იყოს ოთხი რიცხვი, თითოეული არანაკლებ 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` უნდა იყოს [x, y, width, height]: ოთხი სასრული რიცხვი, სიგანე და სიმაღლე არ უნდა იყოს უარყოფითი @msg `{min_key}` is greater than `{max_key}` `{min_key}` მეტია, ვიდრე `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` მიუთითეთ ერთი მაინც: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 73667b09ac..899dfc2f6e 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -236,6 +236,8 @@ digits Malayalam 말라얄람어 Crop Image 이미지 자르기 Image Tools 이미지 도구 Remove Background… 배경 제거… + Draw (Pencil) 그리기(연필) + Place Image 이미지 가져오기 Remove Background 배경 제거 Background 배경 Edge Color 가장자리 색상 @@ -4939,6 +4941,7 @@ theme Appearance 외관 @msg `select` must be true or false `select`은(는) true 또는 false여야 합니다 @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors`와 `maxAnchors`는 정수여야 합니다 @msg `{key}` must be a number not below 0 `{key}`은(는) 0 이상의 숫자여야 합니다 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}`은(는) 0 이상의 숫자 네 개여야 합니다: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`은(는) [x, y, width, height]여야 합니다: 유한한 숫자 네 개이며 너비와 높이는 음수가 아니어야 합니다 @msg `{min_key}` is greater than `{max_key}` `{min_key}`이(가) `{max_key}`보다 큽니다 @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 다음 중 하나 이상을 지정하세요: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -7087,3 +7090,1817 @@ breakText Glyphs 글리프 Grommets 아일렛 Grommet Inset 가장자리에서 아일렛까지 거리 Diameter 지름 +# Status and error messages of commands, scripts, file import and export, and plug-ins. +@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions 엄격 모드 함수에서는 'caller', 'callee', 'arguments'를 사용할 수 없습니다 +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. 프로파일 하나에는 회전체 효과를 하나만 쓸 수 있습니다. 기존 효과의 옵션을 편집하세요. +@msg Add Points at Crossings: a path went missing 교차점에 점 추가: 패스가 사라졌습니다 +@msg Alignment must be one of the choices 정렬은 선택 항목 중 하나여야 합니다 +@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean이 아닌 값에서 Boolean.prototype.{name}을(를) 호출했습니다 +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB 샘플을 CMYK로 읽을 수 없는 CMYK 이미지는 RGB로 가져왔습니다 +@msg CSS options: {e} CSS 옵션: {e} +@msg CSV has a header but no data rows CSV에 머리글은 있지만 데이터 행이 없습니다 +@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark CSV에 NUL 바이트가 있습니다: UTF-8로 저장하거나 바이트 순서 표시가 있는 UTF-16으로 저장하세요 +@msg CSV is empty CSV가 비어 있습니다 +@msg CSV is larger than {MAX_CSV_BYTES} bytes CSV가 {MAX_CSV_BYTES}바이트보다 큽니다 +@msg CSV is not UTF-16 CSV가 UTF-16이 아닙니다 +@msg CSV is not UTF-8 CSV가 UTF-8이 아닙니다 +@msg CSV would be larger than {MAX_CSV_BYTES} bytes CSV가 {MAX_CSV_BYTES}바이트보다 커집니다 +@msg Cannot write to the location of “{name}”; choose where to save it “{name}”의 위치에 쓸 수 없습니다. 저장할 위치를 선택하세요 +@msg Choose original artwork, not a dimension's generated paths 치수가 만든 패스가 아닌 원본 아트웍을 선택하세요 +@msg Contour: select paths or shapes 윤곽: 패스나 모양을 선택하세요 +@msg Contour: the Offset Path effect is not available 윤곽: 패스 간격 띄우기 효과를 사용할 수 없습니다 +@msg Create needs fills, strokes or both 만들려면 칠이나 선, 또는 둘 다 필요합니다 +@msg DCS files can only be placed linked: use File › Place with Link on DCS 파일은 연결된 상태로만 가져올 수 있습니다: 링크를 켜고 파일 › 가져오기를 사용하세요 +@msg DXF options: {e} DXF 옵션: {e} +@msg DXF scale must be a positive number up to {_1}, not {scale} DXF 비율은 {_1} 이하의 양수여야 합니다: {scale} +@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}`: {allowed}이어야 합니다 +@msg Date.prototype method called on a value that is not a Date object Date 개체가 아닌 값에서 Date.prototype 메서드를 호출했습니다 +@msg Dimension is locked or hidden 치수가 잠겨 있거나 숨겨져 있습니다 +@msg Dimension mode does not match its source 치수 모드가 원본과 맞지 않습니다 +@msg Draw Inside needs exactly one selected path, compound path or text 안쪽에 그리기에는 선택한 패스, 컴파운드 패스 또는 텍스트가 정확히 하나 필요합니다 +@msg EPS Save writes all artwork: use document.export for selected-only EPS EPS 저장은 모든 아트웍을 기록합니다: 선택 항목만 EPS로 기록하려면 document.export를 사용하세요 +@msg EPS flattener: {e} EPS 병합: {e} +@msg EPS options: {e} EPS 옵션: {e} +@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}`: {allowed}이어야 합니다 +@msg Error.prototype.toString was called on a value that is not an object 개체가 아닌 값에서 Error.prototype.toString을 호출했습니다 +@msg Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft 화면용 내보내기는 png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai 또는 vectorcraft로 기록합니다 +@msg Flare click coordinates must be finite numbers 플레어 클릭 좌표는 유한한 숫자여야 합니다 +@msg Function.prototype.toString was called on a value that is not a function 함수가 아닌 값에서 Function.prototype.toString을 호출했습니다 +@msg HTML Text applies to object slices of type only HTML 텍스트는 문자의 개체 슬라이스에만 적용됩니다 +@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) 개체 식별 기준이 XML ID입니다: 이름은 올바른 XML ID여야 합니다(글자, 밑줄 또는 콜론으로 시작하고 공백이 없어야 함) +@msg Image Trace failed: internal error: its thread stopped (please report this bug) 이미지 추적 실패: 내부 오류: 스레드가 멈췄습니다(이 버그를 보고해 주세요) +@msg Image Trace failed: {e} 이미지 추적 실패: {e} +@msg Image Trace finished, but its document was closed 이미지 추적이 끝났지만 문서가 닫혔습니다 +@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again 이미지 추적이 끝났지만 그 사이에 이미지나 설정이 바뀌었습니다: 다시 추적하세요 +@msg Import SVG Filter reads SVG files, not SVGZ SVG 필터 가져오기는 SVGZ가 아닌 SVG 파일을 읽습니다 +@msg Invalid dimension offset 잘못된 치수 오프셋 +@msg Invalid point 잘못된 점 +@msg JSON.parse: {why} (at offset {at}) JSON.parse: {why}(오프셋 {at}) +@msg JSON.stringify cannot write a structure that contains itself JSON.stringify는 자신을 포함하는 구조를 기록할 수 없습니다 +@msg Join: a subpath went missing 연결: 하위 패스가 사라졌습니다 +@msg Line Gap must be a number 행 간격은 숫자여야 합니다 +@msg Lock Workspace is not available in VectorCraft VectorCraft에서는 작업 영역 잠그기를 사용할 수 없습니다 +@msg Move Panel to New Window is not available: floating panels stay in the main window 패널을 새 창으로 이동은 사용할 수 없습니다: 떠 있는 패널은 기본 창 안에 있습니다 +@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString에는 Namespace가 필요합니다 +@msg New Document profiles need the preferences folder, which this run doesn't have 새 문서 프로파일에는 환경 설정 폴더가 필요한데, 이번 실행에는 그 폴더가 없습니다 +@msg Number.prototype.{name} was called on a value that is not a number 숫자가 아닌 값에서 Number.prototype.{name}을(를) 호출했습니다 +@msg Object.create's prototype must be an object or null Object.create의 프로토타입은 개체 또는 null이어야 합니다 +@msg Object.{function} was given a value that is not an object Object.{function}에 개체가 아닌 값을 전달했습니다 +@msg Offset Path: the path is too complex to offset 패스 간격 띄우기: 패스가 너무 복잡해서 간격을 띄울 수 없습니다 +@msg Offset Path: {e} 패스 간격 띄우기: {e} +@msg PDF content is incomplete or exceeds the reader's limits; no document was imported PDF 내용이 불완전하거나 읽기 한도를 넘습니다. 문서를 가져오지 않았습니다 +@msg PDF flattener: {e} PDF 병합: {e} +@msg PDF options: {e} PDF 옵션: {e} +@msg QName.prototype.toString needs a QName QName.prototype.toString에는 QName이 필요합니다 +@msg Recolor Artwork is no longer open 아트웍 색상 재지정이 더 이상 열려 있지 않습니다 +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp 개체가 아닌 값에서 RegExp.prototype.{name}을(를) 호출했습니다 +@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 SVG가 UTF-8, UTF-16 또는 ISO 8859-1이 아닙니다 +@msg Save writes one artboard: name one, or export several with document.export 저장은 아트보드 하나를 기록합니다: 하나를 지정하거나 document.export로 여러 개를 내보내세요 +@msg Select a dimension first 먼저 치수를 선택하세요 +@msg Select dimensions to update 업데이트할 치수를 선택하세요 +@msg Select only dimensions 치수만 선택하세요 +@msg Select two or more objects, or a group 개체를 두 개 이상 선택하거나 그룹을 선택하세요 +@msg Shaper needs 1–64 source paths 셰이퍼에는 원본 패스가 1–64개 필요합니다 +@msg Stencil Bridges: the selected art is too large or too detailed to bridge 스텐실 브리지: 선택한 아트웍이 너무 크거나 세밀해서 브리지를 만들 수 없습니다 +@msg String.prototype.toString was called on a value that is not a string 문자열이 아닌 값에서 String.prototype.toString을 호출했습니다 +@msg Switch x/y is for scatter graphs x/y 전환은 분산형 그래프에 사용합니다 +@msg Text options: {e} 텍스트 옵션: {e} +@msg The {name} profile could not be read, so the new document has the default settings: {reason} {name} 프로파일을 읽을 수 없어 새 문서에 기본 설정을 사용했습니다: {reason} +@msg Trap: more than {_1} pairs of objects lie close enough to trap, or too many objects share a horizontal range; trap fewer objects at a time 트랩: 트랩할 만큼 가까운 개체 쌍이 {_1}개보다 많거나, 같은 가로 범위에 있는 개체가 너무 많습니다. 한 번에 더 적은 개체를 트랩하세요 +@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off 위딩 상자 및 선: 아트웍 바깥에 들어갈 위딩 선이 없고 위딩 상자가 꺼져 있습니다 +@msg \ at end of pattern 패턴 끝에 \이(가) 있습니다 +@msg `alignOn` cannot be empty `alignOn`은(는) 비워 둘 수 없습니다 +@msg `alignOn` must be a string `alignOn`은(는) 문자열이어야 합니다 +@msg `align` is start, center or end `align`은(는) start, center 또는 end입니다 +@msg `align` must be a string `align`은(는) 문자열이어야 합니다 +@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 `answer` for this {kind} dialog must be one of: {_1} 이 {kind} 대화 상자의 `answer`은(는) 다음 중 하나여야 합니다: {_1} +@msg `answers` has more than {MAX_ANSWERS} answers `answers`에 응답이 {MAX_ANSWERS}개보다 많습니다 +@msg `answers` is larger than 1 MiB `answers`이(가) 1 MiB보다 큽니다 +@msg `answers` must be a list of answers `answers`은(는) 응답 목록이어야 합니다 +@msg `artboard` must be the index of an artboard of the document `artboard`은(는) 문서에 있는 아트보드의 인덱스여야 합니다 +@msg `artboardsAt` must be [x, y] `artboardsAt`은(는) [x, y]여야 합니다 +@msg `artboards` must be a list of artboard indices `artboards`은(는) 아트보드 인덱스 목록이어야 합니다 +@msg `artboards` names an artboard the document does not have `artboards`에 문서에 없는 아트보드가 있습니다 +@msg `artboards` names an artboard twice `artboards`에 같은 아트보드가 두 번 있습니다 +@msg `asset` is the asset id (see assets.list) `asset`은(는) 에셋 id입니다(assets.list 참조) +@msg `at` must be [x, y] (got {at}) `at`은(는) [x, y]여야 합니다({at} 입력됨) +@msg `at` must be a byte offset `at`은(는) 바이트 오프셋이어야 합니다 +@msg `at` must be a point [x, y] `at`은(는) 점 [x, y]여야 합니다 +@msg `autoLeading` must be a number `autoLeading`은(는) 숫자여야 합니다 +@msg `background` must be "", "matte" or #rrggbb `background`은(는) "", "matte" 또는 #rrggbb여야 합니다 +@msg `beside` needs `side` `beside`에는 `side`가 필요합니다 +@msg `burasagari` must be "none", "standard" or "forced" `burasagari`은(는) "none", "standard" 또는 "forced"여야 합니다 +@msg `center` must be [x, y] `center`은(는) [x, y]여야 합니다 +@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign`은(는) "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" 또는 "icfBottom"이어야 합니다 +@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection`은(는) "default", "leftToRight" 또는 "rightToLeft"여야 합니다 +@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke`에는 `stroke: false`와 `ids: []`가 필요하며 `stop`은 쓸 수 없습니다 +@msg `clear` must be a list of type object ids, not {v} `clear`은(는) 문자 개체 id 목록이어야 합니다: {v} +@msg `clear` must list type objects; {_1} is not one `clear`에는 문자 개체를 나열해야 합니다. {_1}은(는) 문자 개체가 아닙니다 +@msg `clear` must not list `id` `clear`에 `id`를 넣을 수 없습니다 +@msg `code` must be a whole number `code`은(는) 정수여야 합니다 +@msg `colorMode` must be one of rgb, cmyk `colorMode`은(는) rgb, cmyk 중 하나여야 합니다 +@msg `color` needs a `rule` to make the theme from `color`에는 테마를 만들 `rule`이 필요합니다 +@msg `colors` does not apply to the {name} reduction `colors`은(는) {name} 감소에는 적용되지 않습니다 +@msg `colors` is empty `colors`이(가) 비어 있습니다 +@msg `colors` must be a whole number from 2 to 256 `colors`은(는) 2에서 256 사이의 정수여야 합니다 +@msg `colors` must be at least 9 for the uniform reduction uniform 감소에서는 `colors`이(가) 9 이상이어야 합니다 +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors`은(는) 1에서 {MAX_THEME_COLORS} 사이여야 합니다({n} 입력됨) +@msg `columnType` is vertical, uniform, repeating or sliding `columnType`은(는) vertical, uniform, repeating 또는 sliding입니다 +@msg `columns` must be a whole number 1..50 `columns`은(는) 1..50의 정수여야 합니다 +@msg `commands` must not be empty `commands`은(는) 비어 있으면 안 됩니다 +@msg `contents` goes with `to` `contents`은(는) `to`와 함께 씁니다 +@msg `corners` must be sharp, round or square, not `{other}` `corners`은(는) sharp, round 또는 square여야 합니다: `{other}` +@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners`에는 앵커 인덱스(0, 1, 2…)를 나열해야 합니다: {v} +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count`은(는) 0에서 {MAX_COUNT} 사이의 정수여야 합니다 +@msg `count` must be a whole number from 1 to 100 `count`은(는) 1에서 100 사이의 정수여야 합니다 +@msg `dataBase64` is larger than the module limit `dataBase64`이(가) 모듈 한도보다 큽니다 +@msg `dataBase64` is not valid base64 `dataBase64`이(가) 올바른 base64가 아닙니다 +@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly`은(는) `ids`, `reverse` 또는 `append`와 함께 쓸 수 없습니다 +@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth`와 `childLimit`은 요약을 잘라 냅니다: `summary: true`를 전달하세요 +@msg `design` is a design name, or null for plain columns `design`은(는) 디자인 이름이며, 일반 막대에는 null입니다 +@msg `design` is a design name, or null for the default marker `design`은(는) 디자인 이름이며, 기본 마커에는 null입니다 +@msg `destinationFolder` is required when destination is `folder` destination이 `folder`이면 `destinationFolder`가 필요합니다 +@msg `destination` must be `none`, `saveAndClose` or `folder` `destination`은(는) `none`, `saveAndClose` 또는 `folder`여야 합니다 +@msg `diameter` must be from 0.1 to 2500 pt `diameter`은(는) 0.1에서 2500 pt 사이여야 합니다 +@msg `digits` must be one of {_1} `digits`은(는) {_1} 중 하나여야 합니다 +@msg `dimImages` is a percentage or false `dimImages`은(는) 백분율 또는 false입니다 +@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction`은(는) "auto", "leftToRight" 또는 "rightToLeft"여야 합니다 +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount`은(는) 0에서 100 사이의 숫자여야 합니다 +@msg `each` is the value one design stands for, more than 0 `each`은(는) 디자인 하나가 나타내는 값이며 0보다 커야 합니다 +@msg `end` must be "start" or "end" `end`은(는) "start" 또는 "end"여야 합니다 +@msg `ends` applies to joining exactly two paths `ends`은(는) 정확히 두 패스를 연결할 때 적용됩니다 +@msg `event` must be one of {_1} `event`은(는) {_1} 중 하나여야 합니다 +@msg `every` must be "all", "odd" or "even", not {v} `every`은(는) "all", "odd" 또는 "even"이어야 합니다: {v} +@msg `expand` must be true or false `expand`은(는) true 또는 false여야 합니다 +@msg `fidelity` must be from {lo} to {hi} pt `fidelity`은(는) {lo}에서 {hi} pt 사이여야 합니다 +@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files`은(는) 마지막 text.findFontFiles 검색에서 찾은 글꼴 파일의 경로 1개에서 1000개여야 합니다 +@msg `focal` and `radius` cannot be used together `focal`과 `radius`는 함께 쓸 수 없습니다 +@msg `focal` must be an array of [subpath, anchor] pairs `focal`은(는) [subpath, anchor] 쌍의 배열이어야 합니다 +@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal`의 [{si}, {ai}]은(는) 패스의 앵커가 아닙니다 +@msg `folder` must be an absolute folder path `folder`은(는) 폴더의 절대 경로여야 합니다 +@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes`은(는) {"wght"}처럼 축 태그와 값으로 이루어진 개체여야 합니다 +@msg `format` must be a string `format`은(는) 문자열이어야 합니다 +@msg `format` must be one of {_1} `format`은(는) {_1} 중 하나여야 합니다 +@msg `fractions` is chop or scale `fractions`은(는) chop 또는 scale입니다 +@msg `from` and `to` must differ `from`과 `to`는 서로 달라야 합니다 +@msg `from` has an empty frame `from`에 빈 프레임이 있습니다 +@msg `from` must be a non-negative integer `from`은(는) 음이 아닌 정수여야 합니다 +@msg `from` must be area type or type on a path `from`은(는) 영역 문자 또는 패스 위 문자여야 합니다 +@msg `gid` must be a glyph id, not {v} `gid`은(는) 글리프 id여야 합니다: {v} +@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate`은(는) 문자 스타일이 아니라 글자 하나에 속합니다(text.setGlyphAlternate로 설정) +@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate`에는 글리프 선택을 없애는 null만 쓸 수 있습니다(선택은 text.setGlyphAlternate로 지정) +@msg `group` must be one of {_1} `group`은(는) {_1} 중 하나여야 합니다 +@msg `group` must be the Objects on Path group of `member` and `target` `group`은(는) `member`와 `target`이 속한 패스 위 개체 그룹이어야 합니다 +@msg `guide` must be a guide index (guide.list) `guide`은(는) 안내선 인덱스여야 합니다(guide.list) +@msg `hAlign` must be default, left, center or right `hAlign`은(는) default, left, center 또는 right여야 합니다 +@msg `id` is an object id, not {v} `id`은(는) 개체 id입니다: {v} +@msg `id` must be a node id `id`은(는) 노드 id여야 합니다 +@msg `id` must be an object id, not {v} `id`은(는) 개체 id여야 합니다: {v} +@msg `ids` must name Objects on Path groups or objects in them `ids`에는 패스 위 개체 그룹이나 그 안의 개체를 지정해야 합니다 +@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids`에는 id를 최대 {MAX_INSPECT_IDS}개까지 넣을 수 있습니다 +@msg `imageMap` must be none, rectangle or polygon `imageMap`은(는) none, rectangle 또는 polygon이어야 합니다 +@msg `includeArtboardFill` must be true or false `includeArtboardFill`은(는) true 또는 false여야 합니다 +@msg `includeSubdirs` must be true or false `includeSubdirs`은(는) true 또는 false여야 합니다 +@msg `index` must be a point index `index`은(는) 점 인덱스여야 합니다 +@msg `index` must be a whole number or null `index`은(는) 정수 또는 null이어야 합니다 +@msg `indices` must be a non-empty array of item indices `indices`은(는) 항목 인덱스가 하나 이상 있는 배열이어야 합니다 +@msg `item` needs `index` `item`에는 `index`가 필요합니다 +@msg `items` is a list of {kind, item} `items`은(는) {kind, item}의 목록입니다 +@msg `items` is empty `items`이(가) 비어 있습니다 +@msg `items` must be a list of {id, points} `items`은(는) {id, points}의 목록이어야 합니다 +@msg `items` must be a list of {{id, anchors?}}, not {v} `items`은(는) {{id, anchors?}}의 목록이어야 합니다: {v} +@msg `items` must be an array of {id, anchors} `items`은(는) {id, anchors}의 배열이어야 합니다 +@msg `items` must be an array of {{id, corners?}}, not {v} `items`은(는) {{id, corners?}}의 배열이어야 합니다: {v} +@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida`은(는) "none", "short", "medium", "long" 또는 "stylistic"이어야 합니다 +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore`은(는) 문자 스타일이 아니라 줄의 첫 글자에 속합니다(text.setRangeStyle로 설정) +@msg `kind` is one of {_1} `kind`은(는) {_1} 중 하나입니다 +@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind`은(는) "round", "invertedRound" 또는 "chamfer"여야 합니다: {v} +@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind`은(는) `text`, `visibility`, `image` 또는 `graph`여야 합니다 +@msg `kind` must be a string `kind`은(는) 문자열이어야 합니다 +@msg `kind` must be image, noImage or htmlText `kind`은(는) image, noImage 또는 htmlText여야 합니다 +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType`은(는) "pushInFirst", "pushOutFirst" 또는 "pushOutOnly"여야 합니다 +@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku`은(는) "none", "hard" 또는 "soft"여야 합니다 +@msg `label` must be a non-empty name of at most 200 characters `label`은(는) 비어 있지 않은 최대 200자의 이름이어야 합니다 +@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language`은(는) "Turkish"나 "tr" 같은 언어 이름 또는 태그여야 합니다 +@msg `leader` must be a string `leader`은(는) 문자열이어야 합니다 +@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel`은(는) "romanBaseline" 또는 "emBoxTop"이어야 합니다 +@msg `leading` must be from 0.1 to 5000, or null for Auto `leading`은(는) 0.1에서 5000 사이이거나, 자동이면 null이어야 합니다 +@msg `length` must be a number of at most 1000000 pt either way `length`은(는) 양쪽 방향으로 최대 1000000 pt의 숫자여야 합니다 +@msg `line` must be true or false `line`은(는) true 또는 false여야 합니다 +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max`은(는) 1에서 {MAX_SUGGESTIONS} 사이여야 합니다 +@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle`({min_angle}°)은 `maxAngle`({max_angle}°)보다 클 수 없습니다 +@msg `mode` must be "handles", "straight" or "smooth" `mode`은(는) "handles", "straight" 또는 "smooth"여야 합니다 +@msg `mode` must be "straight", "bezier" or "arc" `mode`은(는) "straight", "bezier" 또는 "arc"여야 합니다 +@msg `mode` must be one of {_1} `mode`은(는) {_1} 중 하나여야 합니다 +@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi`은(는) "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" 또는 "lineEndHalf"여야 합니다 +@msg `name` must be a name that is not empty `name`은(는) 비어 있지 않은 이름이어야 합니다 +@msg `name` must not be empty `name`은(는) 비어 있으면 안 됩니다 +@msg `names` is a list of preset names `names`은(는) 사전 설정 이름 목록입니다 +@msg `names` lists preset names `names`에는 사전 설정 이름을 나열합니다 +@msg `names` must be a list of names `names`은(는) 이름 목록이어야 합니다 +@msg `names` must be a list of swatch names `names`은(는) 견본 이름 목록이어야 합니다 +@msg `names` must be an array of style names `names`은(는) 스타일 이름의 배열이어야 합니다 +@msg `new` is a design name, or true `new`은(는) 디자인 이름 또는 true입니다 +@msg `origin` must be [x, y] in points `origin`은(는) 포인트 단위의 [x, y]여야 합니다 +@msg `path` must be a folder `path`은(는) 폴더여야 합니다 +@msg `path` must be a path object `path`은(는) 패스 개체여야 합니다 +@msg `picks` must be an array (got {v}) `picks`은(는) 배열이어야 합니다({v} 입력됨) +@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks`에는 선택을 1개에서 {MAX_PICKS}개까지 넣어야 합니다({_1} 입력됨) +@msg `pivot` must be a whole number from 0 to 8 `pivot`은(는) 0에서 8 사이의 정수여야 합니다 +@msg `points` must be a list of [x, y] points `points`은(는) [x, y] 점의 목록이어야 합니다 +@msg `points` must be a list of point indexes `points`은(는) 점 인덱스 목록이어야 합니다 +@msg `points` must be an array of point indices `points`은(는) 점 인덱스의 배열이어야 합니다 +@msg `points` must be point indexes `points`은(는) 점 인덱스여야 합니다 +@msg `points` must hold from 2 to 100000 points `points`에는 점이 2개에서 100000개 있어야 합니다 +@msg `port` must be "in" or "out" `port`은(는) "in" 또는 "out"이어야 합니다 +@msg `position` is above or below `position`은(는) above 또는 below입니다 +@msg `preset` must be a string `preset`은(는) 문자열이어야 합니다 +@msg `radius` must be 0 or more `radius`은(는) 0 이상이어야 합니다 +@msg `ratio` must be from 0.1 to 0.5 `ratio`은(는) 0.1에서 0.5 사이여야 합니다 +@msg `rect` must be [x, y, width, height] `rect`은(는) [x, y, width, height]여야 합니다 +@msg `rect` must be [x, y, width, height] with a positive size `rect`은(는) 크기가 양수인 [x, y, width, height]여야 합니다 +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect`은(는) [x0, y0, x1, y1]이어야 합니다({r} 입력됨) +@msg `relation` must be overlap, inside or outside `relation`은(는) overlap, inside 또는 outside여야 합니다 +@msg `replace` must be a string `replace`은(는) 문자열이어야 합니다 +@msg `reversed` must be true or false `reversed`은(는) true 또는 false여야 합니다 +@msg `rotate` must be a finite number `rotate`은(는) 유한한 숫자여야 합니다 +@msg `rows` must be lists of numbers or nulls, not {r} `rows`은(는) 숫자 또는 null의 목록이어야 합니다: {r} +@msg `rule` must be nonZero or evenOdd `rule`은(는) nonZero 또는 evenOdd여야 합니다 +@msg `scale` must be a number from 0.001 to 1000 `scale`은(는) 0.001에서 1000 사이의 숫자여야 합니다 +@msg `scale` must be a number in (0, 100] `scale`은(는) (0, 100] 범위의 숫자여야 합니다 +@msg `scope.{key}` must be a non-empty array `scope.{key}`은(는) 비어 있지 않은 배열이어야 합니다 +@msg `scope` must be "document", {artboards} or {layers} `scope`은(는) "document", {artboards} 또는 {layers}여야 합니다 +@msg `scope` must be "selection" or "document", not {other} `scope`은(는) "selection" 또는 "document"여야 합니다: {other} +@msg `script` must be a string `script`은(는) 문자열이어야 합니다 +@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes`은(는) 계열 인덱스가 하나 이상 있는 목록입니다 +@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes`에는 `type` 또는 `valueAxis`가 필요합니다 +@msg `shift` must be a number of points `shift`은(는) 포인트 단위의 숫자여야 합니다 +@msg `showCenter` must be true or false `showCenter`은(는) true 또는 false여야 합니다 +@msg `side` must be "left", "right", "top" or "bottom" `side`은(는) "left", "right", "top" 또는 "bottom"이어야 합니다 +@msg `side` places the artboard: leave out x, y, width and height `side`가 아트보드의 위치를 정합니다: x, y, width, height는 생략하세요 +@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification`은(는) "fullJustify", "alignLeft", "alignCenter" 또는 "alignRight"여야 합니다 +@msg `size` is a percentage of the base text's size, 1 to 100 `size`은(는) 기준 텍스트 크기에 대한 백분율이며 1에서 100 사이입니다 +@msg `source` and `target` are the same object `source`와 `target`이 같은 개체입니다 +@msg `source` is the only path in series group `target` `source`은(는) 계열 그룹 `target`에 있는 유일한 패스입니다 +@msg `source` must be `folder` or `datasets` `source`은(는) `folder` 또는 `datasets`여야 합니다 +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` 값으로는 상자에 선이 {MAX_COUNT}개보다 많이 생깁니다 +@msg `spacing` must be a number `spacing`은(는) 숫자여야 합니다 +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing`은(는) 0 이상의 포인트 값이거나 "auto"여야 합니다 +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing`은(는) 0보다 크고 {MAX_DISTANCE} pt 이하여야 합니다 +@msg `start` must be a finite number `start`은(는) 유한한 숫자여야 합니다 +@msg `starts` must be an array of anchor indices (or nulls) `starts`은(는) 앵커 인덱스(또는 null)의 배열이어야 합니다 +@msg `status` must be linked, embedded, missing or modified, or a list of them `status`은(는) linked, embedded, missing 또는 modified이거나 이들의 목록이어야 합니다 +@msg `stop` must be a stop index `stop`은(는) 정지점 인덱스여야 합니다 +@msg `story` styles the whole story: give no `start` or `end` `story`은(는) 스토리 전체에 스타일을 적용합니다: `start`와 `end`는 지정하지 마세요 +@msg `story` takes the whole story: give no `start` or `end` `story`은(는) 스토리 전체를 대상으로 합니다: `start`와 `end`는 지정하지 마세요 +@msg `strokeMode` applies to strokes (stroke: true) `strokeMode`은(는) 선에 적용됩니다(stroke: true) +@msg `style` must be roman or japanese `style`은(는) roman 또는 japanese여야 합니다 +@msg `subpaths` must be two integer indices `subpaths`은(는) 정수 인덱스 두 개여야 합니다 +@msg `symbol` must be a symbol name `symbol`은(는) 심볼 이름이어야 합니다 +@msg `target` must be "object" or "contents", not {v} `target`은(는) "object" 또는 "contents"여야 합니다: {v} +@msg `target` must be an object of the same Objects on Path group as `member` `target`은(는) `member`와 같은 패스 위 개체 그룹에 있는 개체여야 합니다 +@msg `tint` applies to global and spot colours `tint`은(는) 글로벌 색상과 별색에 적용됩니다 +@msg `tint` goes with a global or spot `swatch` `tint`은(는) 글로벌 색상 또는 별색 `swatch`와 함께 씁니다 +@msg `tint` must be a number (%) `tint`은(는) 숫자(%)여야 합니다 +@msg `tint` needs a global or spot colour `tint`에는 글로벌 색상이나 별색이 필요합니다 +@msg `to` is already threaded `to`은(는) 이미 스레드로 연결되어 있습니다 +@msg `to` is the design's new name `to`은(는) 디자인의 새 이름입니다 +@msg `to` must be [x, y] `to`은(는) [x, y]여야 합니다 +@msg `to` must be a closed path, area type or type on a path other than `from` `to`은(는) `from`이 아닌 닫힌 패스, 영역 문자 또는 패스 위 문자여야 합니다 +@msg `to` must be a whole number `to`은(는) 정수여야 합니다 +@msg `to` takes the rows of one object's stack, not aligned rows `to`에는 정렬된 행이 아니라 한 개체 스택의 행을 지정합니다 +@msg `tolerance` must be from 0 to 100 pt `tolerance`은(는) 0에서 100 pt 사이여야 합니다 +@msg `useLab` must be true or false `useLab`은(는) true 또는 false여야 합니다 +@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign`은(는) default, top, middle, baseline 또는 bottom이어야 합니다 +@msg `values` must be a list `values`은(는) 목록이어야 합니다 +@msg `values` must be an object `values`은(는) 개체여야 합니다 +@msg `values` must name hex, rgb, cmyk, lab or gray `values`에는 hex, rgb, cmyk, lab 또는 gray를 지정해야 합니다 +@msg `values` names a kind twice `values`에 같은 종류가 두 번 있습니다 +@msg `version` must be 0 or {_1} `version`은(는) 0 또는 {_1}이어야 합니다 +@msg `version` must be 0 or {newest} `version`은(는) 0 또는 {newest}이어야 합니다 +@msg `warichuAlign` must be one of {_1} `warichuAlign`은(는) {_1} 중 하나여야 합니다 +@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines`은(는) {_1}에서 {_2} 사이의 정수여야 합니다 +@msg `weight` must be from 0.01 to 1000 pt `weight`은(는) 0.01에서 1000 pt 사이여야 합니다 +@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights`은(는) 행마다 하나씩 숫자 {_1}개의 목록이어야 합니다({v} 입력됨) +@msg `xml` must be a string `xml`은(는) 문자열이어야 합니다 +@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it `{KEY}` 항목 {n}은(는) {_1}의 바이트 {start}에 있는 단락에 앞선 항목과 다른 들여쓰기를 지정합니다 +@msg `{KEY}` entry {n} must be an object `{KEY}` 항목 {n}은(는) 개체여야 합니다 +@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` `{KEY}` 항목 {n}이(가) `leftIndent`와 `firstLineIndent` 중 어느 것도 설정하지 않습니다 +@msg `{KEY}` entry {n}: `id` must be an object id `{KEY}` 항목 {n}: `id`은(는) 개체 id여야 합니다 +@msg `{KEY}` entry {n}: `start` must be a byte offset `{KEY}` 항목 {n}: `start`은(는) 바이트 오프셋이어야 합니다 +@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt `{KEY}` 항목 {n}: `{k}`은(는) -{MAX_PARA}에서 {MAX_PARA} pt 사이의 숫자여야 합니다 +@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} `{KEY}` 항목 {n}: {_1}에는 바이트 {start}에서 시작하는 단락이 없습니다 +@msg `{KEY}` entry {n}: unknown key `{k}` `{KEY}` 항목 {n}: 알 수 없는 키 `{k}` +@msg `{KEY}` entry {n}: {_1} is not a type object `{KEY}` 항목 {n}: {_1}은(는) 문자 개체가 아닙니다 +@msg `{KEY}` is empty `{KEY}`이(가) 비어 있습니다 +@msg `{KEY}` must be a list `{KEY}`은(는) 목록이어야 합니다 +@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}`에는 항목을 최대 {MAX_PARAGRAPH_INDENTS}개까지 넣을 수 있습니다({_1} 입력됨) +@msg `{KEY}` takes no `{other}` beside it `{KEY}`은(는) `{other}`와(과) 함께 쓸 수 없습니다 +@msg `{_1}` can't be edited or removed `{_1}`은(는) 편집하거나 제거할 수 없습니다 +@msg `{_1}` is built in and always listed `{_1}`은(는) 기본 제공 항목이며 항상 목록에 있습니다 +@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}`은(는) 마지막 text.findFontFiles 검색에서 찾은 글꼴 파일이 아닙니다 +@msg `{_1}` would be overwritten by the package: choose a different folder or name 패키지가 `{_1}`을(를) 덮어쓰게 됩니다: 다른 폴더나 이름을 선택하세요 +@msg `{base}` is larger than {_1} MB `{base}`이(가) {_1} MB보다 큽니다 +@msg `{base}` is not in the DCS file's folder `{base}`이(가) DCS 파일의 폴더에 없습니다 +@msg `{file}` has no art to place `{file}`에 가져올 아트웍이 없습니다 +@msg `{file}` is over {_1} MB `{file}`이(가) {_1} MB를 넘습니다 +@msg `{f}` belongs to a VectorCraft that is running `{f}`은(는) 실행 중인 VectorCraft가 사용하고 있습니다 +@msg `{f}` is the copy of a document open now `{f}`은(는) 지금 열려 있는 문서의 복사본입니다 +@msg `{id}` is no writable format (see document.formats) `{id}`은(는) 기록할 수 있는 형식이 아닙니다(document.formats 참조) +@msg `{id}` isn't a loaded library `{id}`은(는) 불러온 라이브러리가 아닙니다 +@msg `{key}` has no colours `{key}`에 색상이 없습니다 +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}`에 단어가 {MAX_EDIT_WORDS}개보다 많습니다 +@msg `{key}` is a design name `{key}`은(는) 디자인 이름입니다 +@msg `{key}` is a design name or a list of them `{key}`은(는) 디자인 이름 또는 디자인 이름 목록입니다 +@msg `{key}` is longer than {MAX_NAME} characters `{key}`이(가) {MAX_NAME}자보다 깁니다 +@msg `{key}` is out of range `{key}`이(가) 범위를 벗어났습니다 +@msg `{key}` must be 0 or more `{key}`은(는) 0 이상이어야 합니다 +@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] numbers `{key}`은(는) 숫자 [x, y]여야 합니다 +@msg `{key}` must be a list of words `{key}`은(는) 단어 목록이어야 합니다 +@msg `{key}` must be a list, not {v} `{key}`은(는) 목록이어야 합니다: {v} +@msg `{key}` must be a non-negative integer `{key}`은(는) 음이 아닌 정수여야 합니다 +@msg `{key}` must be a number or null, not {v} `{key}`은(는) 숫자 또는 null이어야 합니다: {v} +@msg `{key}` must be a number {_1}..{_2} `{key}`은(는) {_1}..{_2}의 숫자여야 합니다 +@msg `{key}` must be a whole number of 0 or more `{key}`은(는) 0 이상의 정수여야 합니다 +@msg `{key}` must be a whole number of at least 0, not {n} `{key}`은(는) 0 이상의 정수여야 합니다: {n} +@msg `{key}` must be an appearance item index (paint order) or null `{key}`은(는) 모양새 항목 인덱스(칠하는 순서) 또는 null이어야 합니다 +@msg `{key}` must be an array of asset ids (see assets.list) `{key}`은(는) 에셋 id의 배열이어야 합니다(assets.list 참조) +@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}`은(는) 0도에서 180도 사이여야 합니다: {a} +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}`은(는) 0에서 {MAX_DISTANCE} pt 사이여야 합니다 +@msg `{key}` must be from 0.01 to 1000 pt `{key}`은(는) 0.01에서 1000 pt 사이여야 합니다 +@msg `{key}` must be from {_1} to {shift_max} `{key}`은(는) {_1}에서 {shift_max} 사이여야 합니다 +@msg `{key}` must be from {lo} to {hi}, not {v} `{key}`은(는) {lo}에서 {hi} 사이여야 합니다: {v} +@msg `{key}` must be one of auto, always, never `{key}`은(는) auto, always, never 중 하나여야 합니다 +@msg `{key}` must be one of miter, round, bevel `{key}`은(는) miter, round, bevel 중 하나여야 합니다 +@msg `{key}` must be one word of at most {_1} characters `{key}`은(는) 최대 {_1}자의 단어 하나여야 합니다 +@msg `{key}` must hold single words of at most {_1} characters `{key}`에는 최대 {_1}자의 단어를 하나씩 넣어야 합니다 +@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}`에는 패스 위 개체 그룹의 패스에 부착된 개체를 지정해야 합니다 +@msg `{key}` names an object that is hidden or locked `{key}`에 지정한 개체가 숨겨졌거나 잠겨 있습니다 +@msg `{key}`: not a colour: {v} `{key}`: 색상이 아닙니다: {v} +@msg `{k}.family` is required `{k}.family`가 필요합니다 +@msg `{k}` is a number 0..1 `{k}`은(는) 0..1의 숫자입니다 +@msg `{k}` is true or false `{k}`은(는) true 또는 false입니다 +@msg `{k}` isn't an export setting: {_1} `{k}`은(는) 내보내기 설정이 아닙니다: {_1} +@msg `{k}` must be a byte offset `{k}`은(는) 바이트 오프셋이어야 합니다 +@msg `{k}` must be a number `{k}`은(는) 숫자여야 합니다 +@msg `{k}` must be a number of points `{k}`은(는) 포인트 단위의 숫자여야 합니다 +@msg `{k}` must be a string `{k}`은(는) 문자열이어야 합니다 +@msg `{k}` must be a whole number `{k}`은(는) 정수여야 합니다 +@msg `{k}` must be from {lo} to {hi} for {key} {key}에서는 `{k}`이(가) {lo}에서 {hi} 사이여야 합니다 +@msg `{k}` must be {what}, not {v} `{k}`은(는) {what}이어야 합니다: {v} +@msg `{l}` isn't a global or spot colour `{l}`은(는) 글로벌 색상이나 별색이 아닙니다 +@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}`은(는) 비트맵이 아닙니다: clipboard.importSvg, importPdf 또는 importText를 사용하세요 +@msg `{name}` can't be deleted `{name}`은(는) 삭제할 수 없습니다 +@msg `{name}` can't be duplicated `{name}`은(는) 복제할 수 없습니다 +@msg `{name}` can't be edited `{name}`은(는) 편집할 수 없습니다 +@msg `{name}` can't be edited this way `{name}`은(는) 이 방법으로 편집할 수 없습니다 +@msg `{name}` can't be renamed `{name}`은(는) 이름을 바꿀 수 없습니다 +@msg `{name}` can't name a folder `{name}`은(는) 폴더 이름으로 쓸 수 없습니다 +@msg `{name}` has no art to place `{name}`에 가져올 아트웍이 없습니다 +@msg `{name}` has no text to place `{name}`에 가져올 텍스트가 없습니다 +@msg `{name}` in the Word document is not UTF-8 or UTF-16 text Word 문서의 `{name}`이(가) UTF-8 또는 UTF-16 텍스트가 아닙니다 +@msg `{name}` in the Word document is not well-formed XML: {e} Word 문서의 `{name}`이(가) 올바른 형식의 XML이 아닙니다: {e} +@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep Word 문서의 `{name}`에 요소가 {MAX_NESTING}단계보다 깊게 중첩되어 있습니다 +@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}`은(는) 기본 제공 사전 설정이며 읽기 전용입니다: 다른 이름으로 저장하세요 +@msg `{name}` is a built-in preset: save under another name `{name}`은(는) 기본 제공 사전 설정입니다: 다른 이름으로 저장하세요 +@msg `{name}` is a built-in profile and stays `{name}`은(는) 기본 제공 프로파일이므로 그대로 남습니다 +@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}`은(는) 별도의 파일이며 DCS 파일의 경로 옆에서만 찾을 수 있습니다 +@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}`은(는) `{base}`의 색조입니다: `{base}`을(를) 편집하세요(색조 견본은 이름만 바꿀 수 있습니다) +@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}`은(는) 작업 공간입니다: 먼저 edit.colorSettings로 다른 작업 공간을 선택하세요 +@msg `{name}` is built in and stays `{name}`은(는) 기본 제공 항목이므로 그대로 남습니다 +@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}`에는 같은 종류의 페인트(색상, 그레이디언트 또는 패턴)만 지정할 수 있습니다 +@msg `{name}`: {_1}: {e} `{name}`: {_1}: {e} +@msg `{n}` can't be deleted `{n}`은(는) 삭제할 수 없습니다 +@msg `{n}` can't be merged `{n}`은(는) 병합할 수 없습니다 +@msg `{n}` can't be moved `{n}`은(는) 이동할 수 없습니다 +@msg `{n}` is not a colour swatch `{n}`은(는) 색상 견본이 아닙니다 +@msg `{n}` isn't a solid colour `{n}`은(는) 단색이 아닙니다 +@msg `{n}`: colour groups hold solid colours only `{n}`: 색상 그룹에는 단색만 넣을 수 있습니다 +@msg `{other}` doesn't trace an image `{other}`은(는) 이미지를 추적하지 않습니다 +@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}`: 이 단계의 참조가 {_1} MB보다 많은 값을 복사해 넣습니다 +@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}`은(는) 축 태그가 아닙니다: 태그는 "wght"처럼 네 글자입니다 +@msg `{to}` is empty or already used `{to}`이(가) 비어 있거나 이미 사용 중입니다 +@msg `{u}` is no Unicode character `{u}`은(는) 유니코드 문자가 아닙니다 +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}`이(가) {MAX_TEXT}바이트보다 깁니다 +@msg `{what}` must be text `{what}`은(는) 텍스트여야 합니다 +@msg `{word}` isn't in the custom dictionary `{word}`은(는) 사용자 사전에 없습니다 +@msg a CMap has too many mappings CMap에 매핑이 너무 많습니다 +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard DXF 도면에는 원점이 하나입니다: 아트보드마다 도면을 하나씩 만들려면 useArtboards: true를 전달하세요 +@msg a PDF presets file with over {_1} presets 사전 설정이 {_1}개를 넘는 PDF 사전 설정 파일입니다 +@msg a \u escape needs four hexadecimal digits \u 이스케이프에는 16진수 네 자리가 필요합니다 +@msg a batch preview cannot switch documents 일괄 처리 미리 보기에서는 문서를 전환할 수 없습니다 +@msg a brush named `{nn}` exists `{nn}` 브러시가 이미 있습니다 +@msg a comma in an XPath expression must separate function arguments XPath 식의 쉼표는 함수 인수를 구분해야 합니다 +@msg a container of the listed objects is gone 나열한 개체의 컨테이너 하나가 없어졌습니다 +@msg a content stream can't be read 콘텐츠 스트림을 읽을 수 없습니다 +@msg a content stream isn't a stream 콘텐츠 스트림이 스트림이 아닙니다 +@msg a design holds at most {_1} objects 디자인에는 개체를 최대 {_1}개까지 넣을 수 있습니다 +@msg a design needs a name 디자인에는 이름이 필요합니다 +@msg a dictionary grew too large 사전이 너무 커졌습니다 +@msg a document holds at most {_1} SVG filters 문서에는 SVG 필터를 최대 {_1}개까지 둘 수 있습니다 +@msg a document keeps at most {_1} graph designs 문서에는 그래프 디자인을 최대 {_1}개까지 둘 수 있습니다 +@msg a document keeps at most {_1} saved selections 문서에는 저장된 선택 영역을 최대 {_1}개까지 둘 수 있습니다 +@msg a document keeps at most {_1} views 문서에는 보기를 최대 {_1}개까지 둘 수 있습니다 +@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files 문서에는 JavaScript 파일을 최대 {MAX_SVG_SCRIPTS}개까지 연결할 수 있습니다 +@msg a document with unsaved changes has no recovery copy 저장하지 않은 변경 내용이 있는 문서에는 복구 복사본이 없습니다 +@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes feConvolveMatrix 필터의 order가 {x}×{y}여서 usvg의 커널 크기 곱셈이 오버플로됩니다 +@msg a font's data is too large 글꼴 데이터가 너무 큽니다 +@msg a font's subroutines are too large 글꼴의 서브루틴이 너무 큽니다 +@msg a function declaration without a name 이름이 없는 함수 선언입니다 +@msg a gradient preset is already called {n} {n}(이)라는 그레이디언트 사전 설정이 이미 있습니다 +@msg a graph has at most {_1} series, so it transposes with at most that many categories 그래프에는 계열이 최대 {_1}개이므로 범주가 그 개수 이하일 때만 행과 열을 바꿀 수 있습니다 +@msg a host value has too many entries 호스트 값에 항목이 너무 많습니다 +@msg a host value is nested too deep 호스트 값이 너무 깊게 중첩되어 있습니다 +@msg a layer can't be merged into its own sublayer 레이어는 자신의 하위 레이어에 병합할 수 없습니다 +@msg a layer's form box isn't an array 레이어 폼의 상자가 배열이 아닙니다 +@msg a layer's form can't be read 레이어의 폼을 읽을 수 없습니다 +@msg a layer's form has no box 레이어의 폼에 상자가 없습니다 +@msg a layer's form has no dictionary 레이어의 폼에 사전이 없습니다 +@msg a legend draws at most {MAX_CHIPS} swatches 범례에는 견본을 최대 {MAX_CHIPS}개까지 그립니다 +@msg a name names one asset: give multiple: false, or one object 이름은 에셋 하나를 가리킵니다: multiple: false를 지정하거나 개체 하나를 지정하세요 +@msg a namespace declaration uses a reserved prefix or URI 네임스페이스 선언에 예약된 접두사나 URI를 사용했습니다 +@msg a namespace qualifier is undefined 네임스페이스 한정자가 정의되지 않았습니다 +@msg a namespace with an empty URI cannot have a prefix URI가 빈 네임스페이스에는 접두사를 붙일 수 없습니다 +@msg a number has no digits 숫자에 자릿수가 없습니다 +@msg a number's exponent has no digits 숫자의 지수에 자릿수가 없습니다 +@msg a number's fraction has no digits 숫자의 소수부에 자릿수가 없습니다 +@msg a page can't be read 페이지를 읽을 수 없습니다 +@msg a page object is used by something else 페이지 개체를 다른 항목이 사용하고 있습니다 +@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) 페이지 미리 보기 이미지는 한 변이 1에서 {THUMBNAIL_SIZE}인 RGB 픽셀이어야 합니다({w} × {h} 입력됨) +@msg a pattern layer doesn't end what it starts 패턴 레이어가 시작한 것을 끝내지 않습니다 +@msg a pattern layer's colors hold something else 패턴 레이어의 색상에 다른 것이 들어 있습니다 +@msg a pattern named `{n}` exists `{n}` 패턴이 이미 있습니다 +@msg a plug-in with id {_1} is already installed id가 {_1}인 플러그인이 이미 설치되어 있습니다 +@msg a preset named `{n}` exists `{n}` 사전 설정이 이미 있습니다 +@msg a print presets file with over {_1} presets 사전 설정이 {_1}개를 넘는 인쇄 사전 설정 파일입니다 +@msg a profile is already called {n} {n}(이)라는 프로파일이 이미 있습니다 +@msg a property descriptor cannot have both a value or writable and a get or set 속성 설명자에는 value 또는 writable과 get 또는 set을 함께 둘 수 없습니다 +@msg a property descriptor must be an object 속성 설명자는 개체여야 합니다 +@msg a property's {name} must be a function or undefined 속성의 {name}은(는) 함수 또는 undefined여야 합니다 +@msg a row can't go into itself 행을 자기 자신 안으로 옮길 수 없습니다 +@msg a series goes on the left or the right value axis 계열은 왼쪽 또는 오른쪽 값 축에 놓입니다 +@msg a start must be an anchor index ≥ 0 or null 시작은 0 이상의 앵커 인덱스 또는 null이어야 합니다 +@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied 그 안의 스트림을 디코딩하면 256 MB를 넘거나 디코딩 매개변수를 적용할 수 없습니다 +@msg a string holds a control character 문자열에 제어 문자가 있습니다 +@msg a string holds an escape sequence JSON does not have 문자열에 JSON에 없는 이스케이프 시퀀스가 있습니다 +@msg a string is not closed 문자열이 닫히지 않았습니다 +@msg a style named `{name}` already exists `{name}` 스타일이 이미 있습니다 +@msg a style named `{to}` already exists `{to}` 스타일이 이미 있습니다 +@msg a swatch needs a colour, gradient or pattern 견본에는 색상, 그레이디언트 또는 패턴이 필요합니다 +@msg a symbol named `{n}` already exists `{n}` 심볼이 이미 있습니다 +@msg a theme has 1 to {THEME_SIZE} colours 테마에는 색상이 1개에서 {THEME_SIZE}개 있습니다 +@msg a theme is already called {name} (replace: "{name}" overwrites it) {name}(이)라는 테마가 이미 있습니다(replace: "{name}"을(를) 지정하면 덮어씁니다) +@msg a toolbar name has at most {MAX_NAME} characters 도구 막대 이름은 최대 {MAX_NAME}자입니다 +@msg a view named `{_1}` exists `{_1}` 보기가 이미 있습니다 +@msg a view named `{name}` exists `{name}` 보기가 이미 있습니다 +@msg a workspace keeps at most {MAX} toolbars 작업 영역에는 도구 막대를 최대 {MAX}개까지 둘 수 있습니다 +@msg a {_1} series can't go in a {_2} graph {_1} 계열은 {_2} 그래프에 넣을 수 없습니다 +@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v}: "pdf"(PDF 호환, 기본값) 또는 "7"(이전 .ai, 버전 7)이어야 합니다 +@msg already editing an opacity mask 이미 불투명 마스크를 편집하고 있습니다 +@msg already editing another object's opacity mask 이미 다른 개체의 불투명 마스크를 편집하고 있습니다 +@msg already in pattern editing mode 이미 패턴 편집 모드입니다 +@msg an SVG filter is named by its id: give `xml` with `name` SVG 필터의 이름은 id입니다: `name`과 함께 `xml`을 지정하세요 +@msg an XML element has two attributes with the same name XML 요소에 이름이 같은 속성이 두 개 있습니다 +@msg an XML value cannot be inserted into itself or its descendants XML 값을 자기 자신이나 그 하위 항목에 삽입할 수 없습니다 +@msg an XML value's children cannot be assigned by position XML 값의 자식은 위치로 대입할 수 없습니다 +@msg an XML value's children cannot be deleted by position XML 값의 자식은 위치로 삭제할 수 없습니다 +@msg an XMLList converts to XML only when it has one item XMLList는 항목이 하나일 때만 XML로 변환됩니다 +@msg an XPath node test is missing XPath 노드 테스트가 없습니다 +@msg an XPath operand is missing XPath 피연산자가 없습니다 +@msg an XPath operator is missing XPath 연산자가 없습니다 +@msg an XPath predicate or step cannot follow here 여기에는 XPath 조건자나 단계가 올 수 없습니다 +@msg an XPath predicate or step follows a value that is not a node-set 노드 집합이 아닌 값 뒤에 XPath 조건자나 단계가 있습니다 +@msg an XPath step is missing XPath 단계가 없습니다 +@msg an artboard is listed twice 아트보드가 두 번 나열되었습니다 +@msg an empty artboard has no width or height to scale to 빈 아트보드에는 맞춰 비율을 조정할 너비나 높이가 없습니다 +@msg an image couldn't be decoded and is missing from the picture 이미지를 디코딩할 수 없어 그림에서 빠졌습니다 +@msg an image is too large for DXF and was left out 이미지가 DXF에 쓰기에는 너무 커서 제외되었습니다 +@msg an image of {_1} MB, at most {_2} MB 이미지가 {_1} MB입니다(최대 {_2} MB) +@msg an image of {declared_w} × {declared_h} pixels is too large to draw {declared_w} × {declared_h} 픽셀 이미지는 너무 커서 그릴 수 없습니다 +@msg an image of {width} × {height} pixels is too large to draw {width} × {height} 픽셀 이미지는 너무 커서 그릴 수 없습니다 +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) {w} × {h} 픽셀 이미지는 너무 커서 변환할 수 없습니다(최대 {_1} 픽셀) +@msg an index must be a whole number 인덱스는 정수여야 합니다 +@msg an inline graphic has no glyphs 인라인 그래픽에 글리프가 없습니다 +@msg an invisible text form can't be read 보이지 않는 텍스트 폼을 읽을 수 없습니다 +@msg an object is missing 개체가 없습니다 +@msg an object's header is missing 개체의 헤더가 없습니다 +@msg an overprinting form can't be read 오버프린트 폼을 읽을 수 없습니다 +@msg an unread object has no readable bounds 읽지 않은 개체에 읽을 수 있는 경계가 없습니다 +@msg an unread object holds a frame of threaded type 읽지 않은 개체에 스레드 문자의 프레임이 있습니다 +@msg an unread object is missing from its layer 읽지 않은 개체가 레이어에 없습니다 +@msg anchor `smooth` must be true or false 앵커의 `smooth`은(는) true 또는 false여야 합니다 +@msg anchor `{key}` must be [x, y] 앵커의 `{key}`은(는) [x, y]여야 합니다 +@msg anchor `{key}` must be a number 앵커의 `{key}`은(는) 숫자여야 합니다 +@msg angle and roundness must be finite 각도와 원형률은 유한해야 합니다 +@msg angles must be a list as long as pins (degrees or null) angles는 pins와 길이가 같은 목록이어야 합니다(도 또는 null) +@msg appearance must be flat|center|edge appearance는 flat|center|edge여야 합니다 +@msg apply is "fill" or "stroke", not `{other}` apply는 "fill" 또는 "stroke"입니다: `{other}` +@msg apply was called on a value that is not a function 함수가 아닌 값에서 apply를 호출했습니다 +@msg area must be a closed polygon of 3 to 100000 [x, y] points area는 [x, y] 점 3개에서 100000개로 이루어진 닫힌 다각형이어야 합니다 +@msg area type in a thread cannot use Dynamic Text: {frames} 스레드로 연결된 영역 문자에는 동적 텍스트를 사용할 수 없습니다: {frames} +@msg area type needs a path that encloses an area 영역 문자에는 영역을 둘러싸는 패스가 필요합니다 +@msg area type {_1} is named twice 영역 문자 {_1}이(가) 두 번 지정되었습니다 +@msg array literal too long 배열 리터럴이 너무 깁니다 +@msg arrowAlign must be extend|tip, got {o} arrowAlign은 extend|tip이어야 합니다({o} 입력됨) +@msg arrowScale must be [start, end] arrowScale은 [start, end]여야 합니다 +@msg artboard is a 0-based index artboard는 0부터 시작하는 인덱스입니다 +@msg artboard must be a 0-based index artboard는 0부터 시작하는 인덱스여야 합니다 +@msg artboard {i} doesn't print 아트보드 {i}은(는) 인쇄되지 않습니다 +@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout은 {layout?, columns?, spacing?, rightToLeft?}여야 합니다 +@msg artboardLayout.columns must be 1 or more artboardLayout.columns는 1 이상이어야 합니다 +@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout은 gridByRow, gridByColumn, row 또는 column이어야 합니다 +@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft는 true 또는 false여야 합니다 +@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing은 0–{MAX_SPACING} pt여야 합니다 +@msg artboards must be a non-empty list artboards는 비어 있지 않은 목록이어야 합니다 +@msg artboards must be a number artboards는 숫자여야 합니다 +@msg asset `{_1}` has no art to export 에셋 `{_1}`에 내보낼 아트웍이 없습니다 +@msg assignment to a constant binding 상수 바인딩에 대입했습니다 +@msg at least one row that isn't excluded needs a weight above 0 제외되지 않은 행 중 하나 이상은 가중치가 0보다 커야 합니다 +@msg at most {MAX_KEYWORDS} keywords 키워드는 최대 {MAX_KEYWORDS}개입니다 +@msg at most {MAX_MATCHES} matches are kept 일치 항목은 최대 {MAX_MATCHES}개까지 보관합니다 +@msg at most {MAX_POINTS} points 점은 최대 {MAX_POINTS}개입니다 +@msg at most {MAX_QUEUE} files 파일은 최대 {MAX_QUEUE}개입니다 +@msg at must be [x, y] at은 [x, y]여야 합니다 +@msg at/rect lie outside the canvas at/rect가 캔버스 밖에 있습니다 +@msg attribute must be size|leading|tracking|kerning|baselineShift attribute는 size|leading|tracking|kerning|baselineShift여야 합니다 +@msg attrs must be an object attrs는 개체여야 합니다 +@msg automation path rejected: the folder's name isn't valid text 자동화 경로가 거부되었습니다: 폴더 이름이 올바른 텍스트가 아닙니다 +@msg automation path rejected: {_1} isn't valid text 자동화 경로가 거부되었습니다: {_1}이(가) 올바른 텍스트가 아닙니다 +@msg automation path rejected: {why}: {path} 자동화 경로가 거부되었습니다: {why}: {path} +@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis는 "vertical", "horizontal" 또는 도 단위 각도여야 합니다: `{given}` +@msg backgroundContents must be transparent or white backgroundContents는 transparent 또는 white여야 합니다 +@msg bad AES key 잘못된 AES 키 +@msg bad `runs`: {e} 잘못된 `runs`: {e} +@msg bad arrowScale 잘못된 arrowScale +@msg bad base64 잘못된 base64 +@msg bad color {c} 잘못된 색상 {c} +@msg bad colour 잘못된 색상 +@msg bad colour in `colors` `colors`에 잘못된 색상이 있습니다 +@msg bad colour {v} 잘못된 색상 {v} +@msg bad colour {v} in groupColors groupColors에 잘못된 색상 {v}이(가) 있습니다 +@msg bad cross-reference entry 잘못된 상호 참조 항목 +@msg bad cross-reference table 잘못된 상호 참조 테이블 +@msg bad current colour {v} 잘못된 현재 색상 {v} +@msg bad dataBase64 잘못된 dataBase64 +@msg bad edit 잘못된 편집 +@msg bad fill {v} 잘못된 칠 {v} +@msg bad fit: {e} 잘못된 fit: {e} +@msg bad model {other} (keep, new or document) 잘못된 model {other}(keep, new 또는 document) +@msg bad new colour {v} 잘못된 새 색상 {v} +@msg bad object 잘못된 개체 +@msg bad object number 잘못된 개체 번호 +@msg bad offset 잘못된 오프셋 +@msg bad path data: {e} 잘못된 패스 데이터: {e} +@msg bad point {v}: expected [x, y] 잘못된 점 {v}: [x, y]여야 합니다 +@msg bad point {v}: expected [x, y] or [x, y, pressure] 잘못된 점 {v}: [x, y] 또는 [x, y, pressure]여야 합니다 +@msg bad reference 잘못된 참조 +@msg bad trailer size 잘못된 트레일러 크기 +@msg bad {k} colour 잘못된 {k} 색상 +@msg batches cannot nest 일괄 처리는 중첩할 수 없습니다 +@msg bind something to a variable first 먼저 변수에 항목을 바인딩하세요 +@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed는 0–{MAX_BLEED} pt여야 합니다: 숫자, [top, bottom, left, right] 또는 {{top, bottom, left, right}} +@msg blend must be a mode name blend는 모드 이름이어야 합니다 +@msg both sides of an XPath | must select nodes XPath |의 양쪽은 모두 노드를 선택해야 합니다 +@msg brush must be a name or null brush는 이름 또는 null이어야 합니다 +@msg by must be a number by는 숫자여야 합니다 +@msg can't add page thumbnails: {why} 페이지 미리 보기 이미지를 추가할 수 없습니다: {why} +@msg can't add the editing data for other apps: {why} 다른 앱용 편집 데이터를 추가할 수 없습니다: {why} +@msg can't edit the written PDF: {why} 기록한 PDF를 편집할 수 없습니다: {why} +@msg can't encrypt the PDF: {why} PDF를 암호화할 수 없습니다: {why} +@msg can't linearise the PDF: {why} PDF를 선형화할 수 없습니다: {why} +@msg can't open `{_1}`: {_2} `{_1}`을(를) 열 수 없습니다: {_2} +@msg can't open `{_1}`: {e} `{_1}`을(를) 열 수 없습니다: {e} +@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) `{name}`을(를) 열 수 없습니다: VectorCraft가 읽는 형식이 아닙니다(document.formats 참조) +@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) `{name}`을(를) 가져올 수 없습니다: VectorCraft가 읽는 형식이 아닙니다(document.formats 참조) +@msg can't read back the written PDF: {e} 기록한 PDF를 다시 읽을 수 없습니다: {e} +@msg can't read this Word document: {why} 이 Word 문서를 읽을 수 없습니다: {why} +@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp .{e} 이미지는 기록할 수 없습니다: .png, .jpg, .gif, .webp, .tif 또는 .bmp를 사용하세요 +@msg can't write PDF layers, overprint or invisible text: {why} PDF 레이어, 오버프린트 또는 보이지 않는 텍스트를 기록할 수 없습니다: {why} +@msg can't write the file's copyright: {why} 파일의 저작권을 기록할 수 없습니다: {why} +@msg can't write the file's modification date: {why} 파일의 수정 날짜를 기록할 수 없습니다: {why} +@msg can't write the pages' user units: {why} 페이지의 사용자 단위를 기록할 수 없습니다: {why} +@msg canceled 취소됨 +@msg cannot convert the object to a primitive value 개체를 기본 값으로 변환할 수 없습니다 +@msg cannot convert undefined or null to an object undefined 또는 null은 개체로 변환할 수 없습니다 +@msg cannot distort a zero-size or non-finite bounding box 크기가 0이거나 유한하지 않은 테두리 상자는 왜곡할 수 없습니다 +@msg cannot move an object into itself 개체를 자기 자신 안으로 이동할 수 없습니다 +@msg case must be upper|lower|title|sentence case는 upper|lower|title|sentence여야 합니다 +@msg category `{c}`: one of {_1} category `{c}`: {_1} 중 하나여야 합니다 +@msg center must be [x, y] center는 [x, y]여야 합니다 +@msg character dimensions cannot be represented with the source text transform 원본 텍스트 변형으로는 문자 치수를 나타낼 수 없습니다 +@msg character dimensions cannot be represented with the target text transform 대상 텍스트 변형으로는 문자 치수를 나타낼 수 없습니다 +@msg choose at least one artboard 아트보드를 하나 이상 선택하세요 +@msg choose at least one status 상태를 하나 이상 선택하세요 +@msg clip must be true or false clip은 true 또는 false여야 합니다 +@msg clippingMask must be true or false clippingMask는 true 또는 false여야 합니다 +@msg closed must be a boolean closed는 부울이어야 합니다 +@msg collapsed must be true or false collapsed는 true 또는 false여야 합니다 +@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile: “{profile}”(이)라는 RGB 또는 CMYK 프로파일이 없습니다 +@msg colorMode must be rgb or cmyk, not `{m}` colorMode는 rgb 또는 cmyk여야 합니다: `{m}` +@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel은 {own}(문서의 모드), grayscale 또는 bitmap이어야 합니다 +@msg colour groups can't go into colour groups 색상 그룹은 색상 그룹 안에 넣을 수 없습니다 +@msg colour, mode and spot apply to solid-colour swatches only colour, mode, spot은 단색 견본에만 적용됩니다 +@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read {column}열: `{cell}`은(는) 그래프 데이터 파일의 `%` 열이며, 이 열은 읽지 않습니다 +@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable {column}열: `{cell}`이(가) 가리키는 `{name}`은(는) {_1} 변수입니다 +@msg column {column}: `{cell}` names no variable {column}열: `{cell}`이(가) 가리키는 변수가 없습니다 +@msg column {column}: the variable name is longer than {MAX_NAME} characters {column}열: 변수 이름이 {MAX_NAME}자보다 깁니다 +@msg column: true floats the whole column and takes no onto, above or below column: true는 열 전체를 띄우며 onto, above, below와 함께 쓸 수 없습니다 +@msg command must be a shape.* command command는 shape.* 명령이어야 합니다 +@msg commands[{i}]: missing `command` commands[{i}]: `command`가 없습니다 +@msg composer must be "singleLine" or "everyLine" composer는 "singleLine" 또는 "everyLine"이어야 합니다 +@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes 컴파운드 모양에는 패스, 컴파운드 패스, 그룹, 컴파운드 모양, 문자, 블렌드, 엔벨로프를 넣을 수 있습니다 +@msg compound {_1} must contain exactly one open path to join 컴파운드 {_1}에는 연결할 열린 패스가 정확히 하나 있어야 합니다 +@msg connect needs a path other than the one extended connect에는 연장한 패스가 아닌 다른 패스가 필요합니다 +@msg connect needs id connect에는 id가 필요합니다 +@msg connect path has no open end connect 패스에 열린 끝이 없습니다 +@msg continuation must be an array of path chunks continuation은 패스 조각의 배열이어야 합니다 +@msg continuation requires points continuation에는 points가 필요합니다 +@msg copy needs path: the file to copy into the document's folder copy에는 path가 필요합니다: 문서의 폴더에 복사할 파일입니다 +@msg copy takes images only: relink placed documents where their files are copy는 이미지에만 쓸 수 있습니다: 가져온 문서는 파일이 있는 위치로 다시 연결하세요 +@msg copying a file isn't available on the web 웹에서는 파일을 복사할 수 없습니다 +@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus는 unknown, copyrighted 또는 publicDomain이어야 합니다 +@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl은 URL이어야 합니다(공백 없이 최대 2048자) +@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners는 [x, y] 점 4개여야 합니다(TL, TR, BR, BL) +@msg counterPer `{c}`: item or file counterPer `{c}`: item 또는 file이어야 합니다 +@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}`: art, trim, bleed, media 상자는 PDF 페이지에만 있습니다. "crop" 또는 "bounding"을 사용하세요 +@msg crop rectangle doesn't overlap the image 자르기 사각형이 이미지와 겹치지 않습니다 +@msg cross-reference and object streams aren't supported 상호 참조 스트림과 개체 스트림은 지원하지 않습니다 +@msg damaged compressed data: {e} 압축 데이터가 손상되었습니다: {e} +@msg dash lengths must be numbers, got {v} 대시 길이는 숫자여야 합니다({v} 입력됨) +@msg dash must be a list of lengths or null, got {v} dash는 길이 목록 또는 null이어야 합니다({v} 입력됨) +@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) data 키 `{k}`: 글자, 숫자, -, _, .만 쓸 수 있습니다(name 또는 vc-…는 안 됨) +@msg data must be an object of key: value, not {v} data는 key: value로 이루어진 개체여야 합니다: {v} +@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare 데이터 세트 `{name}`에 라이브러리가 선언하지 않은 `{var}`의 값이 있습니다 +@msg data set `{name}` holds two values for `{var}` 데이터 세트 `{name}`에 `{var}`의 값이 두 개 있습니다 +@msg data set `{name}` is declared twice 데이터 세트 `{name}`이(가) 두 번 선언되었습니다 +@msg data takes at most {MAX_DATA} keys at once data에는 한 번에 키를 최대 {MAX_DATA}개까지 넣을 수 있습니다 +@msg dataset `{name}` already exists 데이터 세트 `{name}`이(가) 이미 있습니다 +@msg dataset `{to}` already exists 데이터 세트 `{to}`이(가) 이미 있습니다 +@msg date `{d}`: YYYY-MM-DD date `{d}`: YYYY-MM-DD 형식이어야 합니다 +@msg decoded data is too large 디코딩한 데이터가 너무 큽니다 +@msg define a variable first 먼저 변수를 정의하세요 +@msg detect must be true or false detect는 true 또는 false여야 합니다 +@msg direct-select anchor points of a path 패스의 앵커 포인트를 직접 선택하세요 +@msg distance must be > 0 distance는 0보다 커야 합니다 +@msg dx and dy give the arrow's direction dx와 dy는 화살표의 방향을 지정합니다 +@msg dx and dy must be numbers dx와 dy는 숫자여야 합니다 +@msg dxf fitTo must be [width, height], positive dxf fitTo는 양수인 [width, height]여야 합니다 +@msg dxf must be an object of DXF import options dxf는 DXF 가져오기 옵션 개체여야 합니다 +@msg dxf scale must be a positive number up to {_1}, not {s} dxf scale은 {_1} 이하의 양수여야 합니다: {s} +@msg dxf unit `{u}`: a unit such as mm, cm, in or pt dxf unit `{u}`: mm, cm, in, pt 같은 단위여야 합니다 +@msg dxf: {e} dxf: {e} +@msg each format is an object {format, scale?, suffix?} 각 형식은 {format, scale?, suffix?} 개체입니다 +@msg each item needs an id 각 항목에는 id가 필요합니다 +@msg each match needs `kind` 각 일치 항목에는 `kind`가 필요합니다 +@msg each match needs a `name` 각 일치 항목에는 `name`이 필요합니다 +@msg each of `fonts` is {family, style?}; 1 to 1000 of them `fonts`의 각 항목은 {family, style?}이며 1개에서 1000개까지 지정합니다 +@msg each of `items` is {kind, item} `items`의 각 항목은 {kind, item}입니다 +@msg each of `items` is {{id, corners?}} with an object id, not {it} `items`의 각 항목은 개체 id가 있는 {{id, corners?}}입니다: {it} +@msg each path chunk must contain 1 to 4096 points 각 패스 조각에는 점이 1개에서 4096개 있어야 합니다 +@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) 각 선택은 {{at: [x, y]}} 또는 {{rect: [x0, y0, x1, y1]}}입니다({v} 입력됨) +@msg each row needs `from` 각 행에는 `from`이 필요합니다 +@msg each stop needs a position 각 정지점에는 위치가 필요합니다 +@msg each subpath needs an anchors array 각 하위 패스에는 anchors 배열이 필요합니다 +@msg each weight must be a number of 0 or more (got {v}) 각 가중치는 0 이상의 숫자여야 합니다({v} 입력됨) +@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType은 easeIn, easeOut, easeInOut 또는 easeOutIn이어야 합니다 +@msg editingData must be true or false editingData는 true 또는 false여야 합니다 +@msg editingData must be true, false or "always" editingData는 true, false 또는 "always"여야 합니다 +@msg edits overlap 편집 범위가 겹칩니다 +@msg effects of objects whose appearances differ can't be moved together 모양새가 서로 다른 개체의 효과는 함께 이동할 수 없습니다 +@msg empty header in column {_1} {_1}열의 머리글이 비어 있습니다 +@msg encoding `{_1}`: utf8 or utf16 encoding `{_1}`: utf8 또는 utf16이어야 합니다 +@msg ends must be two of "first" and "last" ends는 "first"와 "last" 중에서 고른 두 개여야 합니다 +@msg every numbered name for the result is taken 결과에 붙일 번호 이름이 모두 사용 중입니다 +@msg expanded SVG styles exceed the import size limit 확장한 SVG 스타일이 가져오기 크기 한도를 넘습니다 +@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes 내보내기 사전 설정 파일이 {MAX_PRESET_FILE_BYTES}바이트보다 큽니다 +@msg exportText must be editable or appearance exportText는 editable 또는 appearance여야 합니다 +@msg exportType must be movieClip or graphic, not `{s}` exportType은 movieClip 또는 graphic이어야 합니다: `{s}` +@msg extend needs id extend에는 id가 필요합니다 +@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges에는 태그를 최대 {MAX_FEATURE_CHANGES}개까지 나열할 수 있습니다 +@msg featureChanges must be a list of tags featureChanges는 태그 목록이어야 합니다 +@msg features must be a list of tags features는 태그 목록이어야 합니다 +@msg fidelity must be 0..100 fidelity는 0..100이어야 합니다 +@msg files must be {name, dataBase64} files는 {name, dataBase64}여야 합니다 +@msg finally table {t} out of range finally 테이블 {t}이(가) 범위를 벗어났습니다 +@msg fit must be none|autoHeight|shrinkText|block, got {id} fit은 none|autoHeight|shrinkText|block이어야 합니다({id} 입력됨) +@msg flags cannot be given with a RegExp object RegExp 개체와 함께 플래그를 지정할 수 없습니다 +@msg flattener: {e} 병합: {e} +@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}`: high, medium, low 또는 저장된 사전 설정이어야 합니다(flattener.presets.list 참조) +@msg flipProfile must be along|across, got {o} flipProfile은 along|across여야 합니다({o} 입력됨) +@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}`: none, rows, format, item 또는 itemFormat이어야 합니다 +@msg folders deeper than {MAX_BATCH_DEPTH} levels 폴더가 {MAX_BATCH_DEPTH}단계보다 깊습니다 +@msg folders deeper than {MAX_DEPTH} levels 폴더가 {MAX_DEPTH}단계보다 깊습니다 +@msg font family `{family}` is not available 글꼴 모음 `{family}`을(를) 사용할 수 없습니다 +@msg font family `{tf}` is not available 글꼴 모음 `{tf}`을(를) 사용할 수 없습니다 +@msg format `{f}`: pdf or postscript format `{f}`: pdf 또는 postscript여야 합니다 +@msg format `{f}`: text or json format `{f}`: text 또는 json이어야 합니다 +@msg format is `{format}` format은 `{format}`입니다 +@msg formats must be an array of rows, not {v} formats는 행의 배열이어야 합니다: {v} +@msg frequency must be between 1 and 300 lines per inch frequency는 인치당 1줄에서 300줄 사이여야 합니다 +@msg from and to must be finite from과 to는 유한해야 합니다 +@msg from must be [x0, y0, x1, y1] from은 [x0, y0, x1, y1]이어야 합니다 +@msg function declaration index out of range 함수 선언 인덱스가 범위를 벗어났습니다 +@msg give `before` or `after`, not both `before`와 `after` 중 하나만 지정하세요 +@msg give `char`, `text`, `code` or `unicode` `char`, `text`, `code` 또는 `unicode`를 지정하세요 +@msg give `colors`, or `color` and `rule` `colors`를 지정하거나 `color`와 `rule`을 지정하세요 +@msg give `count` or `spacing`, not both `count`와 `spacing` 중 하나만 지정하세요 +@msg give `event`, such as onclick onclick 같은 `event`를 지정하세요 +@msg give `file`, the JavaScript file's path or URL JavaScript 파일의 경로나 URL인 `file`을 지정하세요 +@msg give `fill` and/or `stroke` `fill`이나 `stroke`, 또는 둘 다 지정하세요 +@msg give `fill`, `stroke` or both `fill`이나 `stroke`, 또는 둘 다 지정하세요 +@msg give `folder` `folder`를 지정하세요 +@msg give `fonts`, or open a document `fonts`를 지정하거나 문서를 여세요 +@msg give `from` or `indices`, not both `from`과 `indices` 중 하나만 지정하세요 +@msg give `id` (no text is being edited) `id`를 지정하세요(편집 중인 텍스트가 없습니다) +@msg give `id` and `anchors`, or `items`, not both `id`와 `anchors`를 지정하거나 `items`를 지정하세요. 둘 다 지정할 수는 없습니다 +@msg give `id` or `guide`, not both `id`와 `guide` 중 하나만 지정하세요 +@msg give `length` or `to` `length` 또는 `to`를 지정하세요 +@msg give `matches`: [{kind, name, target}] `matches`를 지정하세요: [{kind, name, target}] +@msg give `name` (or edit a pattern) `name`을 지정하세요(또는 패턴을 편집하세요) +@msg give `name` or `names` `name` 또는 `names`를 지정하세요 +@msg give `name`, an SVG filter's name SVG 필터의 이름인 `name`을 지정하세요 +@msg give `path` (a .wasm file) or `dataBase64` (the module) `path`(.wasm 파일) 또는 `dataBase64`(모듈)를 지정하세요 +@msg give `path`, `data` or `dataBase64` `path`, `data` 또는 `dataBase64`를 지정하세요 +@msg give `path`, `data`, `dataBase64` or `csv` `path`, `data`, `dataBase64` 또는 `csv`를 지정하세요 +@msg give `path`, `xml` or `dataBase64` `path`, `xml` 또는 `dataBase64`를 지정하세요 +@msg give `path`, an SVG file, or `svg`, SVG text SVG 파일인 `path` 또는 SVG 텍스트인 `svg`를 지정하세요 +@msg give `script`, the JavaScript the event runs ("" removes the event) 이벤트가 실행할 JavaScript인 `script`를 지정하세요(""는 이벤트를 제거합니다) +@msg give `text` (the reading) or `remove` `text`(읽는 법) 또는 `remove`를 지정하세요 +@msg give `x` and `y` (and a `tolerance` of 0 or more) `x`와 `y`를 지정하세요(`tolerance`는 0 이상) +@msg give `xml`, one SVG element SVG 요소 하나인 `xml`을 지정하세요 +@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments mode를 지정하거나 r/g/b, c/m/y/k, gray 또는 tint 조정값을 지정하세요 +@msg give a name, or paint the active proxy with a saved preset 이름을 지정하거나 활성 프록시를 저장된 사전 설정으로 칠하세요 +@msg give a name, or select a stroke with a saved profile 이름을 지정하거나 저장된 프로파일이 있는 선을 선택하세요 +@msg give a pattern name or select an object painted with a pattern 패턴 이름을 지정하거나 패턴으로 칠한 개체를 선택하세요 +@msg give at least one of clip, invert, disabled, linked clip, invert, disabled, linked 중 하나 이상을 지정하세요 +@msg give color, none, swatch or gradient color, none, swatch 또는 gradient를 지정하세요 +@msg give dataBase64 dataBase64를 지정하세요 +@msg give either at (a click) or area (a path drawn around the overlaps) at(클릭) 또는 area(겹친 부분 둘레에 그린 패스) 중 하나를 지정하세요 +@msg give exactly one of `at`, `rect` and `points` `at`, `rect`, `points` 중 정확히 하나를 지정하세요 +@msg give exactly one of `path`, `data` and `dataBase64` `path`, `data`, `dataBase64` 중 정확히 하나를 지정하세요 +@msg give formats or a preset, not both formats와 사전 설정 중 하나만 지정하세요 +@msg give index or indexes, not both index와 indexes 중 하나만 지정하세요 +@msg give newName or delete newName 또는 delete를 지정하세요 +@msg give offset or by offset 또는 by를 지정하세요 +@msg give one element 요소 하나를 지정하세요 +@msg give one of `to`, `rect` or `at` `to`, `rect`, `at` 중 하나를 지정하세요 +@msg give one of assets, layers, selection or fullDocument assets, layers, selection, fullDocument 중 하나를 지정하세요 +@msg give only one of onto, above, below, icon and a column place onto, above, below, icon, 열 위치 중 하나만 지정하세요 +@msg give opacity or blend opacity 또는 blend를 지정하세요 +@msg give origin: [x, y], or reset: true origin: [x, y] 또는 reset: true를 지정하세요 +@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note overprintFill, overprintStroke, showCenter, imageMap, url 또는 note를 지정하세요 +@msg give path (a file) or folder path(파일) 또는 folder를 지정하세요 +@msg give path, or name and dataBase64 path를 지정하거나 name과 dataBase64를 지정하세요 +@msg give paths or files paths 또는 files를 지정하세요 +@msg give rows, rowHeight, columns or columnWidth rows, rowHeight, columns 또는 columnWidth를 지정하세요 +@msg give svg, or dataBase64 of an SVG or SVGZ file svg를 지정하거나 SVG 또는 SVGZ 파일의 dataBase64를 지정하세요 +@msg give text text를 지정하세요 +@msg give the asset a name 에셋에 이름을 지정하세요 +@msg give the preset a `name` 사전 설정에 `name`을 지정하세요 +@msg give the preset a name 사전 설정에 이름을 지정하세요 +@msg give the preset a name (1–64 characters) 사전 설정에 이름을 지정하세요(1–64자) +@msg give the settings to change, e.g. {"formats"}]} 변경할 설정을 지정하세요(예: {"formats"}]}) +@msg give the style a name 스타일에 이름을 지정하세요 +@msg give two or more style `names` 스타일 `names`를 두 개 이상 지정하세요 +@msg give two or more swatches 견본을 두 개 이상 지정하세요 +@msg give x, y, or a handle's length or angle x, y 또는 핸들의 길이나 각도를 지정하세요 +@msg give {key, value} or {values} {key, value} 또는 {values}를 지정하세요 +@msg global must be true or false global은 true 또는 false여야 합니다 +@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) 글자의 스타일(예: 모두 대문자, 작은 대문자 또는 숫자 체계)에서는 {ch}에 글리프 {_1}을(를) 그리지 않습니다 +@msg glyph {g} is not an alternate of {ch} in {_1} {_2} 글리프 {g}은(는) {_1} {_2}에서 {ch}의 대체 글리프가 아닙니다 +@msg gradient must be "objects" or "mesh" gradient는 "objects" 또는 "mesh"여야 합니다 +@msg grays must be "profile" or "black", not `{g}` grays는 "profile" 또는 "black"이어야 합니다: `{g}` +@msg gridColors must be a preset name or two colours gridColors는 사전 설정 이름 또는 색상 두 개여야 합니다 +@msg gridSize must be small, medium or large gridSize는 small, medium 또는 large여야 합니다 +@msg guide {g} is not selected 안내선 {g}이(가) 선택되어 있지 않습니다 +@msg handle must be 0..3 handle은 0..3이어야 합니다 +@msg handler at {_1} expects {_2} values but its range starts with {d} {_1}의 처리기는 값 {_2}개가 필요하지만 그 범위는 {d}개로 시작합니다 +@msg handler depth too large 처리기 깊이가 너무 큽니다 +@msg handler range {_1}..{_2} out of range 처리기 범위 {_1}..{_2}이(가) 범위를 벗어났습니다 +@msg hiddenLayers must be true or false hiddenLayers는 true 또는 false여야 합니다 +@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v}: "layer"(기본값), "omit" 또는 "visible"이어야 합니다 +@msg highlight a layer to merge into 병합해 넣을 레이어를 강조 표시하세요 +@msg highlight two or more rows in one layer or group 한 레이어나 그룹에서 행을 두 개 이상 강조 표시하세요 +@msg highlight two or more rows to merge 병합할 행을 두 개 이상 강조 표시하세요 +@msg hvcC hvcC +@msg icon docks a panel in the icon column: use window.panel.dock icon은 패널을 아이콘 열에 도킹합니다: window.panel.dock을 사용하세요 +@msg id must be a dimension id id는 치수 id여야 합니다 +@msg image has no bounds 이미지에 경계가 없습니다 +@msg image {_1} is linked already 이미지 {_1}은(는) 이미 연결되어 있습니다 +@msg imageMap `{m}`: none, client or server imageMap `{m}`: none, client 또는 server여야 합니다 +@msg images in a spot ink whose samples couldn't be read were imported in RGB 샘플을 읽을 수 없는 별색 잉크 이미지는 RGB로 가져왔습니다 +@msg images past the most pixels one file may hold were left out 파일 하나에 담을 수 있는 최대 픽셀 수를 넘는 이미지는 제외되었습니다 +@msg images too large to read were left out 너무 커서 읽을 수 없는 이미지는 제외되었습니다 +@msg index out of range 인덱스가 범위를 벗어났습니다 +@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes에는 항목을 최대 {MAX_GUIDE_INDEXES}개까지 넣을 수 있습니다 +@msg indexes must be an array of guide indexes indexes는 안내선 인덱스의 배열이어야 합니다 +@msg instances must be 1..1000 instances는 1..1000이어야 합니다 +@msg instruction out of range 명령이 범위를 벗어났습니다 +@msg instruction {pc} ({op}) pops below the stack 명령 {pc}({op})이(가) 스택 바닥 아래에서 값을 꺼냅니다 +@msg instruction {pc} reached with stack depths {seen} and {d} 명령 {pc}에 스택 깊이 {seen}와(과) {d}(으)로 도달했습니다 +@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, anchor]여야 합니다 +@msg invalid angle 잘못된 각도 +@msg invalid array length 잘못된 배열 길이 +@msg invalid brush: {e} 잘못된 브러시: {e} +@msg invalid color 잘못된 색상 +@msg invalid colour {c} 잘못된 색상 {c} +@msg invalid delta 잘못된 델타 +@msg invalid group 잘못된 그룹 +@msg invalid object id {v}: expected a non-negative integer 잘못된 개체 id {v}: 음이 아닌 정수여야 합니다 +@msg invalid point 잘못된 점 +@msg invalid regular expression flag '{_1}' 잘못된 정규식 플래그 '{_1}' +@msg invalid regular expression: {m} 잘못된 정규식: {m} +@msg invalid size 잘못된 크기 +@msg invalid t 잘못된 t +@msg invalid time value 잘못된 시간 값 +@msg invalid tolerance 잘못된 허용치 +@msg is a confirm's answer, whose `value` must be true or false confirm의 응답이며 `value`은(는) true 또는 false여야 합니다 +@msg is a prompt's answer, whose `value` must be a string or null prompt의 응답이며 `value`은(는) 문자열 또는 null이어야 합니다 +@msg is an alert's answer, whose `value` must be "ok" alert의 응답이며 `value`은(는) "ok"여야 합니다 +@msg is longer than 16 KiB 16 KiB보다 깁니다 +@msg it draws its image through a clipping path 이미지를 클리핑 패스를 통해 그립니다 +@msg it draws no image 이미지를 그리지 않습니다 +@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes {MAX_DICTIONARY_FILE_BYTES}바이트보다 큽니다 +@msg it isn't a file 파일이 아닙니다 +@msg it names {_1} plates, and at most {MAX_PLATES} are read 색판 {_1}개를 지정하지만 최대 {MAX_PLATES}개까지만 읽습니다 +@msg its art is too long or too detailed for a head 아트웍이 화살촉으로 쓰기에는 너무 길거나 세밀합니다 +@msg its art paints nothing 아트웍이 아무것도 칠하지 않습니다 +@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) 아트보드가 모두 캔버스 밖에 있습니다(원점에서 {_1} pt 넘게 떨어져 있음) +@msg its clipping masks are nested too deeply 클리핑 마스크가 너무 깊게 중첩되어 있습니다 +@msg its image is too large to read 이미지가 너무 커서 읽을 수 없습니다 +@msg its metadata has no description 메타데이터에 설명이 없습니다 +@msg its metadata has no direct length 메타데이터에 직접 길이가 없습니다 +@msg its plates are too large: more than {MAX_SAMPLES} pixels together 색판이 너무 큽니다: 모두 합쳐 {MAX_SAMPLES} 픽셀을 넘습니다 +@msg its plates are too large: {_1} plates of {w} × {h} pixels 색판이 너무 큽니다: {w} × {h} 픽셀 색판 {_1}개 +@msg its plates can't be kept together 색판을 함께 보관할 수 없습니다 +@msg its shapes could not be combined into one 모양을 하나로 결합할 수 없습니다 +@msg join two different paths 서로 다른 두 패스를 연결하세요 +@msg jump to {pc}, past the end 끝을 지나 {pc}(으)로 점프합니다 +@msg justify must be auto|left|center|right|justifyAll justify는 auto|left|center|right|justifyAll이어야 합니다 +@msg kbps must be a positive number, not {kbps} kbps는 양수여야 합니다: {kbps} +@msg keywords must be a list of words or a comma-separated string keywords는 단어 목록이거나 쉼표로 구분한 문자열이어야 합니다 +@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords는 제어 문자 없이 최대 {MAX_KEYWORD}자여야 합니다 +@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind는 1, 2 또는 3이어야 합니다. 아니면 사전 설정 `name`을 지정하세요(perspective.presets.list 참조) +@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind는 graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle 또는 paraStyle이어야 합니다 +@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout은 "on", "off", "neutral" 또는 부울이어야 합니다: {v} +@msg layer `{name}` has no art to export 레이어 `{name}`에 내보낼 아트웍이 없습니다 +@msg layer must name a layer or sublayer layer에는 레이어나 하위 레이어를 지정해야 합니다 +@msg layers can't be intertwined: select the objects in them 레이어는 교차할 수 없습니다: 레이어 안의 개체를 선택하세요 +@msg layers go only at the top level or in layers, objects in layers and groups 레이어는 최상위나 레이어 안에만, 개체는 레이어와 그룹 안에만 둘 수 있습니다 +@msg layers must be true or false layers는 true 또는 false여야 합니다 +@msg layout must be gridByRow, gridByColumn, row or column layout은 gridByRow, gridByColumn, row 또는 column이어야 합니다 +@msg leading must be a number or "auto" leading은 숫자 또는 "auto"여야 합니다 +@msg left/right must be non-negative widths left/right는 음이 아닌 너비여야 합니다 +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding은 {_1} 중 하나여야 합니다 +@msg level `{t}`: 2 or 3 level `{t}`: 2 또는 3이어야 합니다 +@msg line {line}: more than {MAX_CSV_COLS} columns {line}행: 열이 {MAX_CSV_COLS}개보다 많습니다 +@msg line {line}: unclosed quote {line}행: 따옴표가 닫히지 않았습니다 +@msg line {line}: value for `{var}` must match its variable kind {line}행: `{var}`의 값은 변수 종류와 맞아야 합니다 +@msg lineEndings `{other}`: lf or crlf lineEndings `{other}`: lf 또는 crlf여야 합니다 +@msg loading profiles from disk isn't available in the browser 브라우저에서는 디스크에서 프로파일을 불러올 수 없습니다 +@msg localize: the first argument must be a localization object or a string localize: 첫 번째 인수는 지역화 개체 또는 문자열이어야 합니다 +@msg margin: a {m} pt margin leaves no imageable area on the paper margin: 여백이 {m} pt이면 용지에 인쇄 가능 영역이 남지 않습니다 +@msg matrix must be finite and invertible, depth finite matrix는 유한하고 역행렬이 있어야 하며 depth는 유한해야 합니다 +@msg matte: {e} matte: {e} +@msg member is for order front, forward, backward or back member는 order가 front, forward, backward 또는 back일 때 씁니다 +@msg missing `artboards` `artboards`가 없습니다 +@msg missing `at` [x, y] `at` [x, y]가 없습니다 +@msg missing `attrs` `attrs`가 없습니다 +@msg missing `color` or `opacity` `color` 또는 `opacity`가 없습니다 +@msg missing `colors` `colors`가 없습니다 +@msg missing `commands` `commands`가 없습니다 +@msg missing `destinationFolder` `destinationFolder`가 없습니다 +@msg missing `effect` id `effect` id가 없습니다 +@msg missing `family` `family`가 없습니다 +@msg missing `find` `find`가 없습니다 +@msg missing `gid` (a glyph id, or null for the default glyph) `gid`가 없습니다(글리프 id, 기본 글리프는 null) +@msg missing `group` `group`이 없습니다 +@msg missing `id` `id`가 없습니다 +@msg missing `id`: give one, or select one type object `id`가 없습니다: id를 지정하거나 문자 개체 하나를 선택하세요 +@msg missing `index` (a point index, or null to clear) `index`가 없습니다(점 인덱스, 지우려면 null) +@msg missing `index` (a stop index, or null to clear) `index`가 없습니다(정지점 인덱스, 지우려면 null) +@msg missing `index` (an item index or null) `index`가 없습니다(항목 인덱스 또는 null) +@msg missing `item` `item`이 없습니다 +@msg missing `library` `library`가 없습니다 +@msg missing `library` (see graphicStyle.libraries) `library`가 없습니다(graphicStyle.libraries 참조) +@msg missing `library` (see swatch.library.list) `library`가 없습니다(swatch.library.list 참조) +@msg missing `map` (rows [{from, to}] or {current}) `map`이 없습니다(행 [{from, to}] 또는 {current}) +@msg missing `matrix` [a,b,c,d,e,f] `matrix` [a,b,c,d,e,f]가 없습니다 +@msg missing `name` (see gradient.presets.list) `name`이 없습니다(gradient.presets.list 참조) +@msg missing `offset` (a distance in points) `offset`이 없습니다(포인트 단위 거리) +@msg missing `path` `path`가 없습니다 +@msg missing `path` (a path object id) `path`가 없습니다(패스 개체 id) +@msg missing `picks` `picks`가 없습니다 +@msg missing `rule` `rule`이 없습니다 +@msg missing `source` id `source` id가 없습니다 +@msg missing `start` (the byte offset of a character) `start`가 없습니다(글자의 바이트 오프셋) +@msg missing `status`: linked, embedded, missing or modified `status`가 없습니다: linked, embedded, missing 또는 modified +@msg missing `target` id `target` id가 없습니다 +@msg missing `text` `text`가 없습니다 +@msg missing `to` `to`가 없습니다 +@msg missing `to` (an index) `to`가 없습니다(인덱스) +@msg missing `to`: a non-negative integer `to`가 없습니다: 음이 아닌 정수 +@msg missing `type` `type`이 없습니다 +@msg missing `value` `value`가 없습니다 +@msg missing `word` `word`가 없습니다 +@msg missing `{key}` `{key}`이(가) 없습니다 +@msg missing `{k}` `{k}`이(가) 없습니다 +@msg missing anchor 앵커가 없습니다 +@msg missing anchors anchors가 없습니다 +@msg missing anchors [[subpath, anchor]…] anchors [[subpath, anchor]…]가 없습니다 +@msg missing array `{key}` 배열 `{key}`이(가) 없습니다 +@msg missing case case가 없습니다 +@msg missing color color가 없습니다 +@msg missing commands commands가 없습니다 +@msg missing destination folder 대상 폴더가 없습니다 +@msg missing from from이 없습니다 +@msg missing group group이 없습니다 +@msg missing id or ids id 또는 ids가 없습니다 +@msg missing ids ids가 없습니다 +@msg missing indexes indexes가 없습니다 +@msg missing matrix [a,b,c,d,e,f] matrix [a,b,c,d,e,f]가 없습니다 +@msg missing name name이 없습니다 +@msg missing op op가 없습니다 +@msg missing or invalid `color` `color`가 없거나 잘못되었습니다 +@msg missing or unknown `tool` `tool`이 없거나 알 수 없는 값입니다 +@msg missing point 점이 없습니다 +@msg missing point [x,y] 점 [x,y]가 없습니다 +@msg missing point `{key}` ([x, y]) 점 `{key}`([x, y])이(가) 없습니다 +@msg missing points points가 없습니다 +@msg missing points [[x,y]…] points [[x,y]…]가 없습니다 +@msg missing segment 세그먼트가 없습니다 +@msg missing stops stops가 없습니다 +@msg missing subpaths subpaths가 없습니다 +@msg missing target target이 없습니다 +@msg missing to to가 없습니다 +@msg missing x, y x, y가 없습니다 +@msg mode is set, add or toggle, not `{other}` mode는 set, add 또는 toggle입니다: `{other}` +@msg mode must be "cmyk" or "rgb" mode는 "cmyk" 또는 "rgb"여야 합니다 +@msg mode must be area|onPath, got {o} mode는 area|onPath여야 합니다({o} 입력됨) +@msg more than 10000 linked files: package dependencies in smaller parts 연결된 파일이 10000개보다 많습니다: 종속 파일을 더 작게 나누어 패키지하세요 +@msg more than 256 commands 명령이 256개보다 많습니다 +@msg more than {MAX_BATCH_FILES} data sets 데이터 세트가 {MAX_BATCH_FILES}개보다 많습니다 +@msg more than {MAX_BATCH_FILES} files 파일이 {MAX_BATCH_FILES}개보다 많습니다 +@msg more than {MAX_CSV_ROWS} data rows 데이터 행이 {MAX_CSV_ROWS}개보다 많습니다 +@msg more than {MAX_FILES} images 이미지가 {MAX_FILES}개보다 많습니다 +@msg more than {MAX_IMPORT} presets 사전 설정이 {MAX_IMPORT}개보다 많습니다 +@msg moved must be [[x,y]…] moved는 [[x,y]…]여야 합니다 +@msg must be an object with `kind` and `value` `kind`와 `value`가 있는 개체여야 합니다 +@msg must have `kind` alert, confirm or prompt `kind`가 alert, confirm 또는 prompt여야 합니다 +@msg must have exactly `kind` and `value` `kind`와 `value`만 있어야 합니다 +@msg name (a toolbar's name) is required name(도구 막대 이름)이 필요합니다 +@msg name (a toolbar's name) must be a string name(도구 막대 이름)은 문자열이어야 합니다 +@msg name must be a toolbar's name name은 도구 막대 이름이어야 합니다 +@msg names is empty names가 비어 있습니다 +@msg names must be an array of symbol names names는 심볼 이름의 배열이어야 합니다 +@msg need `anchor` or `segment` + `t` `anchor` 또는 `segment` + `t`가 필요합니다 +@msg need at least one anchor 앵커가 하나 이상 필요합니다 +@msg need at least one point 점이 하나 이상 필요합니다 +@msg need at least two points 점이 두 개 이상 필요합니다 +@msg needs a closing quote after the name 이름 뒤에 닫는 따옴표가 필요합니다 +@msg needs a name 이름이 필요합니다 +@msg needs a name of at most 64 bytes 최대 64바이트의 이름이 필요합니다 +@msg needs a name of letters, digits and _, or a quoted name 글자, 숫자, _로 이루어진 이름이나 따옴표로 묶은 이름이 필요합니다 +@msg needs a name without control characters 제어 문자가 없는 이름이 필요합니다 +@msg needs points points가 필요합니다 +@msg nesting too deep 중첩이 너무 깊습니다 +@msg nineSlice must be true or false nineSlice는 true 또는 false여야 합니다 +@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides는 [left, top, right, bottom]이어야 하고, 각 값은 -16..16이며 left + right < 1, top + bottom < 1이어야 합니다 +@msg no HP-GL format HP-GL 형식이 없습니다 +@msg no New Document profile called `{shown}` ({_1}) `{shown}`(이)라는 새 문서 프로파일이 없습니다({_1}) +@msg no PDF preset named `{n}` `{n}` PDF 사전 설정이 없습니다 +@msg no SVG filter at index {k} 인덱스 {k}에 SVG 필터가 없습니다 +@msg no action set is current 현재 액션 세트가 없습니다 +@msg no active data set 활성 데이터 세트가 없습니다 +@msg no appearance item {index} 모양새 항목 {index}이(가) 없습니다 +@msg no arrowheads file here 여기에는 화살촉 파일이 없습니다 +@msg no artboard {_1} 아트보드 {_1}이(가) 없습니다 +@msg no artboard {artboard} (0-based; the document has {_1}) 아트보드 {artboard}이(가) 없습니다(0부터 시작, 문서의 아트보드 수: {_1}) +@msg no asset {id} 에셋 {id}이(가) 없습니다 +@msg no asset {id} (see assets.list) 에셋 {id}이(가) 없습니다(assets.list 참조) +@msg no assets to export 내보낼 에셋이 없습니다 +@msg no assets yet: collect art for export first (assets.add) 아직 에셋이 없습니다: 먼저 내보낼 아트웍을 수집하세요(assets.add) +@msg no brush named `{n}` `{n}` 브러시가 없습니다 +@msg no character at byte {start} 바이트 {start}에 글자가 없습니다 +@msg no character has code {code} 코드가 {code}인 글자가 없습니다 +@msg no colour 색상이 없습니다 +@msg no colour group `{group}` 색상 그룹 `{group}`이(가) 없습니다 +@msg no colour group `{name}` 색상 그룹 `{name}`이(가) 없습니다 +@msg no custom arrowhead {name} (see stroke.arrowheads.list) 사용자 정의 화살촉 {name}이(가) 없습니다(stroke.arrowheads.list 참조) +@msg no data sets 데이터 세트가 없습니다 +@msg no dataset named `{from}` `{from}` 데이터 세트가 없습니다 +@msg no dataset named `{name}` `{name}` 데이터 세트가 없습니다 +@msg no datasets 데이터 세트가 없습니다 +@msg no design named `{name}` `{name}` 디자인이 없습니다 +@msg no design named `{n}` `{n}` 디자인이 없습니다 +@msg no design named `{old}` `{old}` 디자인이 없습니다 +@msg no encoder for {_1} yet 아직 {_1} 인코더가 없습니다 +@msg no export preset `{name}` (see document.exportPresets.list) 내보내기 사전 설정 `{name}`이(가) 없습니다(document.exportPresets.list 참조) +@msg no file to open 열 파일이 없습니다 +@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder 폴더가 없습니다: `path`를 전달하거나 환경 설정 › 플러그인 & 스크래치 디스크 › 추가 플러그인 폴더를 설정하세요 +@msg no fonts to look for: they are all available 찾을 글꼴이 없습니다: 모든 글꼴을 사용할 수 있습니다 +@msg no formats to export 내보낼 형식이 없습니다 +@msg no gradient preset `{name}` (see gradient.presets.list) 그레이디언트 사전 설정 `{name}`이(가) 없습니다(gradient.presets.list 참조) +@msg no graphic style `{n}` in `{_1}` `{_1}`에 그래픽 스타일 `{n}`이(가) 없습니다 +@msg no graphic style library `{key}` (see graphicStyle.libraries) 그래픽 스타일 라이브러리 `{key}`이(가) 없습니다(graphicStyle.libraries 참조) +@msg no guide selected 선택한 안내선이 없습니다 +@msg no guide selected (or give an index) 선택한 안내선이 없습니다(또는 인덱스를 지정하세요) +@msg no guide {i} 안내선 {i}이(가) 없습니다 +@msg no images in the folder 폴더에 이미지가 없습니다 +@msg no instructions 명령이 없습니다 +@msg no item at index {_1} 인덱스 {_1}에 항목이 없습니다 +@msg no item at index {from} 인덱스 {from}에 항목이 없습니다 +@msg no item at one of the indices {idx} 인덱스 {idx} 중 하나에 항목이 없습니다 +@msg no layer 레이어가 없습니다 +@msg no layer to rename 이름을 바꿀 레이어가 없습니다 +@msg no layer {id} (see layer.list) 레이어 {id}이(가) 없습니다(layer.list 참조) +@msg no layers to export 내보낼 레이어가 없습니다 +@msg no library `{key}` 라이브러리 `{key}`이(가) 없습니다 +@msg no metafile format 메타파일 형식이 없습니다 +@msg no object {_1} 개체 {_1}이(가) 없습니다 +@msg no objects 개체가 없습니다 +@msg no parent layer 상위 레이어가 없습니다 +@msg no perspective grid preset named `{b}` (see perspective.presets.list) `{b}` 원근 격자 사전 설정이 없습니다(perspective.presets.list 참조) +@msg no perspective grid preset named `{n}` `{n}` 원근 격자 사전 설정이 없습니다 +@msg no point selected (give `index`) 선택한 점이 없습니다(`index`를 지정하세요) +@msg no preset called `{from}` (see file.newPresets) `{from}` 사전 설정이 없습니다(file.newPresets 참조) +@msg no preset called `{name}` (see file.newPresets) `{name}` 사전 설정이 없습니다(file.newPresets 참조) +@msg no preset named `{n}` `{n}` 사전 설정이 없습니다 +@msg no print preset named `{b}` (see print.presets.list) `{b}` 인쇄 사전 설정이 없습니다(print.presets.list 참조) +@msg no print preset named `{name}` (presets: {_1}) `{name}` 인쇄 사전 설정이 없습니다(사전 설정: {_1}) +@msg no print preset named `{n}` `{n}` 인쇄 사전 설정이 없습니다 +@msg no profile named `{name}` `{name}` 프로파일이 없습니다 +@msg no random numbers ({e}) 난수를 얻을 수 없습니다({e}) +@msg no readable files in the folder 폴더에 읽을 수 있는 파일이 없습니다 +@msg no recovery copy `{f}` (see file.recovery.list) 복구 복사본 `{f}`이(가) 없습니다(file.recovery.list 참조) +@msg no rows 행이 없습니다 +@msg no saved PDF preset named `{name}` `{name}`(이)라는 저장된 PDF 사전 설정이 없습니다 +@msg no saved export preset `{name}` 저장된 내보내기 사전 설정 `{name}`이(가) 없습니다 +@msg no saved gradient preset called {name} {name}(이)라는 저장된 그레이디언트 사전 설정이 없습니다 +@msg no saved gradient presets 저장된 그레이디언트 사전 설정이 없습니다 +@msg no saved preset called `{name}` `{name}`(이)라는 저장된 사전 설정이 없습니다 +@msg no saved presets to export (name [Default] in `names` to export it) 내보낼 저장된 사전 설정이 없습니다([Default]를 내보내려면 `names`에 지정하세요) +@msg no saved presets to export (name built-in ones in `names`) 내보낼 저장된 사전 설정이 없습니다(기본 제공 사전 설정은 `names`에 지정하세요) +@msg no saved print preset named `{name}` `{name}`(이)라는 저장된 인쇄 사전 설정이 없습니다 +@msg no saved profile called {name} {name}(이)라는 저장된 프로파일이 없습니다 +@msg no saved selection `{name}` 저장된 선택 영역 `{name}`이(가) 없습니다 +@msg no saved theme called {name} (see colorTheme.list) {name}(이)라는 저장된 테마가 없습니다(colorTheme.list 참조) +@msg no saved views 저장된 보기가 없습니다 +@msg no selected or given object has a visible 3D effect 선택하거나 지정한 개체 중에 보이는 3D 효과가 있는 개체가 없습니다 +@msg no series {i} 계열 {i}이(가) 없습니다 +@msg no slice {n} in the image (see slice.list) 이미지에 슬라이스 {n}이(가) 없습니다(slice.list 참조) +@msg no source art 원본 아트웍이 없습니다 +@msg no stop {i} (the gradient has {n}) 정지점 {i}이(가) 없습니다(그레이디언트의 정지점 수: {n}) +@msg no style `{name}` 스타일 `{name}`이(가) 없습니다 +@msg no such artboard {v} 해당 아트보드가 없습니다: {v} +@msg no such assets (see assets.list) 해당 에셋이 없습니다(assets.list 참조) +@msg no such frame anchor, or the frame would reach past the canvas 해당 프레임 앵커가 없거나 프레임이 캔버스를 벗어납니다 +@msg no such objects 해당 개체가 없습니다 +@msg no such path anchor, or the path would reach past the canvas 해당 패스 앵커가 없거나 패스가 캔버스를 벗어납니다 +@msg no such rows 해당 행이 없습니다 +@msg no swatch `{keep}` 견본 `{keep}`이(가) 없습니다 +@msg no swatch `{n}` 견본 `{n}`이(가) 없습니다 +@msg no swatch `{n}` (swatches and colour groups move separately) 견본 `{n}`이(가) 없습니다(견본과 색상 그룹은 따로 이동합니다) +@msg no swatch library `{key}` (see swatch.library.list) 견본 라이브러리 `{key}`이(가) 없습니다(swatch.library.list 참조) +@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) 견본 라이브러리 `{key}`이(가) 없습니다(swatch.library.list 참조, "document": 문서의 견본) +@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) 견본 라이브러리 `{key}`이(가) 없습니다(swatch.library.list 참조, "{DOCUMENT_SWATCHES}": 문서의 견본) +@msg no swatch named `{n}` `{n}` 견본이 없습니다 +@msg no swatch or colour group `{n}` 견본 또는 색상 그룹 `{n}`이(가) 없습니다 +@msg no swatch or colour group `{n}` in `{_1}` `{_1}`에 견본 또는 색상 그룹 `{n}`이(가) 없습니다 +@msg no symbol named `{_1}` `{_1}` 심볼이 없습니다 +@msg no symbol named `{name}` `{name}` 심볼이 없습니다 +@msg no text objects selected 선택한 텍스트 개체가 없습니다 +@msg no toolbar named `{_1}` `{_1}` 도구 막대가 없습니다 +@msg no toolbar named `{name}` `{name}` 도구 막대가 없습니다 +@msg no tracing preset `{preset}` (see imageTrace.presets) 추적 사전 설정 `{preset}`이(가) 없습니다(imageTrace.presets 참조) +@msg no user library folder here (save with a path, or without one to get the data) 여기에는 사용자 라이브러리 폴더가 없습니다(경로를 지정해 저장하거나, 경로 없이 저장해 데이터를 받으세요) +@msg no variable named `{from}` `{from}` 변수가 없습니다 +@msg no variable named `{name}` `{name}` 변수가 없습니다 +@msg no variable named `{var}` `{var}` 변수가 없습니다 +@msg no variable named `{v}` `{v}` 변수가 없습니다 +@msg no view named `{name}` `{name}` 보기가 없습니다 +@msg node {_1} already has the text variable `{_2}` 노드 {_1}에 이미 텍스트 변수 `{_2}`이(가) 있습니다 +@msg node {_1} is not a graph 노드 {_1}은(는) 그래프가 아닙니다 +@msg node {_1} is not an image 노드 {_1}은(는) 이미지가 아닙니다 +@msg node {_1} is not type 노드 {_1}은(는) 문자가 아닙니다 +@msg not a DCS file DCS 파일이 아닙니다 +@msg not a PDF presets file (format `{_1}`) PDF 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a PDF presets file: {e} PDF 사전 설정 파일이 아닙니다: {e} +@msg not a RegExp object RegExp 개체가 아닙니다 +@msg not a Word document: its WordDocument stream doesn't start with a file information block Word 문서가 아닙니다: WordDocument 스트림이 파일 정보 블록으로 시작하지 않습니다 +@msg not a Word document: its compound file has no WordDocument stream Word 문서가 아닙니다: 복합 파일에 WordDocument 스트림이 없습니다 +@msg not a Word document: its main part is not a WordprocessingML document Word 문서가 아닙니다: 주 부분이 WordprocessingML 문서가 아닙니다 +@msg not a Word document: its package has no main document part Word 문서가 아닙니다: 패키지에 주 문서 부분이 없습니다 +@msg not a closed path 닫힌 패스가 아닙니다 +@msg not a flattener presets file (format `{_1}`) 병합 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a flattener presets file: {e} 병합 사전 설정 파일이 아닙니다: {e} +@msg not a layer or group 레이어나 그룹이 아닙니다 +@msg not a library file: {e} 라이브러리 파일이 아닙니다: {e} +@msg not a mesh envelope point 메시 엔벨로프 점이 아닙니다 +@msg not a perspective grid presets file (format `{_1}`) 원근 격자 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a perspective grid presets file: {e} 원근 격자 사전 설정 파일이 아닙니다: {e} +@msg not a print presets file (format `{_1}`) 인쇄 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a print presets file: {e} 인쇄 사전 설정 파일이 아닙니다: {e} +@msg not a regular expression 정규식이 아닙니다 +@msg not a stored or deflated central record 저장 방식이나 deflate 방식의 중앙 레코드가 아닙니다 +@msg not a valid SVGZ file: {e} 올바른 SVGZ 파일이 아닙니다: {e} +@msg not an RTF file: it doesn't start with {\rtf RTF 파일이 아닙니다: {\rtf(으)로 시작하지 않습니다 +@msg not an action set: the text doesn't start with /version and a number 액션 세트가 아닙니다: 텍스트가 /version과 숫자로 시작하지 않습니다 +@msg not an image VectorCraft reads (see document.formats) VectorCraft가 읽는 이미지가 아닙니다(document.formats 참조) +@msg not area type (text in a frame) outside perspective 원근 밖의 영역 문자(프레임 안의 텍스트)가 아닙니다 +@msg not editing an opacity mask 불투명 마스크를 편집하고 있지 않습니다 +@msg not type on a path outside perspective 원근 밖의 패스 위 문자가 아닙니다 +@msg nothing prints 인쇄할 것이 없습니다 +@msg nothing to bind 바인딩할 항목이 없습니다 +@msg nothing to change 변경할 항목이 없습니다 +@msg nothing to change: give dx, dy, hScale, vScale or rotation 변경할 항목이 없습니다: dx, dy, hScale, vScale 또는 rotation을 지정하세요 +@msg nothing to crop 자를 항목이 없습니다 +@msg nothing to export 내보낼 항목이 없습니다 +@msg nothing to export: the document has no visible art 내보낼 항목이 없습니다: 문서에 보이는 아트웍이 없습니다 +@msg nothing to mark 표시할 항목이 없습니다 +@msg nothing to print: every artboard picked is blank 인쇄할 것이 없습니다: 선택한 아트보드가 모두 비어 있습니다 +@msg nothing to print: the document has no art that prints 인쇄할 것이 없습니다: 문서에 인쇄되는 아트웍이 없습니다 +@msg nothing to repeat 반복할 항목이 없습니다 +@msg numbers out of order in {_1} quantifier {_1} 수량자의 숫자 순서가 잘못되었습니다 +@msg object is not a path 개체가 패스가 아닙니다 +@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object 개체 투명도는 강조 표시한 글자에 샘플링할 수 없습니다. 스포이드 옵션에서 투명도를 끄거나 개체 전체를 선택하세요 +@msg object {_1} can't be set as point type 개체 {_1}은(는) 포인트 문자로 설정할 수 없습니다 +@msg object {_1} has lost its file's contents: relink it 개체 {_1}이(가) 파일 내용을 잃었습니다: 다시 연결하세요 +@msg object {_1} has no path 개체 {_1}에 패스가 없습니다 +@msg object {_1} has no size 개체 {_1}에 크기가 없습니다 +@msg object {_1} is hidden or locked, or on a locked artboard 개체 {_1}이(가) 숨겨졌거나 잠겨 있거나 잠긴 아트보드에 있습니다 +@msg object {_1} is not a placed document 개체 {_1}은(는) 가져온 문서가 아닙니다 +@msg object {_1} is not an Intertwine group 개체 {_1}은(는) 교차 그룹이 아닙니다 +@msg object {_1} is not an image 개체 {_1}은(는) 이미지가 아닙니다 +@msg object {_1} is not an image or a linked file 개체 {_1}은(는) 이미지나 연결된 파일이 아닙니다 +@msg object {_1} is not an image or a placed document 개체 {_1}은(는) 이미지나 가져온 문서가 아닙니다 +@msg object {_1} is not in this Intertwine group 개체 {_1}은(는) 이 교차 그룹에 없습니다 +@msg object {_1} needs a 3D effect 개체 {_1}에는 3D 효과가 필요합니다 +@msg object {_1} needs a 3D effect at index {index} 개체 {_1}에는 인덱스 {index}에 3D 효과가 필요합니다 +@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic 개체 {_1}: 속이 빈 돌출 및 경사에는 캡이 없습니다: 그래픽을 매핑하려면 캡을 켜세요 +@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front 개체 {_1}: 깊이가 0이면 개체에 면이 하나뿐입니다: 그래픽을 앞면에 매핑하세요 +@msg object {_1}: {_2} 개체 {_1}: {_2} +@msg object {_1}: {e} 개체 {_1}: {e} +@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams 개체 {_1}: 인덱스 {index}의 {name}에는 조명 사전 설정이 없습니다. effect.setParams로 조명을 설정하세요 +@msg object {id} has anchors 0–{_1} for `corners`, not {past} 개체 {id}에서 `corners`에 쓸 수 있는 앵커는 0–{_1}입니다: {past} +@msg object {id} has {n} anchors, so no anchor {past} 개체 {id}에는 앵커가 {n}개이므로 앵커 {past}은(는) 없습니다 +@msg objects must be none, move or copy objects는 none, move 또는 copy여야 합니다 +@msg objects were left out 개체가 제외되었습니다 +@msg offset out of range 오프셋이 범위를 벗어났습니다 +@msg on must be true or false on은 true 또는 false여야 합니다 +@msg only groups, layers, type and images have a Contents (Characters) row 콘텐츠(글자) 행은 그룹, 레이어, 문자, 이미지에만 있습니다 +@msg only interior mesh lines can be deleted 안쪽 메시 선만 삭제할 수 있습니다 +@msg only solid colours can be added (not gradients or patterns) 단색만 추가할 수 있습니다(그레이디언트나 패턴은 안 됨) +@msg only solid-colour swatches can be spot colours 단색 견본만 별색이 될 수 있습니다 +@msg only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first 이 Affinity 파일은 포함된 미리 보기만 읽을 수 있었습니다: 파일 › 열기로 경고와 함께 확인하거나 먼저 Affinity에서 SVG나 PDF로 내보내세요 +@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning 이 Affinity 파일은 포함된 미리 보기만 읽을 수 있었습니다. 파일 › 열기로 경고와 함께 확인하세요 +@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon, 열 위치는 window.panel.float와 window.panel.dock에 씁니다 +@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon, 열 위치는 anchor, side, zone, before와 함께 쓸 수 없습니다 +@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon, 열 위치는 x, y, width, height와 함께 쓸 수 없습니다 +@msg opacity must be a number opacity는 숫자여야 합니다 +@msg open a document to look for its linked files 연결된 파일을 찾으려면 문서를 여세요 +@msg operand too large 피연산자가 너무 큽니다 +@msg options must be an object (see file.formatOptions) options는 개체여야 합니다(file.formatOptions 참조) +@msg order is a whole number from 0 to {others}, not `{v}` order는 0에서 {others} 사이의 정수입니다: `{v}` +@msg order must be leftToRight or rightToLeft order는 leftToRight 또는 rightToLeft여야 합니다 +@msg orientation must be portrait or landscape, not `{o}` orientation은 portrait 또는 landscape여야 합니다: `{o}` +@msg outlineText {v}: true or false outlineText {v}: true 또는 false여야 합니다 +@msg output intent profile: {e} 출력 의도 프로파일: {e} +@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination: “{name}”(이)라는 RGB 또는 CMYK 프로파일이 없습니다 +@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination: {_1} 파일은 CMYK인데 “{name}”은(는) RGB 프로파일입니다 +@msg output.destination: {e} output.destination: {e} +@msg output.inks: no ink is set to print output.inks: 인쇄하도록 설정된 잉크가 없습니다 +@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent: “{name}”(이)라는 프로파일이 없으며, {std} 출력 의도는 프로파일을 포함해야 합니다 +@msg output.outputIntent: no profile is called “{name}”: a {std} output intent embeds a profile, or names a registered printing condition (outputConditionId and registry) output.outputIntent: “{name}”(이)라는 프로파일이 없습니다: {std} 출력 의도는 프로파일을 포함하거나 등록된 인쇄 조건(outputConditionId와 registry)을 지정합니다 +@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent: {std} 파일은 CMYK로 인쇄하는데 “{name}”은(는) RGB 프로파일입니다 +@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent: “{name}”은(는) 회색 프로파일이어서 {std} 출력 의도로 쓸 수 없습니다 +@msg overlap: {o} pt is half a tile or more overlap: {o} pt는 타일의 절반 이상입니다 +@msg overlapping Shaper paths must be in the same layer or group 겹치는 셰이퍼 패스는 같은 레이어나 그룹에 있어야 합니다 +@msg overlapping edits 편집 범위가 겹칩니다 +@msg page art conflicts with image data for {key} 페이지 아트웍이 {key}의 이미지 데이터와 충돌합니다 +@msg page art conflicts with pattern {name} 페이지 아트웍이 패턴 {name}와(과) 충돌합니다 +@msg page art conflicts with swatch {name} 페이지 아트웍이 견본 {name}와(과) 충돌합니다 +@msg page art is missing image data for {key} 페이지 아트웍에 {key}의 이미지 데이터가 없습니다 +@msg page art is missing pattern {name} 페이지 아트웍에 패턴 {name}이(가) 없습니다 +@msg page art is missing swatch {name} 페이지 아트웍에 견본 {name}이(가) 없습니다 +@msg page must be a whole number from 1 page는 1 이상의 정수여야 합니다 +@msg page thumbnail: {e} 페이지 미리 보기 이미지: {e} +@msg page {page} of the PDF has no objects PDF의 {page}페이지에 개체가 없습니다 +@msg page {page}: `{file}` has {n} {artboards} {page}페이지: `{file}`에는 {artboards}이(가) {n}개 있습니다 +@msg page {page}: `{name}` has {n} {pages} {page}페이지: `{name}`에는 {pages}이(가) {n}개 있습니다 +@msg pages must be a range such as "2-3" or a page number, not {v} pages는 "2-3" 같은 범위 또는 페이지 번호여야 합니다: {v} +@msg pages: {e} pages: {e} +@msg panel position must be finite 패널 위치는 유한해야 합니다 +@msg parameters must be an object 매개변수는 개체여야 합니다 +@msg params must be an object params는 개체여야 합니다 +@msg passwords can have up to {MAX_PASSWORD} bytes 암호는 최대 {MAX_PASSWORD}바이트까지 쓸 수 있습니다 +@msg path chunks must share their boundary point 패스 조각은 경계 점을 공유해야 합니다 +@msg path has no open end 패스에 열린 끝이 없습니다 +@msg paths must be strings paths는 문자열이어야 합니다 +@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false를 쓰려면 편집 가능한 아트웍을 보존하도록 모든 아트보드를 포함해야 합니다 +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false useArtboards가 false일 때 pdfCompatible: false를 쓰려면 편집 가능한 아트웍을 보존하도록 아트보드를 포함해야 합니다 +@msg pdfs `{m}`: single or multiple pdfs `{m}`: single 또는 multiple이어야 합니다 +@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom은 "artwork" 또는 "swatches"입니다: {v} +@msg pins and moved must be non-empty lists of the same length pins와 moved는 길이가 같고 비어 있지 않은 목록이어야 합니다 +@msg pins must be [[x,y]…] pins는 [[x,y]…]여야 합니다 +@msg pins must be finite pins는 유한해야 합니다 +@msg place is above, below or inside, not `{other}` place는 above, below 또는 inside입니다: `{other}` +@msg plane must be left|right|ground plane은 left|right|ground여야 합니다 +@msg plane must be left|right|ground|none plane은 left|right|ground|none이어야 합니다 +@msg point is outside the mesh 점이 메시 밖에 있습니다 +@msg point x must be a number 점의 x는 숫자여야 합니다 +@msg point y must be a number 점의 y는 숫자여야 합니다 +@msg points must be [t, left, right] with non-negative widths points는 너비가 음이 아닌 [t, left, right]여야 합니다 +@msg points must be [x,y] pairs points는 [x,y] 쌍이어야 합니다 +@msg points must be a non-empty [[x,y,pressure?]…] list points는 비어 있지 않은 [[x,y,pressure?]…] 목록이어야 합니다 +@msg points must be a non-empty list of finite [x,y] points는 유한한 [x,y]의 비어 있지 않은 목록이어야 합니다 +@msg points must be an array or null points는 배열 또는 null이어야 합니다 +@msg points must be finite points는 유한해야 합니다 +@msg points must be finite [x,y] pairs points는 유한한 [x,y] 쌍이어야 합니다 +@msg points must be finite canvas points points는 유한한 캔버스 점이어야 합니다 +@msg points must contain 1–4096 coordinates points에는 좌표가 1–4096개 있어야 합니다 +@msg position must be normal, superscript or subscript position은 normal, superscript 또는 subscript여야 합니다 +@msg position must be topLeft, topRight, bottomLeft or bottomRight position은 topLeft, topRight, bottomLeft 또는 bottomRight여야 합니다 +@msg positions must be between 0 and 100000 pt positions는 0에서 100000 pt 사이여야 합니다 +@msg ppi must be a positive number, not {ppi} ppi는 양수여야 합니다: {ppi} +@msg ppi must be between 1 and 2400 ppi는 1에서 2400 사이여야 합니다 +@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}`은(는) transforms|bounds|fileDimensions|fit|fill이 아닙니다 +@msg preserveShape must be clippingMask|transparency preserveShape는 clippingMask|transparency여야 합니다 +@msg preset `{id}`: mobile or density preset `{id}`: mobile 또는 density여야 합니다 +@msg preset must be standard, diffuse, topLeft or right preset은 standard, diffuse, topLeft 또는 right여야 합니다 +@msg preset must be the name of a print preset preset은 인쇄 사전 설정의 이름이어야 합니다 +@msg previewMode must be default, pixel or overprint previewMode는 default, pixel 또는 overprint여야 합니다 +@msg print settings: {e} 인쇄 설정: {e} +@msg procedures nest too deeply 프로시저가 너무 깊게 중첩되어 있습니다 +@msg procedures nested too deeply 프로시저가 너무 깊게 중첩되었습니다 +@msg quotes must be {double?, single?} quotes는 {double?, single?}여야 합니다 +@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k}은(는) 여는 따옴표와 닫는 따옴표여야 합니다(예: "“”") +@msg range out of order in character class 문자 클래스의 범위 순서가 잘못되었습니다 +@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi는 1–2400이어야 합니다(72 screen, 150 medium, 300 high) +@msg rating must be a whole number from 0 to {MAX_RATING} rating은 0에서 {MAX_RATING} 사이의 정수여야 합니다 +@msg rect and {k} contradict each other: give the area or the artboards rect와 {k}이(가) 서로 맞지 않습니다: 영역이나 아트보드 중 하나를 지정하세요 +@msg rect applies to raster formats, not {_1} rect는 래스터 형식에 적용되며 {_1}에는 적용되지 않습니다 +@msg rect is [x, y, width, height] in points rect는 포인트 단위의 [x, y, width, height]입니다 +@msg rect must be [x, y, width, height] rect는 [x, y, width, height]여야 합니다 +@msg rect must be [x, y, width, height] with a positive width and height rect는 너비와 높이가 양수인 [x, y, width, height]여야 합니다 +@msg rect needs finite numbers and a positive width and height rect에는 유한한 숫자와 양수인 너비와 높이가 필요합니다 +@msg redraw needs id redraw에는 id가 필요합니다 +@msg redraw within must be a finite distance of 0 or more redraw의 within은 0 이상의 유한한 거리여야 합니다 +@msg reduce of an empty array with no initial value 초기값 없이 빈 배열에 reduce를 호출했습니다 +@msg registration must be 0..8 registration은 0..8이어야 합니다 +@msg regular expression flag '{_1}' given twice 정규식 플래그 '{_1}'을(를) 두 번 지정했습니다 +@msg release a layer or a group 레이어나 그룹을 해제하세요 +@msg replace keeps the replaced object's place and transform: drop template, at and rect replace는 바꾼 개체의 위치와 변형을 유지합니다: template, at, rect를 빼세요 +@msg replace: select the one object to replace replace: 바꿀 개체 하나를 선택하세요 +@msg resolution must be a number or screen|medium|high resolution은 숫자 또는 screen|medium|high여야 합니다 +@msg resolution must be between 1 and 2400 ppi resolution은 1에서 2400 ppi 사이여야 합니다 +@msg rotation must be a number of degrees rotation은 도 단위의 숫자여야 합니다 +@msg rotation must be finite rotation은 유한해야 합니다 +@msg rows and cols must be 1..50 rows와 cols는 1..50이어야 합니다 +@msg rows and columns must be 1..500 rows와 columns는 1..500이어야 합니다 +@msg rows of objects whose appearances differ can't be reordered together 모양새가 서로 다른 개체의 행은 함께 순서를 바꿀 수 없습니다 +@msg sampled text would exceed the canvas (coordinates out of range) 샘플링한 텍스트가 캔버스를 벗어납니다(좌표 범위 초과) +@msg save the document first: Package collects a saved document 먼저 문서를 저장하세요: 패키지는 저장된 문서를 수집합니다 +@msg save the document first: copy puts the file in its folder 먼저 문서를 저장하세요: copy는 파일을 문서의 폴더에 넣습니다 +@msg savedPreset: {e} savedPreset: {e} +@msg scale `{s}`: a factor such as 2x, a width (100w), a height (100h) or a resolution (72ppi) scale `{s}`: 2x 같은 배율, 너비(100w), 높이(100h) 또는 해상도(72ppi)여야 합니다 +@msg scale must be a number above 0, at most 64 scale은 0보다 크고 64 이하인 숫자여야 합니다 +@msg scale must be non-zero scale은 0이 아니어야 합니다 +@msg scope must be selection or all (got {other}) scope는 selection 또는 all이어야 합니다({other} 입력됨) +@msg scribble is too long 스크리블이 너무 깁니다 +@msg segment does not exist 세그먼트가 없습니다 +@msg select a Shaper group 셰이퍼 그룹을 선택하세요 +@msg select a blend to set easing or color shift 이징이나 색상 이동을 설정할 블렌드를 선택하세요 +@msg select a compound shape 컴파운드 모양을 선택하세요 +@msg select a graph 그래프를 선택하세요 +@msg select a group or layer with visible, unlocked members 보이고 잠기지 않은 구성원이 있는 그룹이나 레이어를 선택하세요 +@msg select a layer 레이어를 선택하세요 +@msg select a text object 텍스트 개체를 선택하세요 +@msg select an SVG filter SVG 필터를 선택하세요 +@msg select an image (or an Image Trace group) 이미지(또는 이미지 추적 그룹)를 선택하세요 +@msg select an image, or pass id 이미지를 선택하거나 id를 전달하세요 +@msg select an object (or pass `id`) 개체를 선택하세요(또는 `id`를 전달하세요) +@msg select an object with a 3D effect 3D 효과가 있는 개체를 선택하세요 +@msg select an object with a 3D effect, or give its id in ids 3D 효과가 있는 개체를 선택하거나 ids에 그 id를 지정하세요 +@msg select an object with a freeform gradient 자유 형태 그레이디언트가 있는 개체를 선택하세요 +@msg select an object with an opacity mask 불투명 마스크가 있는 개체를 선택하세요 +@msg select area type 영역 문자를 선택하세요 +@msg select area type (text in a frame) 영역 문자(프레임 안의 텍스트)를 선택하세요 +@msg select area type and closed paths (at least one area type object) 영역 문자와 닫힌 패스를 선택하세요(영역 문자 개체가 하나 이상 있어야 함) +@msg select area type whose first line can fit its frame 첫 줄이 프레임에 들어가는 영역 문자를 선택하세요 +@msg select artwork to add its colours 색상을 추가할 아트웍을 선택하세요 +@msg select artwork to make a group of its colours 색상으로 그룹을 만들 아트웍을 선택하세요 +@msg select artwork to recolour, or give a colour `group` 색상을 재지정할 아트웍을 선택하거나 색상 `group`을 지정하세요 +@msg select exactly two path ends to join 연결할 패스 끝을 정확히 두 개 선택하세요 +@msg select members of a compound shape 컴파운드 모양의 구성원을 선택하세요 +@msg select objects or give ids 개체를 선택하거나 ids를 지정하세요 +@msg select objects to align to the guide 안내선에 정렬할 개체를 선택하세요 +@msg select one Intertwine group 교차 그룹 하나를 선택하세요 +@msg select one image or pass id 이미지 하나를 선택하거나 id를 전달하세요 +@msg select one layer, group or object to rename 이름을 바꿀 레이어, 그룹 또는 개체를 하나 선택하세요 +@msg select paths 패스를 선택하세요 +@msg select paths or compound paths 패스나 컴파운드 패스를 선택하세요 +@msg select rows or objects to collect 수집할 행이나 개체를 선택하세요 +@msg select something to export 내보낼 항목을 선택하세요 +@msg select something to export (or turn off selectedOnly) 내보낼 항목을 선택하세요(또는 selectedOnly를 끄세요) +@msg select text to redefine the style from 스타일을 재정의할 기준 텍스트를 선택하세요 +@msg select the art and, on top of it, the mask object 아트웍과 그 위에 있는 마스크 개체를 선택하세요 +@msg select the art for the design 디자인에 쓸 아트웍을 선택하세요 +@msg select the art for the pattern tile 패턴 타일에 쓸 아트웍을 선택하세요 +@msg select the art to add first 먼저 추가할 아트웍을 선택하세요 +@msg select the art to collect (or give its ids) 수집할 아트웍을 선택하세요(또는 ids를 지정하세요) +@msg select the art to weed 위딩할 아트웍을 선택하세요 +@msg select the banner, or mark every artboard 배너를 선택하거나 모든 아트보드에 표시하세요 +@msg select the content and a path on top 콘텐츠와 그 위의 패스를 선택하세요 +@msg select the images to relink, or pass ids 다시 연결할 이미지를 선택하거나 ids를 전달하세요 +@msg select the images, or pass ids 이미지를 선택하거나 ids를 전달하세요 +@msg select the linked images or placed documents, or pass ids 연결된 이미지나 가져온 문서를 선택하거나 ids를 전달하세요 +@msg select the object to copy from 복사해 올 개체를 선택하세요 +@msg select the objects (or give their ids) 개체를 선택하세요(또는 ids를 지정하세요) +@msg select the objects to attach to the path 패스에 부착할 개체를 선택하세요 +@msg select the objects to bind 바인딩할 개체를 선택하세요 +@msg select two or more objects to intertwine 교차할 개체를 두 개 이상 선택하세요 +@msg select two or more slices to combine 결합할 슬라이스를 두 개 이상 선택하세요 +@msg select type on a path 패스 위 문자를 선택하세요 +@msg select type on a path or area type 패스 위 문자 또는 영역 문자를 선택하세요 +@msg selection `{m}`: one or each selection `{m}`: one 또는 each여야 합니다 +@msg settings is an object of document.exportForScreens params settings는 document.exportForScreens 매개변수의 개체입니다 +@msg settings must be an object settings는 개체여야 합니다 +@msg show `{show}`: all, missing, modified or embedded show `{show}`: all, missing, modified 또는 embedded여야 합니다 +@msg show must be true or false show는 true 또는 false여야 합니다 +@msg side is left or right, not `{v}` side는 left 또는 right입니다: `{v}` +@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size는 "small", "medium", "large"이거나 0보다 크고 {_1} 이하인 포인트 값이어야 합니다 +@msg size must be positive size는 양수여야 합니다 +@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode는 "fixed" 또는 "pressure"입니다: `{other}` +@msg sizeMode is "fixed" or "pressure", not {other} sizeMode는 "fixed" 또는 "pressure"입니다: {other} +@msg sizeVariation must be 0 or more sizeVariation은 0 이상이어야 합니다 +@msg smallCaps must be true or false smallCaps는 true 또는 false여야 합니다 +@msg sort `{x}`: name, kind or status sort `{x}`: name, kind 또는 status여야 합니다 +@msg source paths are too large for Shaper 원본 패스가 셰이퍼에 쓰기에는 너무 큽니다 +@msg source text transform is collapsed or unrepresentable 원본 텍스트 변형이 축소되었거나 나타낼 수 없습니다 +@msg spot, mode and group apply to solid colours only spot, mode, group은 단색에만 적용됩니다 +@msg status `{s}`: linked, embedded, missing or modified status `{s}`: linked, embedded, missing 또는 modified여야 합니다 +@msg steps must be 1..1000 steps는 1..1000이어야 합니다 +@msg steps must be 1..{MAX_STEPS} steps는 1..{MAX_STEPS}이어야 합니다 +@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) 정지점 {i}: `plates`은(는) 0에서 1 사이의 숫자 네 개(C, M, Y, K)여야 합니다 +@msg stopped before the archive was finished 아카이브를 끝내기 전에 중지되었습니다 +@msg strokeMode must be within|along|across, got {m} strokeMode는 within|along|across여야 합니다({m} 입력됨) +@msg strokeOptions must be an object of stroke.set options strokeOptions는 stroke.set 옵션의 개체여야 합니다 +@msg strokeWidth must be a positive number of pixels strokeWidth는 픽셀 단위의 양수여야 합니다 +@msg subpath {si} is not an open subpath 하위 패스 {si}은(는) 열린 하위 패스가 아닙니다 +@msg subpaths required when joining the same path 같은 패스를 연결할 때는 subpaths가 필요합니다 +@msg swatchConflict must be "merge" or "add", not {v} swatchConflict는 "merge" 또는 "add"여야 합니다: {v} +@msg table must be true or false table은 true 또는 false여야 합니다 +@msg takes one name 이름 하나만 지정할 수 있습니다 +@msg target text transform is collapsed or unrepresentable 대상 텍스트 변형이 축소되었거나 나타낼 수 없습니다 +@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text는 {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?}여야 합니다 +@msg text transform is collapsed or unrepresentable 텍스트 변형이 축소되었거나 나타낼 수 없습니다 +@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces는 2에서 100 사이의 정수여야 합니다 +@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}`: "{_1}" 또는 "{_2}"을(를) 사용하세요 +@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage: 이미지 파일의 {path} 또는 {name, dataBase64}를 지정하거나 effect.list가 제공하는 텍스처를 지정하세요 +@msg textureImage: {e} textureImage: {e} +@msg that makes more than {MAX_PIECES} slices 그러면 슬라이스가 {MAX_PIECES}개보다 많아집니다 +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer 그러면 조각(최대 100000개)이나 대시 단계가 너무 많아집니다: 대시 패턴을 단순하게 하거나 대시를 더 길게 하세요 +@msg that object is not in the overlap 그 개체는 겹친 부분에 없습니다 +@msg the Affinity document has no page with a size Affinity 문서에 크기가 있는 페이지가 없습니다 +@msg the CMYK image couldn't be written CMYK 이미지를 기록할 수 없습니다 +@msg the Layers panel is not shown 레이어 패널이 표시되어 있지 않습니다 +@msg the Layers panel's search, filter or Show Layers Only hides its row 레이어 패널의 검색, 필터 또는 레이어만 표시 때문에 해당 행이 숨겨져 있습니다 +@msg the Objects on Path group is hidden or locked 패스 위 개체 그룹이 숨겨졌거나 잠겨 있습니다 +@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected PDF 보안 설정이 암호 없이 저장되었습니다: openPassword 또는 permissionsPassword를 다시 지정하거나, 보호 없이 내보내려면 passwordRequired가 없는 security를 지정하세요 +@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) RTF 파일에 단락이나 서식 변경이 너무 많아 문자로 가져올 수 없습니다(최대 단락 {MAX_PARAGRAPHS}개, 런 {MAX_RUNS}개) +@msg the RTF file is too large to place as type (at most {_1} MB) RTF 파일이 너무 커서 문자로 가져올 수 없습니다(최대 {_1} MB) +@msg the SVG expands to more than {0} nested SVG images SVG를 확장하면 중첩된 SVG 이미지가 {0}개보다 많아집니다 +@msg the SVG expands to more than {0} objects SVG를 확장하면 개체가 {0}개보다 많아집니다 +@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes SVG 파일이 {MAX_IMPORT_BYTES}바이트보다 큽니다 +@msg the SVG has no drawable objects SVG에 그릴 수 있는 개체가 없습니다 +@msg the SVGZ file unpacks to more than {_1} MB SVGZ 파일의 압축을 풀면 {_1} MB를 넘습니다 +@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place Word 문서의 속성이 {MAX_PAGES}페이지보다 많아 가져올 수 없습니다 +@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place Word 문서에 문자 또는 단락 속성의 런이 {MAX_RUNS}개보다 많아 가져올 수 없습니다 +@msg the Word document has more than {_1} MB of formatting properties, too many to place Word 문서의 서식 속성이 {_1} MB보다 많아 가져올 수 없습니다 +@msg the Word document has too much text or formatting to place as type (at most {MAX_PARAGRAPHS} paragraphs, {MAX_RUNS} runs and {_1} MB of text) Word 문서에 텍스트나 서식이 너무 많아 문자로 가져올 수 없습니다(최대 단락 {MAX_PARAGRAPHS}개, 런 {MAX_RUNS}개, 텍스트 {_1} MB) +@msg the Word document has too much text to place as type (at most {_1} MB) Word 문서에 텍스트가 너무 많아 문자로 가져올 수 없습니다(최대 {_1} MB) +@msg the Word document is encrypted or protected with a password; save it without one to place it Word 문서가 암호화되었거나 암호로 보호되어 있습니다. 가져오려면 암호 없이 저장하세요 +@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) Word 문서가 너무 커서 가져올 수 없습니다({name} 스트림이 {_1} MB를 넘습니다) +@msg the Word document was last saved with fast save, which VectorCraft doesn't read; save it again with fast save turned off Word 문서가 마지막에 빠른 저장으로 저장되었는데, VectorCraft는 이 방식을 읽지 않습니다. 빠른 저장을 끄고 다시 저장하세요 +@msg the Word document was saved by Word 6.0 or Word 95, which VectorCraft doesn't read; save it as a Word 97-2003 document Word 문서가 Word 6.0 또는 Word 95에서 저장되었는데, VectorCraft는 이 형식을 읽지 않습니다. Word 97-2003 문서로 저장하세요 +@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place Word 문서의 텍스트가 {MAX_PIECES}개보다 많은 조각으로 이루어져 있어 가져올 수 없습니다 +@msg the XML operation needs an XML or XMLList value XML 연산에는 XML 또는 XMLList 값이 필요합니다 +@msg the XPath expression does not evaluate to a node-set XPath 식의 결과가 노드 집합이 아닙니다 +@msg the XPath expression ends early XPath 식이 중간에 끝났습니다 +@msg the XPath expression has an unmatched ) XPath 식에 짝이 없는 )가 있습니다 +@msg the XPath expression has an unmatched ] XPath 식에 짝이 없는 ]가 있습니다 +@msg the XPath expression has an unmatched closing bracket XPath 식에 짝이 없는 닫는 괄호가 있습니다 +@msg the XPath expression selects a namespace node, which is not an XML value XPath 식이 XML 값이 아닌 네임스페이스 노드를 선택합니다 +@msg the XPath expression selects the root node, which is not an XML value XPath 식이 XML 값이 아닌 루트 노드를 선택합니다 +@msg the XPath function {name} needs a node-set XPath 함수 {name}에는 노드 집합이 필요합니다 +@msg the XPath function {name} takes the wrong number of arguments XPath 함수 {name}의 인수 개수가 맞지 않습니다 +@msg the XPath name functions need a node-set XPath 이름 함수에는 노드 집합이 필요합니다 +@msg the XPath prefix {shown} is not bound to a namespace XPath 접두사 {shown}이(가) 네임스페이스에 바인딩되어 있지 않습니다 +@msg the XPath variable ${shown} is not bound XPath 변수 ${shown}이(가) 바인딩되어 있지 않습니다 +@msg the action set ids are not a list 액션 세트 id가 목록이 아닙니다 +@msg the active paint is not a gradient 활성 페인트가 그레이디언트가 아닙니다 +@msg the argument list is not an object 인수 목록이 개체가 아닙니다 +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size 아트보드가 캔버스에 들어가지 않습니다: 아트보드 수나 간격, 크기를 줄이세요 +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out 아트웍이 EPS에 쓰기에는 너무 큽니다: PostScript 256 MB 이후의 내용은 제외되었습니다 +@msg the banner has no width or height 배너에 너비나 높이가 없습니다 +@msg the bitmaps hold more pixels than one file may 비트맵의 픽셀 수가 파일 하나에 담을 수 있는 양보다 많습니다 +@msg the character's size can't be measured 글자의 크기를 잴 수 없습니다 +@msg the code runs past its last instruction 코드가 마지막 명령을 지나 실행됩니다 +@msg the compound shape is too complex to expand 컴파운드 모양이 너무 복잡해서 확장할 수 없습니다 +@msg the container of the listed objects is gone 나열한 개체의 컨테이너가 없어졌습니다 +@msg the copy `{user_id}` can't be read as a library 복사본 `{user_id}`을(를) 라이브러리로 읽을 수 없습니다 +@msg the copy would leave the canvas 복사본이 캔버스를 벗어납니다 +@msg the curve tolerance must be from 0.01 to 10 points 곡선 허용치는 0.01에서 10포인트 사이여야 합니다 +@msg the custom dictionary couldn't be saved to {_1}: {e} 사용자 사전을 {_1}에 저장할 수 없습니다: {e} +@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is 사용자 사전 {_1}을(를) 읽을 수 없어({why}) 그대로 둡니다 +@msg the cut cannot be made on this art: {e} 이 아트웍은 자를 수 없습니다: {e} +@msg the data isn't an EMF or WMF picture 데이터가 EMF 또는 WMF 그림이 아닙니다 +@msg the data isn't an EPS or PostScript picture 데이터가 EPS 또는 PostScript 그림이 아닙니다 +@msg the deferred paste is no longer available 지연된 붙여넣기를 더 이상 사용할 수 없습니다 +@msg the design is too big to draw at every data point of this graph 디자인이 너무 커서 이 그래프의 모든 데이터 점에 그릴 수 없습니다 +@msg the design is too big to draw in every column of this graph 디자인이 너무 커서 이 그래프의 모든 막대에 그릴 수 없습니다 +@msg the dialog it was opened over is no longer open 이 대화 상자가 열렸던 바탕 대화 상자가 더 이상 열려 있지 않습니다 +@msg the dictionary stack overflowed 사전 스택이 오버플로되었습니다 +@msg the document already has an SVG filter named {name} 문서에 {name}(이)라는 SVG 필터가 이미 있습니다 +@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it 문서에 {name}(이)라는 다른 SVG 필터가 있습니다: svgFilter.apply를 사용하거나, 편집하려면 `replace`와 함께 svgFilter.define을 사용하세요 +@msg the document has no SVG filter named {name} 문서에 {name}(이)라는 SVG 필터가 없습니다 +@msg the document has no SVG filter named {r} 문서에 {r}(이)라는 SVG 필터가 없습니다 +@msg the document has no color swatches 문서에 색상 견본이 없습니다 +@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 문서의 견본에 색상이 {_1}개 있습니다. document 감소는 최대 255개까지 쓸 수 있습니다 +@msg the editing data's hash has no place 편집 데이터의 해시를 넣을 자리가 없습니다 +@msg the entities the document declares expand to too much text 문서가 선언한 엔티티를 확장하면 텍스트가 너무 많아집니다 +@msg the envelope's content has no bounds 엔벨로프의 콘텐츠에 경계가 없습니다 +@msg the envelope's mesh is malformed 엔벨로프의 메시가 올바르지 않습니다 +@msg the file holds no `variableSets` element 파일에 `variableSets` 요소가 없습니다 +@msg the file is larger than {_1} MB 파일이 {_1} MB보다 큽니다 +@msg the file is too large 파일이 너무 큽니다 +@msg the file isn't valid SVG: {e} 파일이 올바른 SVG가 아닙니다: {e} +@msg the file names no plate 파일에 지정된 색판이 없습니다 +@msg the file was updated or encrypted already 파일이 이미 업데이트되었거나 암호화되었습니다 +@msg the first character of a line has no character before it to kern: it moves only up and down (dy) 줄의 첫 글자는 앞에 커닝할 글자가 없습니다: 위아래로만 이동합니다(dy) +@msg the font {_1} is not available 글꼴 {_1}을(를) 사용할 수 없습니다 +@msg the format has no file extension 이 형식에는 파일 확장자가 없습니다 +@msg the function's prototype is not an object 함수의 프로토타입이 개체가 아닙니다 +@msg the graphic can't be read: {e} 그래픽을 읽을 수 없습니다: {e} +@msg the graphic is empty 그래픽이 비어 있습니다 +@msg the graphic's data is damaged 그래픽의 데이터가 손상되었습니다 +@msg the graphic's document has no artboard 그래픽의 문서에 아트보드가 없습니다 +@msg the grommet inset is too large for the banner 가장자리에서 아일렛까지 거리가 배너에 비해 너무 큽니다 +@msg the guide is hidden or locked 안내선이 숨겨졌거나 잠겨 있습니다 +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them `{name}`의 머리글 `{cell}`은(는) 변수 `{other}`(으)로 다시 읽힙니다: 둘 중 하나의 이름을 바꾸세요 +@msg the hem is too deep for the banner 단 접기가 배너에 비해 너무 깊습니다 +@msg the image `{path}` isn't followed: {r} 이미지 `{path}`을(를) 따라가지 않습니다: {r} +@msg the image has no embedded pixels 이미지에 포함된 픽셀이 없습니다 +@msg the image is empty 이미지가 비어 있습니다 +@msg the image is fully transparent 이미지가 완전히 투명합니다 +@msg the image is not on an artboard; pass rect 이미지가 아트보드 위에 없습니다. rect를 전달하세요 +@msg the image's pixels are missing 이미지의 픽셀이 없습니다 +@msg the image's pixels aren't in the document 이미지의 픽셀이 문서에 없습니다 +@msg the image's pixels can't be read: {e} 이미지의 픽셀을 읽을 수 없습니다: {e} +@msg the image's transparency doesn't fit it 이미지의 투명도가 이미지와 맞지 않습니다 +@msg the job would print more than {MAX_PAGES} pages 이 작업은 {MAX_PAGES}페이지보다 많이 인쇄하게 됩니다 +@msg the job would print more than {_1} pages 이 작업은 {_1}페이지보다 많이 인쇄하게 됩니다 +@msg the layers hold no art to mask 레이어에 마스크할 아트웍이 없습니다 +@msg the layout moved 레이아웃이 이동했습니다 +@msg the library has a group `{name}` already 라이브러리에 `{name}` 그룹이 이미 있습니다 +@msg the library has no group `{name}` 라이브러리에 `{name}` 그룹이 없습니다 +@msg the library has no item `{item}` 라이브러리에 `{item}` 항목이 없습니다 +@msg the library has no item `{other}` 라이브러리에 `{other}` 항목이 없습니다 +@msg the library holds {MAX_GROUPS} groups already 라이브러리에 이미 그룹이 {MAX_GROUPS}개 있습니다 +@msg the library holds {MAX_ITEMS} items of this kind already 라이브러리에 이 종류의 항목이 이미 {MAX_ITEMS}개 있습니다 +@msg the library's file is outside the read roots 라이브러리 파일이 읽기 루트 밖에 있습니다 +@msg the line is too long, or the paths it passes too many, to check 선이 너무 길거나 선이 지나는 패스가 너무 많아 확인할 수 없습니다 +@msg the method needs an XML value 이 메서드에는 XML 값이 필요합니다 +@msg the name is empty 이름이 비어 있습니다 +@msg the object has no fill, stroke or effect 개체에 칠, 선 또는 효과가 없습니다 +@msg the object has no graphic style (pass `name`) 개체에 그래픽 스타일이 없습니다(`name`을 전달하세요) +@msg the objects don't match the plan 개체가 계획과 맞지 않습니다 +@msg the open and permissions passwords must differ 열기 암호와 권한 암호는 서로 달라야 합니다 +@msg the operand stack overflowed 피연산자 스택이 오버플로되었습니다 +@msg the page has no area 페이지에 면적이 없습니다 +@msg the page's type in an unread object's box isn't in its page art 읽지 않은 개체의 상자에 있는 페이지 문자가 페이지 아트웍에 없습니다 +@msg the paint behind the proxy is not a gradient 프록시 뒤의 페인트가 그레이디언트가 아닙니다 +@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) 페인트가 자유 형태 그레이디언트가 아닙니다(paint.editGradient {kind}(으)로 적용하세요) +@msg the parameters or body do not form one function 매개변수나 본문이 하나의 함수를 이루지 않습니다 +@msg the parent must be a layer or sublayer 상위 항목은 레이어나 하위 레이어여야 합니다 +@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it 붙여넣은 SVG 필터 {name}은(는) 문서에 있는 같은 이름의 필터와 다르게 그려지며, 쓸 수 있는 빈 이름이 없습니다 +@msg the path can't be extended 패스를 연장할 수 없습니다 +@msg the path can't be inside one of the objects to attach 패스가 부착할 개체 안에 있으면 안 됩니다 +@msg the path has no segments to measure 패스에 잴 세그먼트가 없습니다 +@msg the path is empty 패스가 비어 있습니다 +@msg the path is too short to shorten that much 패스가 너무 짧아 그만큼 줄일 수 없습니다 +@msg the paths are too many or too tangled to find where they cross 패스가 너무 많거나 너무 얽혀 있어 교차하는 곳을 찾을 수 없습니다 +@msg the pen table holds more than 255 pens 펜 테이블에 펜이 255개보다 많습니다 +@msg the picture has no objects 그림에 개체가 없습니다 +@msg the plates can't be kept 색판을 보관할 수 없습니다 +@msg the position table does not match the instructions 위치 테이블이 명령과 맞지 않습니다 +@msg the program runs too long 프로그램 실행 시간이 너무 깁니다 +@msg the program uses too much memory 프로그램이 메모리를 너무 많이 사용합니다 +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes 프롬프트의 텍스트가 {MAX_PROMPT_BYTES}바이트보다 깁니다 +@msg the radix must be between 2 and 36 기수는 2에서 36 사이여야 합니다 +@msg the rectangle must have a width and a height 사각형에는 너비와 높이가 있어야 합니다 +@msg the rectangle needs a size and must lie on the canvas 사각형에는 크기가 있어야 하고 캔버스 위에 있어야 합니다 +@msg the rectangle or line reaches too many paths to check 사각형이나 선이 닿는 패스가 너무 많아 확인할 수 없습니다 +@msg the right side of 'in' is not an object 'in'의 오른쪽이 개체가 아닙니다 +@msg the right side of 'instanceof' is not a function 'instanceof'의 오른쪽이 함수가 아닙니다 +@msg the selected art has no width 선택한 아트웍에 너비가 없습니다 +@msg the selected art is nested more than {MAX_DEPTH} levels deep 선택한 아트웍이 {MAX_DEPTH}단계보다 깊게 중첩되어 있습니다 +@msg the selected art is too large to measure 선택한 아트웍이 너무 커서 잴 수 없습니다 +@msg the selection draws nothing 선택 영역이 아무것도 그리지 않습니다 +@msg the selection has no area 선택 영역에 면적이 없습니다 +@msg the selection has no art to export 선택 영역에 내보낼 아트웍이 없습니다 +@msg the selection has no bounds 선택 영역에 경계가 없습니다 +@msg the selection has no graphic style 선택 영역에 그래픽 스타일이 없습니다 +@msg the selection has no outlines to weed around 선택 영역에 둘레를 위딩할 윤곽선이 없습니다 +@msg the slice would leave the canvas 슬라이스가 캔버스를 벗어납니다 +@msg the spacing makes more than {MAX_GROMMETS} grommets 이 간격으로는 아일렛이 {MAX_GROMMETS}개보다 많아집니다 +@msg the spot colour couldn't be added 별색을 추가할 수 없습니다 +@msg the subpath has no ends to extend (it is closed or a single point) 하위 패스에 연장할 끝이 없습니다(닫혀 있거나 점 하나입니다) +@msg the symbols are nested too deeply to expand their instances 심볼이 너무 깊게 중첩되어 있어 인스턴스를 확장할 수 없습니다 +@msg the text file is too large to place as type (at most {_1} MB) 텍스트 파일이 너무 커서 문자로 가져올 수 없습니다(최대 {_1} MB) +@msg the text is empty 텍스트가 비어 있습니다 +@msg the text is over {_1} MB 텍스트가 {_1} MB를 넘습니다 +@msg the top object must be a path 맨 위 개체는 패스여야 합니다 +@msg the trace was cancelled 추적이 취소되었습니다 +@msg the trailer has no size 트레일러에 크기가 없습니다 +@msg the type object is flattened to nothing 문자 개체를 병합하면 아무것도 남지 않습니다 +@msg the value bind was called on is not a function bind를 호출한 값이 함수가 아닙니다 +@msg the value is not a constructor 값이 생성자가 아닙니다 +@msg the value is not a function 값이 함수가 아닙니다 +@msg the value is not a valid XML name 값이 올바른 XML 이름이 아닙니다 +@msg the value is not an XML attribute name 값이 XML 속성 이름이 아닙니다 +@msg the value is not an XML name 값이 XML 이름이 아닙니다 +@msg the value of axis `{tag}` must be a number or null 축 `{tag}`의 값은 숫자 또는 null이어야 합니다 +@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets 변수 라이브러리가 데이터 세트를 {MAX_LIBRARY_DATASETS}개보다 많이 선언합니다 +@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities 변수 라이브러리가 엔티티를 {MAX_LIBRARY_ENTITIES}개보다 많이 선언합니다 +@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables 변수 라이브러리가 변수를 {MAX_LIBRARY_VARIABLES}개보다 많이 선언합니다 +@msg the variable library declares no variables 변수 라이브러리가 변수를 선언하지 않습니다 +@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes 변수 라이브러리가 {MAX_LIBRARY_BYTES}바이트보다 큽니다 +@msg the variable library is not UTF-8 변수 라이브러리가 UTF-8이 아닙니다 +@msg the variable library is not well-formed XML: {e} 변수 라이브러리가 올바른 형식의 XML이 아닙니다: {e} +@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep 변수 라이브러리에 요소가 {MAX_LIBRARY_NESTING}단계보다 깊게 중첩되어 있습니다 +@msg the variable library's document type declaration holds something other than entities of plain text 변수 라이브러리의 문서 형식 선언에 일반 텍스트 엔티티가 아닌 것이 있습니다 +@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes 변수 라이브러리의 엔티티를 확장하면 {MAX_LIBRARY_BYTES}바이트를 넘습니다 +@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines 위딩 선이 아트웍의 윤곽선과 너무 많이 교차해 잴 수 없습니다. 선을 줄이세요 +@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap 위딩 선이 너무 많은 윤곽선 세그먼트에 너무 가깝게 지나가 잴 수 없습니다. 선을 줄이거나 간격을 줄이세요 +@msg the weeding lines would be more than {max_pieces} pieces 위딩 선이 {max_pieces}개보다 많은 조각이 됩니다 +@msg the window has no dock 창에 도킹 영역이 없습니다 +@msg the written PDF has no catalog 기록한 PDF에 카탈로그가 없습니다 +@msg the written PDF has no cross-reference table 기록한 PDF에 상호 참조 테이블이 없습니다 +@msg the written PDF's metadata can't be edited 기록한 PDF의 메타데이터를 편집할 수 없습니다 +@msg the {_1} plate lies outside the file {_1} 색판이 파일 밖에 있습니다 +@msg there are no colour swatches to draw 그릴 색상 견본이 없습니다 +@msg there is already a design named `{name}` `{name}` 디자인이 이미 있습니다 +@msg there is already a design named `{to}` `{to}` 디자인이 이미 있습니다 +@msg there is already a toolbar named `{name}` `{name}` 도구 막대가 이미 있습니다 +@msg there is already a toolbar named `{to}` `{to}` 도구 막대가 이미 있습니다 +@msg there is no banner to mark 표시할 배너가 없습니다 +@msg there is no library: create one first 라이브러리가 없습니다: 먼저 하나 만드세요 +@msg these paths are too degenerate for Shaper 이 패스는 셰이퍼에 쓰기에는 지나치게 퇴화되어 있습니다 +@msg this is a binary DXF file, which can't be read: save it from the CAD app as an ASCII DXF and open that 바이너리 DXF 파일은 읽을 수 없습니다. CAD 앱에서 ASCII DXF로 저장한 후 여세요 +@msg this warp's points can't be edited: use Reset with Mesh first 이 왜곡의 점은 편집할 수 없습니다: 먼저 메시로 재설정을 사용하세요 +@msg thumbnail: no page {v} (the {what} has {count}) thumbnail: 페이지 {v}이(가) 없습니다({what}의 페이지 수: {count}) +@msg tileRange of {what}: {e} {what}의 tileRange: {e} +@msg title can't be empty title은 비워 둘 수 없습니다 +@msg to (the new name) is required to(새 이름)가 필요합니다 +@msg to must be fill or stroke to는 fill 또는 stroke여야 합니다 +@msg to must be smooth or corner to는 smooth 또는 corner여야 합니다 +@msg toExponential() digits must be between 0 and 20 toExponential()의 자릿수는 0에서 20 사이여야 합니다 +@msg toFixed() digits must be between 0 and 20 toFixed()의 자릿수는 0에서 20 사이여야 합니다 +@msg toISOString is not a function toISOString이 함수가 아닙니다 +@msg toPrecision() argument must be between 1 and 21 toPrecision()의 인수는 1에서 21 사이여야 합니다 +@msg toSource: a getter cannot be written as source toSource: getter는 소스로 기록할 수 없습니다 +@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource: 요소 외에 속성이 있거나, 열거할 수 없는 속성이 있거나, 프로토타입이 다른 배열은 소스로 기록할 수 없습니다 +@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource: 열거할 수 없는 속성이 있거나 프로토타입이 Object.prototype이 아닌 개체는 소스로 기록할 수 없습니다 +@msg toSource: the source is too long toSource: 소스가 너무 깁니다 +@msg toSource: the value contains itself toSource: 값이 자신을 포함합니다 +@msg toSource: the value has too many entries toSource: 값에 항목이 너무 많습니다 +@msg toSource: the value is nested too deep toSource: 값이 너무 깊게 중첩되어 있습니다 +@msg toString is not a function toString이 함수가 아닙니다 +@msg too large for a zip archive: package to a folder zip 아카이브에 넣기에는 너무 큽니다: 폴더로 패키지하세요 +@msg too many Shaper edits 셰이퍼 편집이 너무 많습니다 +@msg too many `points` (at most 100000) `points`가 너무 많습니다(최대 100000개) +@msg too many arguments 인수가 너무 많습니다 +@msg too many characters: at most {MAX_CHARS} 글자가 너무 많습니다: 최대 {MAX_CHARS}자 +@msg too many dots for this size: lower the frequency 이 크기에는 점이 너무 많습니다: 주파수를 낮추세요 +@msg too many filters over one file 한 파일에 필터가 너무 많습니다 +@msg too many overlapping paths for Shaper 셰이퍼에 쓰기에는 겹치는 패스가 너무 많습니다 +@msg too many quantifiers 수량자가 너무 많습니다 +@msg too many saved clips 저장된 클립이 너무 많습니다 +@msg too many saved graphics states 저장된 그래픽 상태가 너무 많습니다 +@msg toolbar (a toolbar's name) must be a string toolbar(도구 막대 이름)는 문자열이어야 합니다 +@msg toolbar name can't be empty 도구 막대 이름은 비워 둘 수 없습니다 +@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v}: "flatten"(기본값) 또는 "omit"이어야 합니다 +@msg turn on at least one side 한쪽 이상을 켜세요 +@msg two saved selections would be named `{_1}` 저장된 선택 영역 두 개의 이름이 `{_1}`이(가) 됩니다 +@msg type dimension cannot be represented with this text transform 이 텍스트 변형으로는 문자 치수를 나타낼 수 없습니다 +@msg type dimension must be finite 문자 치수는 유한해야 합니다 +@msg unexpected resources 예상하지 못한 리소스 +@msg unknown CMYK profile `{name}` 알 수 없는 CMYK 프로파일 `{name}` +@msg unknown File Info field `{k}` 알 수 없는 파일 정보 필드 `{k}` +@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; prefix - to turn off; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take an alternate's number as `tag=N` with N from 0 to {_1}, as `salt=2`) 알 수 없는 OpenType 기능 `{t}`(liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt. 끄려면 앞에 -를 붙이세요. swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt는 대체 글리프 번호를 `tag=N` 형식으로 받으며 N은 0에서 {_1} 사이입니다. 예: `salt=2`) +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) featureChanges의 알 수 없는 OpenType 기능 `{t}`(`features`가 받는 태그를 사용하세요) +@msg unknown PDF preset `{name}` (presets: {_1}) 알 수 없는 PDF 프리셋 `{name}`(프리셋: {_1}) +@msg unknown Warp style 알 수 없는 왜곡 스타일 +@msg unknown alignToPath `{a}` 알 수 없는 alignToPath `{a}` +@msg unknown arrowhead {v} 알 수 없는 화살촉 {v} +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) 알 수 없는 artboardLayout 키 `{k}`(layout, columns, spacing, rightToLeft) +@msg unknown attribute `{attribute}` 알 수 없는 속성 `{attribute}` +@msg unknown attribute `{k}` 알 수 없는 속성 `{k}` +@msg unknown axis `{other}` (vertical|horizontal|free) 알 수 없는 축 `{other}`(vertical|horizontal|free) +@msg unknown blend mode `{b}` 알 수 없는 혼합 모드 `{b}` +@msg unknown blend mode {b} 알 수 없는 혼합 모드 {b} +@msg unknown brush type `{o}` 알 수 없는 브러시 유형 `{o}` +@msg unknown category `{c}` 알 수 없는 범주 `{c}` +@msg unknown category `{c}` ({_1}) 알 수 없는 범주 `{c}`({_1}) +@msg unknown character `{c}` 알 수 없는 문자 `{c}` +@msg unknown character style number field 알 수 없는 문자 스타일 숫자 필드 +@msg unknown dither `{d}` (diffusion, pattern, noise or none) 알 수 없는 디더 `{d}`(diffusion, pattern, noise 또는 none) +@msg unknown effect `{e}` 알 수 없는 효과 `{e}` +@msg unknown end {e} 알 수 없는 끝 {e} +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) 알 수 없는 flattenerPreset `{name}`(High Resolution, Medium Resolution, Low Resolution 또는 저장된 프리셋: flattener.presets.list 참조) +@msg unknown format `{f}` (vcswatches, gpl, ase or css) 알 수 없는 형식 `{f}`(vcswatches, gpl, ase 또는 css) +@msg unknown from {f} 알 수 없는 from {f} +@msg unknown graph type `{t}` 알 수 없는 그래프 유형 `{t}` +@msg unknown grid colours `{name}` (one of {_1}) 알 수 없는 격자 색상 `{name}`({_1} 중 하나) +@msg unknown gridType `{t}` 알 수 없는 gridType `{t}` +@msg unknown highlight `{v}` 알 수 없는 강조 `{v}` +@msg unknown intent `{i}` 알 수 없는 의도 `{i}` +@msg unknown issue kind {id} 알 수 없는 문제 종류 {id} +@msg unknown issue kind {v} 알 수 없는 문제 종류 {v} +@msg unknown knockout state {k} 알 수 없는 녹아웃 상태 {k} +@msg unknown language `{language}` (one of {_1}) 알 수 없는 언어 `{language}`({_1} 중 하나) +@msg unknown language `{l}` (one of {_1}) 알 수 없는 언어 `{l}`({_1} 중 하나) +@msg unknown method `{m}` ({_1}) 알 수 없는 방법 `{m}`({_1}) +@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}`(set, add, subtract) +@msg unknown mode `{o}` 알 수 없는 모드 `{o}` +@msg unknown mode {m} 알 수 없는 모드 {m} +@msg unknown model `{other}` 알 수 없는 모델 `{other}` +@msg unknown model `{other}` (keep, new or document) 알 수 없는 모델 `{other}`(keep, new 또는 document) +@msg unknown op {o} 알 수 없는 연산 {o} +@msg unknown order `{o}`: toggle, front, forward, backward or back 알 수 없는 순서 `{o}`: toggle, front, forward, backward 또는 back +@msg unknown overprints `{v}` (preserve, simulate or discard) 알 수 없는 오버프린트 `{v}`(preserve, simulate 또는 discard) +@msg unknown panel 알 수 없는 패널 +@msg unknown panel `{panel}` 알 수 없는 패널 `{panel}` +@msg unknown parameter `{key}` 알 수 없는 매개변수 `{key}` +@msg unknown parameter `{within}settings.{format}`; {why} 알 수 없는 매개변수 `{within}settings.{format}`: {why} +@msg unknown parameter `{within}{key}`; {why} 알 수 없는 매개변수 `{within}{key}`: {why} +@msg unknown pieLegend `{v}` (none, standard or wedges) 알 수 없는 pieLegend `{v}`(none, standard 또는 wedges) +@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}` +@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}`(double, single) +@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) 알 수 없는 색상 축소 방식 `{reduction}`(selective, perceptual, adaptive, web, gray, blackWhite, uniform 또는 document) +@msg unknown resolution `{v}` 알 수 없는 해상도 `{v}` +@msg unknown rule `{rule}` ({_1}) 알 수 없는 규칙 `{rule}`({_1}) +@msg unknown rule `{r}` (see color.harmony) 알 수 없는 규칙 `{r}`(color.harmony 참조) +@msg unknown setting `{k}` 알 수 없는 설정 `{k}` +@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) 알 수 없는 모양 `{v}`(circle|ellipse|square|diamond|line) +@msg unknown spacing `{o}` (smooth|steps|distance) 알 수 없는 간격 `{o}`(smooth|steps|distance) +@msg unknown tileType `{t}` ({_1}) 알 수 없는 tileType `{t}`({_1}) +@msg unknown units 알 수 없는 단위 +@msg unknown units `{u}` 알 수 없는 단위 `{u}` +@msg unknown value axis `{v}` (left, right or both) 알 수 없는 값 축 `{v}`(left, right 또는 both) +@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) 알 수 없는 변형 `{v}`(tintsShades|warmCool|vividMuted) +@msg unknown warp style `{s}` 알 수 없는 왜곡 스타일 `{s}` +@msg unknown {key} `{v}` (none, short or full) 알 수 없는 {key} `{v}`(none, short 또는 full) +@msg unknown {key} profile `{n}` 알 수 없는 {key} 프로파일 `{n}` +@msg unknown {k} key `{key}` (size, position) 알 수 없는 {k} 키 `{key}`(size, position) +@msg unmatched ')' 짝이 맞지 않는 ')' +@msg unreadable object 읽을 수 없는 개체 +@msg unsafe destination file name 안전하지 않은 대상 파일 이름 +@msg unterminated character class 닫히지 않은 문자 클래스 +@msg unterminated group 닫히지 않은 그룹 +@msg use paths or compound paths 패스나 컴파운드 패스를 사용하세요 +@msg useArtboards must be true or false, not {v} useArtboards는 {v}이(가) 아니라 true 또는 false여야 합니다 +@msg value for `{var}` must match its variable kind `{var}`의 값은 변수 종류와 맞아야 합니다 +@msg variable `{_1}` is already defined 변수 `{_1}`이(가) 이미 정의되어 있습니다 +@msg variable `{name}` has no `trait` 변수 `{name}`에 `trait`이(가) 없습니다 +@msg variable `{name}` is already defined 변수 `{name}`이(가) 이미 정의되어 있습니다 +@msg variable `{name}` is declared twice 변수 `{name}`이(가) 두 번 선언되었습니다 +@msg variable `{to}` already exists 변수 `{to}`이(가) 이미 있습니다 +@msg view is required view가 필요합니다 +@msg view must be one of {_1} view는 {_1} 중 하나여야 합니다 +@msg warpStyle can only edit an applied Warp effect warpStyle은 적용된 왜곡 효과만 편집할 수 있습니다 +@msg width and height must be more than 0 and at most {limit} 너비와 높이는 0보다 크고 {limit} 이하여야 합니다 +@msg width factors must be finite 너비 계수는 유한해야 합니다 +@msg width must be positive width는 양수여야 합니다 +@msg width out of range 너비가 범위를 벗어났습니다 +@msg width/height must be > 0 너비/높이는 0보다 커야 합니다 +@msg with no hem and no grommets there is nothing to mark 단 접기와 아일렛이 모두 없으면 표시할 것이 없습니다 +@msg wordGap and lineGap need area type in the selection wordGap과 lineGap을 쓰려면 선택 항목에 영역 문자가 있어야 합니다 +@msg x and y must be finite numbers x와 y는 유한한 숫자여야 합니다 +@msg x and y must be given together x와 y는 함께 지정해야 합니다 +@msg xpath needs its variables as an object xpath는 변수를 개체로 받아야 합니다 +@msg xpath needs the expression as a string xpath는 식을 문자열로 받아야 합니다 +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone에는 기준이 필요합니다: anchor로 패널 옆에 도킹하거나 side(Start 또는 End)로 새 도크 열에 도킹하세요 +@msg zoom must be positive zoom은 양수여야 합니다 +@msg {C}: no effect at index {list} {C}: 인덱스 {list}에 효과가 없습니다 +@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET}은(는) 보호되어 있어 그대로 유지됩니다 +@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET}은(는) 보호되어 있습니다: 설정을 다른 이름으로 저장하세요 +@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED}. 또한 설정과 캐시를 {_1}에서 {_2}(으)로 되돌려 옮길 수 없습니다: {e} +@msg {LABEL} produced an empty result {LABEL}의 결과가 비어 있습니다 +@msg {_1} MB of document, at most {_2} MB 문서 {_1} MB, 최대 {_2} MB +@msg {_1} MB of images, at most {_2} MB 이미지 {_1} MB, 최대 {_2} MB +@msg {_1} bytes of RGBA data for a {width} × {height} image {width} × {height} 이미지에 대한 RGBA 데이터 {_1}바이트 +@msg {_1} can't be read, or changed since it was read: relink or update it first {_1}을(를) 읽을 수 없거나 읽은 뒤에 바뀌었습니다: 먼저 다시 연결하거나 업데이트하세요 +@msg {_1} can't go into {into} {_1}은(는) {into}에 넣을 수 없습니다 +@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1}은(는) 인쇄를 저해상도로 제한할 수 없습니다: PDF 1.4 이상을 선택하거나 고해상도 인쇄를 허용하세요 +@msg {_1} files can't be opened yet {_1} 파일은 아직 열 수 없습니다 +@msg {_1} files can't be password-protected: remove the passwords or choose no standard {_1} 파일은 암호로 보호할 수 없습니다: 암호를 제거하거나 표준을 선택하지 마세요 +@msg {_1} files can't be {_2} {_1} 파일은 {_2}일 수 없습니다 +@msg {_1} files can't carry editing data: turn off preserveEditing {_1} 파일에는 편집 데이터를 담을 수 없습니다: preserveEditing을 끄세요 +@msg {_1} files can't have PDF layers: turn off createLayers {_1} 파일에는 PDF 레이어를 둘 수 없습니다: createLayers를 끄세요 +@msg {_1} files need a creation date, and this platform has no clock to give one {_1} 파일에는 작성 날짜가 필요하지만 이 플랫폼에는 날짜를 알려 줄 시계가 없습니다 +@msg {_1} is not a folder: {e} {_1}은(는) 폴더가 아닙니다: {e} +@msg {_1} isn't a path {_1}은(는) 패스가 아닙니다 +@msg {_1} options: {e} {_1} 옵션: {e} +@msg {_1} paints nothing to rasterize {_1}에는 래스터화할 칠이 없습니다 +@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) {_1} 암호에는 ASCII 문자를 {MAX_LEGACY_PASSWORD}자까지 쓸 수 있습니다(PDF 1.7 이상은 모든 문자를 받습니다) +@msg {_1} with linked images or an image map writes several files: use document.export 연결된 이미지나 이미지 맵이 있는 {_1}은(는) 여러 파일을 기록합니다: document.export를 사용하세요 +@msg {_1} writes one file per artboard here: use document.export, or name one artboard 여기서 {_1}은(는) 아트보드마다 파일을 하나씩 기록합니다: document.export를 사용하거나 아트보드 하나를 지정하세요 +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2}픽셀은 {_3}에 너무 큽니다(한 변 최대 {max}픽셀): 비율을 낮추세요 +@msg {_1}: belongs to another account {_1}: 다른 계정의 소유입니다 +@msg {_1}: not a folder of its own {_1}: 자체 폴더가 아닙니다 +@msg {_1}: not a plain file {_1}: 일반 파일이 아닙니다 +@msg {_1}: {_2} and {_3} are in the way {_1}: {_2}와(과) {_3}이(가) 가로막고 있습니다 +@msg {code}: {0} {code}: {0} +@msg {dest}: {e} {dest}: {e} +@msg {dir}: {e} {dir}: {e} +@msg {doc} has no folder {doc}에는 폴더가 없습니다 +@msg {file} isn't linked {file}은(는) 연결되어 있지 않습니다 +@msg {file} names more than one linked JavaScript file {file}이(가) 연결된 JavaScript 파일을 둘 이상 지정합니다 +@msg {folder} is not a package folder {folder}은(는) 패키지 폴더가 아닙니다 +@msg {folder}: more than {max} files and folders {folder}: 파일과 폴더가 {max}개를 넘습니다 +@msg {folder}: {e} {folder}: {e} +@msg {g} is not a Live Paint group {g}은(는) 라이브 페인트 그룹이 아닙니다 +@msg {id} is not a slice {id}은(는) 슬라이스가 아닙니다 +@msg {key} must be 0–{MAX_ADD_AROUND} pt {key}은(는) 0–{MAX_ADD_AROUND} pt여야 합니다 +@msg {key} must be [x,y] {key}은(는) [x,y]여야 합니다 +@msg {key} must be a percentage {min}..100 {key}은(는) {min}..100 사이의 백분율이어야 합니다 +@msg {key} must be a positive number, not {n} {key}은(는) {n}이(가) 아니라 양수여야 합니다 +@msg {key} must be a positive number, not {v} {key}은(는) {v}이(가) 아니라 양수여야 합니다 +@msg {key} must be one of {_1} {key}은(는) {_1} 중 하나여야 합니다 +@msg {key} must be {_1}, got {s} {key}은(는) {_1}이어야 합니다({s} 입력됨) +@msg {key} must name a floating panel {key}은(는) 떠 있는 패널을 지정해야 합니다 +@msg {key} must name a panel in another group {key}은(는) 다른 그룹의 패널을 지정해야 합니다 +@msg {key} must name a panel that shows {key}은(는) 표시된 패널을 지정해야 합니다 +@msg {key} must name a registered panel {key}은(는) 등록된 패널을 지정해야 합니다 +@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) {kind} 파일에는 투명도가 없는데 이 파일에는 {what}이(가) 있게 됩니다: 먼저 병합하세요(object.flattenTransparency) +@msg {kind} images can't be opened (see document.formats) {kind} 이미지는 열 수 없습니다(document.formats 참조) +@msg {k} `{v}` is not one of the choices {k} `{v}`은(는) 선택 항목 중 하나가 아닙니다 +@msg {k} has control characters{_1} {k}에 제어 문자가 있습니다{_1} +@msg {k} is too long (at most {MAX_TEXT} characters) {k}이(가) 너무 깁니다(최대 {MAX_TEXT}자) +@msg {k} must be > 0 {k}은(는) 0보다 커야 합니다 +@msg {k} must be a number (pt) or a length such as "210 mm" {k}은(는) 숫자(pt) 또는 "210 mm" 같은 길이여야 합니다 +@msg {k} must be a number from {min} to {max} {k}은(는) {min}에서 {max} 사이의 숫자여야 합니다 +@msg {k} must be a number or "auto" {k}은(는) 숫자 또는 "auto"여야 합니다 +@msg {k} must be a string {k}은(는) 문자열이어야 합니다 +@msg {k} must be text {k}은(는) 텍스트여야 합니다 +@msg {k} must be true or false {k}은(는) true 또는 false여야 합니다 +@msg {k} must be {{size?, position?}} {k}은(는) {{size?, position?}}여야 합니다 +@msg {k}: a custom arrowhead's path needs an area to fill and at most {most} anchors and {most} subpaths, each anchor within {far} head units of its tip {k}: 사용자 정의 화살촉의 패스에는 칠할 영역이 있어야 하고 기준점은 {most}개 이하, 하위 패스는 {most}개 이하여야 하며, 각 기준점은 끝에서 화살촉 단위로 {far} 이내에 있어야 합니다 +@msg {k}: bad custom arrowhead path: {e} {k}: 잘못된 사용자 정의 화살촉 패스: {e} +@msg {k}: bad custom arrowhead: {e} {k}: 잘못된 사용자 정의 화살촉: {e} +@msg {k}: can't read `{s}` as a length {k}: `{s}`을(를) 길이로 읽을 수 없습니다 +@msg {k}: custom must be a name {k}: custom은 이름이어야 합니다 +@msg {label} must be a number at most {max} {label}은(는) {max} 이하의 숫자여야 합니다 +@msg {label} must be a number from -1296 to 1296 {label}은(는) -1296에서 1296 사이의 숫자여야 합니다 +@msg {label} must be a number from 1 to 10000 {label}은(는) 1에서 10000 사이의 숫자여야 합니다 +@msg {name} `{s}`: {ids} {name} `{s}`: {ids} +@msg {name} is built in and can't be deleted {name}은(는) 기본 제공 항목이라 삭제할 수 없습니다 +@msg {name} must be a number {name}은(는) 숫자여야 합니다 +@msg {name} must be {lo}–{hi}{unit} (got {v}) {name}은(는) {lo}–{hi}{unit}여야 합니다({v} 입력됨) +@msg {name}: CRC or size mismatch {name}: CRC 또는 크기가 일치하지 않습니다 +@msg {name}: only this Affinity file's embedded preview could be read; use File › Open to see it with its warning, or export SVG or PDF from Affinity before placing {name}: 이 Affinity 파일은 포함된 미리 보기만 읽을 수 있었습니다. 경고와 함께 보려면 파일 › 열기를 사용하거나, 가져오기 전에 Affinity에서 SVG 또는 PDF로 내보내세요 +@msg {name}: {e} {name}: {e} +@msg {native}; its embedded preview can't be read either ({e}) {native}. 포함된 미리 보기도 읽을 수 없습니다({e}) +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: 복사는 색판이 파일 안에 있는 DCS 파일만 받습니다. 색판 파일이 있는 위치의 파일로 다시 연결하세요 +@msg {path}: larger than {_1} MB {path}: {_1} MB보다 큽니다 +@msg {saving} failed: {e} {saving} 실패: {e} +@msg {shown} is not an XPath 1.0 function {shown}은(는) XPath 1.0 함수가 아닙니다 +@msg {std} files are CMYK, and the file would have colours in {_1} {std} 파일은 CMYK인데 이 파일에는 {_1} 색상이 있게 됩니다 +@msg {std} files embed their fonts, and the file would have one that isn't {std} 파일은 글꼴을 포함하는데 이 파일에는 포함되지 않은 글꼴이 있게 됩니다 +@msg {target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway {target}은(는) 이 문서를 읽어 온 파일이며 읽을 때 빠진 내용이 있습니다({what}): 덮어쓰면 그 내용을 영구히 잃습니다. 다른 파일에 기록하거나, 그래도 바꾸려면 acknowledgeLoss: true를 전달하세요 +@msg {ty} brushes need selected art (or params.{k}) {ty} 브러시에는 선택한 아트웍(또는 params.{k})이 필요합니다 +@msg {v} is not an asset id {v}은(는) 에셋 id가 아닙니다 +@msg {v} names no layer or sublayer {v}에 해당하는 레이어나 하위 레이어가 없습니다 +@msg {what} can't be intertwined {what}은(는) 교차할 수 없습니다 +@msg {what} doesn't fit the paper with its bleed and marks {what}은(는) 도련과 표시를 포함하면 용지에 맞지 않습니다 +@msg {what} is not a function {what}은(는) 함수가 아닙니다 +@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what}은(는) 한 장에 {MAX_COPIES_PER_SHEET}개보다 많이 인쇄됩니다: 크기를 키우세요 +@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what}은(는) 타일 {MAX_TILES}개보다 많이 나뉘어 인쇄됩니다: 크기를 줄이세요 +@msg {what} {i} out of range {what} {i}이(가) 범위를 벗어났습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index c8ccb0bda4..9e09c23950 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -157,6 +157,7 @@ @msg `select` must be true or false `select` moet true of false zijn @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` en `maxAnchors` moeten gehele getallen zijn @msg `{key}` must be a number not below 0 `{key}` moet een getal van minstens 0 zijn +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` moet uit vier getallen van minstens 0 bestaan: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` moet [x, y, width, height] zijn: vier eindige getallen, width en height niet negatief @msg `{min_key}` is greater than `{max_key}` `{min_key}` is groter dan `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` geef minstens een van `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -711,6 +712,8 @@ Crop Image Afbeelding bijsnijden Image Tools Afbeeldingshulpmiddelen Remove Background… Achtergrond verwijderen… + Draw (Pencil) Tekenen (Potlood) + Place Image Afbeelding plaatsen Remove Background Achtergrond verwijderen Background Achtergrond Edge Color Randkleur diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 300972cea6..ebe2d0160f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -171,6 +171,8 @@ Crop Image Kadruj obraz Image Tools Narzędzia obrazu Remove Background… Usuń tło… + Draw (Pencil) Rysuj (ołówek) + Place Image Umieść obraz Remove Background Usuń tło Background Tło Edge Color Kolor krawędzi @@ -4624,6 +4626,7 @@ imageTrace Outline Kontur @msg `select` must be true or false `select` musi mieć wartość true lub false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` i `maxAnchors` muszą być liczbami całkowitymi @msg `{key}` must be a number not below 0 `{key}` musi być liczbą nie mniejszą niż 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` musi być czterema liczbami nie mniejszymi niż 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musi mieć postać [x, y, width, height]: cztery skończone liczby, szerokość i wysokość nieujemne @msg `{min_key}` is greater than `{max_key}` `{min_key}` jest większe niż `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` podaj co najmniej jeden z parametrów `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 9834b5c06f..ff082c970c 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -631,6 +631,8 @@ antiAlias Sharp Nítido Crop Image Recortar Imagem Image Tools Ferramentas de imagem Remove Background… Remover Fundo… + Draw (Pencil) Desenhar (Lápis) + Place Image Colocar Imagem Remove Background Remover Fundo Background Fundo Edge Color Cor da Borda @@ -3954,6 +3956,7 @@ axis Both Ambos @msg `select` must be true or false `select` deve ser true ou false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devem ser números inteiros @msg `{key}` must be a number not below 0 `{key}` deve ser um número não inferior a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` deve ser quatro números não inferiores a 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve ser [x, y, width, height]: quatro números finitos, com largura e altura não negativas @msg `{min_key}` is greater than `{max_key}` `{min_key}` é maior que `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` informe pelo menos um de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 97f1bef682..ae2671e8ea 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -703,6 +703,8 @@ font-technology Variable Вариативные Crop Image Обрезать изображение Image Tools Инструменты изображения Remove Background… Удалить фон… + Draw (Pencil) Рисовать (Карандаш) + Place Image Поместить изображение Remove Background Удалить фон Background Фон Edge Color Цвет края @@ -4527,6 +4529,7 @@ axis Both Оба @msg `select` must be true or false `select` должно быть true или false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` и `maxAnchors` должны быть целыми числами @msg `{key}` must be a number not below 0 `{key}` должно быть числом не меньше 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` должно быть четырьмя числами не меньше 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` должно быть [x, y, width, height]: четыре конечных числа, ширина и высота не отрицательные @msg `{min_key}` is greater than `{max_key}` `{min_key}` больше, чем `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажите хотя бы один из параметров `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -7585,3 +7588,119 @@ shape Open Открытый Grommets Люверсы Grommet Inset Отступ люверсов от края Diameter Диаметр + +# Messages of the effects, 3D, image export, color profiles, swatch exchange files, color books and DXF import. +@msg 3D effects require finite coordinates within 100 million points 3D-эффектам нужны конечные координаты в пределах 100 миллионов пунктов +@msg 3D effects require one to ten lights 3D-эффектам нужно от одного до десяти источников света +@msg BMP depth {_1}: 1, 4, 8, 16, 24 or 32 bits per pixel Глубина цвета BMP {_1}: допустимо 1, 4, 8, 16, 24 или 32 бита на пиксель +@msg BMP encoding failed: {width} × {height} pixels doesn't fit the format or the pixel buffer Ошибка кодирования BMP: {width} × {height} пикселей не соответствует формату или буферу пикселей +@msg GIF encoding failed: the palette image is malformed Ошибка кодирования GIF: изображение с палитрой имеет неверную структуру +@msg GIF encoding failed: {e} Ошибка кодирования GIF: {e} +@msg JPEG colour profile: {e} Цветовой профиль JPEG: {e} +@msg JPEG encoding failed: pixel buffer doesn't match the image size Ошибка кодирования JPEG: буфер пикселей не соответствует размеру изображения +@msg JPEG encoding failed: {e} Ошибка кодирования JPEG: {e} +@msg PNG encoding failed: pixel buffer doesn't match the image size Ошибка кодирования PNG: буфер пикселей не соответствует размеру изображения +@msg PNG encoding failed: the palette image is malformed Ошибка кодирования PNG: изображение с палитрой имеет неверную структуру +@msg PNG encoding failed: {e} Ошибка кодирования PNG: {e} +@msg PNG encoding failed: {width} × {height} pixels is not a valid image size Ошибка кодирования PNG: {width} × {height} пикселей — недопустимый размер изображения +@msg PSD colour profile: {e} Цветовой профиль PSD: {e} +@msg PSD encoding failed: the pixel buffer doesn't match the image size Ошибка кодирования PSD: буфер пикселей не соответствует размеру изображения +@msg Revolve profile contains invalid coordinates Профиль вращения содержит недопустимые координаты +@msg Revolve profile is too complex Профиль вращения слишком сложен +@msg Revolve projection is not finite Проекция вращения не является конечной +@msg Revolve requires finite profile coordinates within 100 million points Для вращения нужны конечные координаты профиля в пределах 100 миллионов пунктов +@msg Revolve surface is too complex; reduce Segments or simplify the profile Поверхность вращения слишком сложна; уменьшите «Сегменты» или упростите профиль +@msg Revolve surface is too complex; simplify the profile Поверхность вращения слишком сложна; упростите профиль +@msg Revolve visibility cannot split the projected boundary Расчёт видимости вращения не может разделить спроецированную границу +@msg Revolve visibility creates too many fragments Расчёт видимости вращения создаёт слишком много фрагментов +@msg Revolve visibility creates too many vector anchors Расчёт видимости вращения создаёт слишком много векторных опорных точек +@msg Revolve visibility exceeds its geometry work limit Расчёт видимости вращения превышает свой предел геометрических вычислений +@msg Revolve visibility exceeds its spatial index limit Расчёт видимости вращения превышает свой предел пространственного индекса +@msg Revolve visibility has too many boundary crossings В расчёте видимости вращения слишком много пересечений границы +@msg Revolve visibility has too many input anchors В расчёте видимости вращения слишком много входных опорных точек +@msg Revolve visibility requires convex cap sectors Для расчёта видимости вращения нужны выпуклые секторы торцов +@msg Revolve visibility requires finite projected faces Для расчёта видимости вращения нужны конечные спроецированные грани +@msg SVG options: {e} Параметры SVG: {e} +@msg Save for Web settings: {e} Параметры «Сохранить для web»: {e} +@msg TIFF colour profile: {e} Цветовой профиль TIFF: {e} +@msg TIFF encoding failed: the pixel buffer doesn't match the image size Ошибка кодирования TIFF: буфер пикселей не соответствует размеру изображения +@msg TIFF encoding failed: {e} Ошибка кодирования TIFF: {e} +@msg Targa depth {_1}: 16, 24 or 32 bits per pixel Глубина цвета Targa {_1}: допустимо 16, 24 или 32 бита на пиксель +@msg Targa encoding failed: the pixel buffer doesn't match the image size Ошибка кодирования Targa: буфер пикселей не соответствует размеру изображения +@msg The 3D projection is not finite 3D-проекция не является конечной +@msg The 3D shadow is too complex; turn Shadows off or simplify the art 3D-тень слишком сложна; отключите «Тени» или упростите графику +@msg The shape contains invalid coordinates Фигура содержит недопустимые координаты +@msg Too many Revolve faces for visibility evaluation Слишком много граней вращения для расчёта видимости +@msg VectorCraft reads at most {ASE_MAX_ENTRIES} colors and groups from a swatch exchange file, and this library has {entries} VectorCraft читает из файла обмена образцами не более {ASE_MAX_ENTRIES} цветов и групп, а в этой библиотеке их {entries} +@msg WebP encoding failed: {e} Ошибка кодирования WebP: {e} +@msg [{j}] `{_1}`: {why} [{j}] `{_1}`: {why} +@msg `answers[{i}]` {e} `answers[{i}]` {e} +@msg `view` is smallThumb, mediumThumb, largeThumb, smallList or largeList `view` принимает значения smallThumb, mediumThumb, largeThumb, smallList или largeList +@msg a line can't run from a point to itself линия не может идти от точки к самой себе +@msg a line needs at least two points линии нужны хотя бы две точки +@msg a number cannot be read не удаётся прочитать число +@msg a swatch exchange block is too long блок файла обмена образцами слишком длинный +@msg a swatch exchange name is too long имя в файле обмена образцами слишком длинное +@msg a {sw} × {sh} image can't be resized to {width} × {height} pixels размер изображения {sw} × {sh} нельзя изменить до {width} × {height} пикселей +@msg a {width} × {height} image is too large изображение {width} × {height} слишком велико +@msg a {width} × {height} image: {e} изображение {width} × {height}: {e} +@msg an image could not be written: {e} не удалось записать изображение: {e} +@msg an image that couldn't be read was left out изображение, которое не удалось прочитать, пропущено +@msg can't open “{name}”: {e} не удаётся открыть «{name}»: {e} +@msg cannot align zip entries to {align} bytes не удаётся выровнять записи zip по {align} байт +@msg cannot open the automation {kind} root `{_1}`: {e} не удаётся открыть корневую папку автоматизации ({kind}) `{_1}`: {e} +@msg cannot read profile: {0} не удаётся прочитать профиль: {0} +@msg color book version {version} isn't supported (only 1) версия цветовой книги {version} не поддерживается (только 1) +@msg color books in color space {other} aren't supported (only RGB, CMYK and Lab) цветовые книги в цветовом пространстве {other} не поддерживаются (только RGB, CMYK и Lab) +@msg file name too long to zip: {_1} имя файла слишком длинное для zip: {_1} +@msg file name too long to zip: {name} имя файла слишком длинное для zip: {name} +@msg freeform line {i}: {e} произвольная линия {i}: {e} +@msg freeform point {i}: {e} произвольная точка {i}: {e} +@msg give `{key}` or `{other}`, not both укажите `{key}` или `{other}`, но не то и другое +@msg grayscale profiles can't be working spaces yet профили оттенков серого пока не могут быть рабочими пространствами +@msg input contains NaN or infinite coordinates входные данные содержат координаты NaN или бесконечные координаты +@msg input path could not be closed не удалось замкнуть входной контур +@msg invalid ICC profile: {0} недопустимый профиль ICC: {0} +@msg invalid scale {scale} недопустимый масштаб {scale} +@msg its localized names take more than {_1} KB его локализованные имена занимают более {_1} КБ +@msg its objects are nested too deep ({e}) его объекты вложены слишком глубоко ({e}) +@msg its tag table lists the `{_1}` tag more than once в его таблице тегов тег `{_1}` указан более одного раза +@msg not a graphic style library: {e} не библиотека графических стилей: {e} +@msg not a swatch library (.vcswatches or .gpl) не библиотека образцов (.vcswatches или .gpl) +@msg not a swatch library (format `{_1}`) не библиотека образцов (формат `{_1}`) +@msg not a swatch library: {e} не библиотека образцов: {e} +@msg not an action set file ({e}) не файл набора действий ({e}) +@msg profile colour space {other} is not RGB, CMYK or Gray цветовое пространство профиля {other} не является RGB, CMYK или оттенками серого +@msg regular expression too large регулярное выражение слишком велико +@msg script file is not UTF-8 text: {value} файл скрипта не является текстом UTF-8: {value} +@msg stop {i}: {e} точка перехода {i}: {e} +@msg swatch exchange version {major}.{minor} isn't supported (only 1.x) версия файла обмена образцами {major}.{minor} не поддерживается (только 1.x) +@msg the color book holds more than {ASE_MAX_ENTRIES} colors в цветовой книге больше {ASE_MAX_ENTRIES} цветов +@msg the color book is cut short цветовая книга обрезана +@msg the color book's names take more than {_1} MB имена цветовой книги занимают более {_1} МБ +@msg the image would be {w} × {h} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {_1} megapixels: lower the scale or resolution изображение имело бы размер {w} × {h} пикселей; растровый экспорт ограничен {MAX_RASTER_SIDE} пикселями по стороне и {_1} мегапикселями: уменьшите масштаб или разрешение +@msg the optimised image can't be read back: {e} оптимизированное изображение не удаётся прочитать обратно: {e} +@msg the pixel buffer doesn't match the image size буфер пикселей не соответствует размеру изображения +@msg the preview: {e} изображение для просмотра: {e} +@msg the request may not read the New Document profiles folder запросу не разрешено читать папку профилей нового документа +@msg the shapes are too degenerate to combine фигуры слишком вырождены для объединения +@msg the shapes cross themselves and each other too often to combine фигуры слишком часто пересекают себя и друг друга для объединения +@msg the swatch exchange file holds more than {ASE_MAX_ENTRIES} colors and groups в файле обмена образцами больше {ASE_MAX_ENTRIES} цветов и групп +@msg the swatch exchange file is cut short файл обмена образцами обрезан +@msg the {_1} plate can't be read: {e} не удаётся прочитать пластину {_1}: {e} +@msg this binary DXF file is damaged or cut short этот двоичный файл DXF повреждён или обрезан +@msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that это чертёж DWG, который невозможно прочитать: сохраните его из приложения CAD как DXF и откройте его +@msg too many classes слишком много классов +@msg too many groups слишком много групп +@msg too many layers for PSD (at most 32767) слишком много слоёв для PSD (не более 32767) +@msg too many swatch exchange blocks слишком много блоков в файле обмена образцами +@msg unknown colour profile: {0} неизвестный цветовой профиль: {0} +@msg {_1} clashes with a built-in profile name {_1} совпадает с именем встроенного профиля +@msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} пикселей — слишком большой размер для GIF (не более 65535 пикселей по стороне): уменьшите разрешение +@msg {_1}: no usable transform to/from sRGB {_1}: нет пригодного преобразования в sRGB или из sRGB +@msg {name} is not a CMYK profile {name} — не профиль CMYK +@msg {name} is not an RGB profile {name} — не профиль RGB +@msg {path}: could not be read {path}: не удалось прочитать +@msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} пикселей — слишком большой размер для JPEG (не более 65535 пикселей по стороне): уменьшите разрешение +@msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} пикселей — слишком большой размер для Targa (не более 65535 пикселей по стороне): уменьшите разрешение +@msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} пикселей — слишком большой размер для PSD (не более {MAX_SIDE} пикселей по стороне): уменьшите разрешение diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 1aad2f3653..abf9b23a81 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -693,6 +693,8 @@ font-technology Variable Variabla Crop Image Beskär bild Image Tools Bildverktyg Remove Background… Ta bort bakgrund… + Draw (Pencil) Rita (blyertspenna) + Place Image Placera bild Remove Background Ta bort bakgrund Background Bakgrund Edge Color Kantfärg @@ -4398,6 +4400,7 @@ axis Both Båda @msg `select` must be true or false `select` måste vara true eller false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` och `maxAnchors` måste vara heltal @msg `{key}` must be a number not below 0 `{key}` måste vara ett tal som inte är under 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` måste vara fyra tal som inte är under 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` måste vara [x, y, width, height]: fyra ändliga tal, där bredd och höjd inte är negativa @msg `{min_key}` is greater than `{max_key}` `{min_key}` är större än `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` ange minst en av `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index bb79a093ec..4f9f03e5f6 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -3,6 +3,20 @@ use super::*; const ZH: fn() -> Lang = || Lang::from_code("zh-hant").expect("zh-hant registered"); +#[test] +fn gujarati_resolves_locales_and_takes_the_singular_for_zero_and_one() { + // The catalog is present even when this web build cannot display it. + let gu = Lang(LANGUAGES.iter().find(|l| l.code == "gu").expect("gu registered")); + let selectable = gu.0.available().then_some(gu); + assert_eq!(Lang::from_code("gu"), selectable); + for tag in ["gu", "gu-IN", "gu_IN.UTF-8", "GU-in", "gu-Gujr-IN"] { + assert_eq!(lang_from_tag(tag), selectable, "{tag}"); + } + assert_eq!(gu.name(), "ગુજરાતી"); + assert_eq!([0, 1, 2, 21].map(gu.0.plural), [0, 0, 1, 1]); + assert_eq!(tr(gu, "Save"), "સાચવો"); +} + #[test] fn hindi_resolves_locales_and_uses_zero_one_plurals() { let hi = Lang::from_code("hi").expect("hi registered"); @@ -209,6 +223,17 @@ fn tags_map_to_languages() { assert_eq!(lang_from_tag("_"), None); } +#[test] +fn gujarati_web_selection_requires_a_bundled_interface_font() { + let gu = LANGUAGES.iter().find(|l| l.code == "gu").expect("Gujarati catalog"); + assert!(gu.available_with_fonts(false, false), "native installed-font fallback"); + assert!(!gu.available_with_fonts(true, false), "web must not offer missing glyphs"); + assert!(gu.available_with_fonts(true, true)); + assert!(LANGUAGES[0].available_with_fonts(true, false), "English fallback"); + assert_eq!(Lang::from_code("gu").is_some(), gu.available()); + assert_eq!(Lang::for_picker().iter().any(|l| l.code() == "gu"), gu.available()); +} + /// The Bosnian, Georgian, Ukrainian, Hungarian, Korean, Vietnamese, Swedish and Catalan tags of /// [`tags_map_to_languages`]. fn other_tags_map_to_languages() { @@ -823,6 +848,11 @@ fn is_devanagari(c: char) -> bool { matches!(c as u32, 0x0900..=0x097F | 0x1CD0..=0x1CFF | 0xA8E0..=0xA8FF) } +#[cfg(feature = "interface-fonts")] +fn is_gujarati(c: char) -> bool { + matches!(c as u32, 0x0A80..=0x0AFF) +} + /// The UI fonts draw every character the catalogs use, in every family the UI draws text with /// (the web build has no system fonts to fall back to). Chinese and Japanese characters come from /// the craft-fonts build input, which is optional: without it they are left to the installed @@ -836,6 +866,7 @@ fn bundled_fonts_cover_the_catalogs() { let japanese_ui_font = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.is_japanese()); let georgian_ui_font = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.is_georgian()); let devanagari_ui_font = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.scripts.contains(&"Deva")); + let gujarati_ui_font = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.scripts.contains(&"Gujr")); let ctx = egui::Context::default(); crate::theme::install_fonts(&ctx); let mut output = ctx.run_ui(egui::RawInput::default(), |_| {}); @@ -859,6 +890,7 @@ fn bundled_fonts_cover_the_catalogs() { && (cjk_checked || !is_cjk(*c)) && (georgian_ui_font || !is_georgian(*c)) && (devanagari_ui_font || !is_devanagari(*c)) + && (gujarati_ui_font || !is_gujarati(*c)) }) .collect(); for family in &families { diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 2e169f550a..f1f0814f31 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -685,6 +685,8 @@ font-technology Variable Değişken Crop Image Resmi Kırp Image Tools Resim Araçları Remove Background… Arka Planı Kaldır… + Draw (Pencil) Çiz (Kurşun Kalem) + Place Image Resim Yerleştir Remove Background Arka Planı Kaldır Background Arka Plan Edge Color Kenar Rengi @@ -4247,6 +4249,7 @@ axis Both Her İkisi @msg `select` must be true or false `select` true veya false olmalıdır @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ve `maxAnchors` tam sayı olmalıdır @msg `{key}` must be a number not below 0 `{key}` 0'dan küçük olmayan bir sayı olmalıdır +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 0'dan küçük olmayan dört sayı olmalıdır: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` [x, y, width, height] olmalıdır: dört sonlu sayı, genişlik ve yükseklik negatif olmamalı @msg `{min_key}` is greater than `{max_key}` `{min_key}`, `{max_key}` değerinden büyük @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` şunlardan en az birini verin: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index e4541b4b31..c59dbaeca7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -699,6 +699,8 @@ font-technology Variable Варіативні Crop Image Обрізати зображення Image Tools Інструменти зображення Remove Background… Видалити фон… + Draw (Pencil) Малювати (Олівець) + Place Image Помістити зображення Remove Background Видалити фон Background Фон Edge Color Колір краю @@ -4526,6 +4528,7 @@ axis Both Обидві @msg `select` must be true or false `select` має бути true або false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` і `maxAnchors` мають бути цілими числами @msg `{key}` must be a number not below 0 `{key}` має бути числом, не меншим за 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` має бути чотирма числами, не меншими за 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` має бути [x, y, width, height]: чотири скінченні числа, ширина й висота невід’ємні @msg `{min_key}` is greater than `{max_key}` `{min_key}` більше за `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажіть принаймні один із параметрів `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 62888f86cc..f1a145b133 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -806,6 +806,8 @@ font-technology Variable Biến thiên Crop Image Cắt xén ảnh Image Tools Công cụ hình ảnh Remove Background… Xóa nền… + Draw (Pencil) Vẽ (Bút chì) + Place Image Đặt hình ảnh Remove Background Xóa nền Background Nền Edge Color Màu cạnh @@ -4575,6 +4577,7 @@ theme Appearance Giao diện @msg `select` must be true or false `select` phải là true hoặc false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` và `maxAnchors` phải là số nguyên @msg `{key}` must be a number not below 0 `{key}` phải là một số không nhỏ hơn 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` phải là bốn số không nhỏ hơn 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` phải là [x, y, width, height]: bốn số hữu hạn, chiều rộng và chiều cao không âm @msg `{min_key}` is greater than `{max_key}` `{min_key}` lớn hơn `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` hãy cung cấp ít nhất một trong `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index ef10c08695..662d30b71c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -650,6 +650,8 @@ antiAlias Sharp 锐利 Crop Image 裁剪图像 Image Tools 图像工具 Remove Background… 移除背景… + Draw (Pencil) 绘制 (铅笔) + Place Image 置入图像 Remove Background 移除背景 Background 背景 Edge Color 边缘颜色 @@ -4695,6 +4697,7 @@ imageTrace Outline 轮廓 @msg `select` must be true or false `select` 必须是 true 或 false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必须是整数 @msg `{key}` must be a number not below 0 `{key}` 必须是不小于 0 的数字 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 必须是四个不小于 0 的数字:[tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必须是 [x, y, width, height]:四个有限数字,宽度和高度不能为负 @msg `{min_key}` is greater than `{max_key}` `{min_key}` 大于 `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 请至少提供以下其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -9149,3 +9152,119 @@ breakText Glyphs 字形 @msg {what} can't be intertwined {what} 不能交织 @msg {what} doesn't fit the paper with its bleed and marks {what} 连同出血和标记在纸张上放不下 @msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} 会打印成超过 {MAX_TILES} 个拼贴:请将其缩小 + +# Messages of the effects, 3D, image export, color profiles, swatch exchange files, color books and DXF import. +@msg 3D effects require finite coordinates within 100 million points 3D 效果需要 1 亿点以内的有限坐标 +@msg 3D effects require one to ten lights 3D 效果需要一到十个光源 +@msg BMP depth {_1}: 1, 4, 8, 16, 24 or 32 bits per pixel BMP 位深度 {_1}:须为每像素 1、4、8、16、24 或 32 位 +@msg BMP encoding failed: {width} × {height} pixels doesn't fit the format or the pixel buffer BMP 编码失败:{width} × {height} 像素与该格式或像素缓冲区不符 +@msg GIF encoding failed: the palette image is malformed GIF 编码失败:调板图像格式错误 +@msg GIF encoding failed: {e} GIF 编码失败:{e} +@msg JPEG colour profile: {e} JPEG 颜色配置文件:{e} +@msg JPEG encoding failed: pixel buffer doesn't match the image size JPEG 编码失败:像素缓冲区与图像尺寸不符 +@msg JPEG encoding failed: {e} JPEG 编码失败:{e} +@msg PNG encoding failed: pixel buffer doesn't match the image size PNG 编码失败:像素缓冲区与图像尺寸不符 +@msg PNG encoding failed: the palette image is malformed PNG 编码失败:调板图像格式错误 +@msg PNG encoding failed: {e} PNG 编码失败:{e} +@msg PNG encoding failed: {width} × {height} pixels is not a valid image size PNG 编码失败:{width} × {height} 像素不是有效的图像尺寸 +@msg PSD colour profile: {e} PSD 颜色配置文件:{e} +@msg PSD encoding failed: the pixel buffer doesn't match the image size PSD 编码失败:像素缓冲区与图像尺寸不符 +@msg Revolve profile contains invalid coordinates 回转轮廓包含无效坐标 +@msg Revolve profile is too complex 回转轮廓过于复杂 +@msg Revolve projection is not finite 回转投影不是有限值 +@msg Revolve requires finite profile coordinates within 100 million points 回转需要 1 亿点以内的有限轮廓坐标 +@msg Revolve surface is too complex; reduce Segments or simplify the profile 回转表面过于复杂;请减少分段数或简化轮廓 +@msg Revolve surface is too complex; simplify the profile 回转表面过于复杂;请简化轮廓 +@msg Revolve visibility cannot split the projected boundary 回转可见性计算无法分割投影边界 +@msg Revolve visibility creates too many fragments 回转可见性计算产生的碎片过多 +@msg Revolve visibility creates too many vector anchors 回转可见性计算产生的矢量锚点过多 +@msg Revolve visibility exceeds its geometry work limit 回转可见性计算超出几何运算上限 +@msg Revolve visibility exceeds its spatial index limit 回转可见性计算超出空间索引上限 +@msg Revolve visibility has too many boundary crossings 回转可见性计算中的边界交叉过多 +@msg Revolve visibility has too many input anchors 回转可见性计算的输入锚点过多 +@msg Revolve visibility requires convex cap sectors 回转可见性计算需要凸形端盖扇区 +@msg Revolve visibility requires finite projected faces 回转可见性计算需要有限的投影面 +@msg SVG options: {e} SVG 选项:{e} +@msg Save for Web settings: {e} 存储为 Web 所用格式设置:{e} +@msg TIFF colour profile: {e} TIFF 颜色配置文件:{e} +@msg TIFF encoding failed: the pixel buffer doesn't match the image size TIFF 编码失败:像素缓冲区与图像尺寸不符 +@msg TIFF encoding failed: {e} TIFF 编码失败:{e} +@msg Targa depth {_1}: 16, 24 or 32 bits per pixel Targa 位深度 {_1}:须为每像素 16、24 或 32 位 +@msg Targa encoding failed: the pixel buffer doesn't match the image size Targa 编码失败:像素缓冲区与图像尺寸不符 +@msg The 3D projection is not finite 3D 投影不是有限值 +@msg The 3D shadow is too complex; turn Shadows off or simplify the art 3D 阴影过于复杂;请关闭阴影或简化图稿 +@msg The shape contains invalid coordinates 形状包含无效坐标 +@msg Too many Revolve faces for visibility evaluation 回转的面过多,无法进行可见性计算 +@msg VectorCraft reads at most {ASE_MAX_ENTRIES} colors and groups from a swatch exchange file, and this library has {entries} VectorCraft 最多从色板交换文件中读取 {ASE_MAX_ENTRIES} 个颜色和组,而此库有 {entries} 个 +@msg WebP encoding failed: {e} WebP 编码失败:{e} +@msg [{j}] `{_1}`: {why} [{j}] `{_1}`:{why} +@msg `answers[{i}]` {e} `answers[{i}]` {e} +@msg `view` is smallThumb, mediumThumb, largeThumb, smallList or largeList `view` 必须是 smallThumb、mediumThumb、largeThumb、smallList 或 largeList +@msg a line can't run from a point to itself 曲线不能从一个点连到该点自身 +@msg a line needs at least two points 曲线至少需要两个点 +@msg a number cannot be read 无法读取数字 +@msg a swatch exchange block is too long 色板交换文件中的某个块过长 +@msg a swatch exchange name is too long 色板交换文件中的某个名称过长 +@msg a {sw} × {sh} image can't be resized to {width} × {height} pixels 无法将 {sw} × {sh} 的图像调整为 {width} × {height} 像素 +@msg a {width} × {height} image is too large {width} × {height} 的图像过大 +@msg a {width} × {height} image: {e} {width} × {height} 的图像:{e} +@msg an image could not be written: {e} 无法写入某个图像:{e} +@msg an image that couldn't be read was left out 已略去一个无法读取的图像 +@msg can't open “{name}”: {e} 无法打开“{name}”:{e} +@msg cannot align zip entries to {align} bytes 无法将 zip 条目对齐到 {align} 字节 +@msg cannot open the automation {kind} root `{_1}`: {e} 无法打开自动化 {kind} 根目录 `{_1}`:{e} +@msg cannot read profile: {0} 无法读取配置文件:{0} +@msg color book version {version} isn't supported (only 1) 不支持色谱版本 {version} (仅支持 1) +@msg color books in color space {other} aren't supported (only RGB, CMYK and Lab) 不支持色彩空间为 {other} 的色谱 (仅支持 RGB、CMYK 和 Lab) +@msg file name too long to zip: {_1} 文件名过长,无法放入 zip:{_1} +@msg file name too long to zip: {name} 文件名过长,无法放入 zip:{name} +@msg freeform line {i}: {e} 自由曲线 {i}:{e} +@msg freeform point {i}: {e} 自由点 {i}:{e} +@msg give `{key}` or `{other}`, not both 请提供 `{key}` 或 `{other}`,不要同时提供两者 +@msg grayscale profiles can't be working spaces yet 灰度配置文件目前还不能用作工作空间 +@msg input contains NaN or infinite coordinates 输入包含 NaN 或无穷大坐标 +@msg input path could not be closed 无法闭合输入路径 +@msg invalid ICC profile: {0} ICC 配置文件无效:{0} +@msg invalid scale {scale} 缩放比例 {scale} 无效 +@msg its localized names take more than {_1} KB 其本地化名称超过 {_1} KB +@msg its objects are nested too deep ({e}) 其对象嵌套过深 ({e}) +@msg its tag table lists the `{_1}` tag more than once 其标签表多次列出 `{_1}` 标签 +@msg not a graphic style library: {e} 不是图形样式库:{e} +@msg not a swatch library (.vcswatches or .gpl) 不是色板库 (.vcswatches 或 .gpl) +@msg not a swatch library (format `{_1}`) 不是色板库 (格式为 `{_1}`) +@msg not a swatch library: {e} 不是色板库:{e} +@msg not an action set file ({e}) 不是动作组文件 ({e}) +@msg profile colour space {other} is not RGB, CMYK or Gray 配置文件的色彩空间 {other} 不是 RGB、CMYK 或灰度 +@msg regular expression too large 正则表达式过大 +@msg script file is not UTF-8 text: {value} 脚本文件不是 UTF-8 文本:{value} +@msg stop {i}: {e} 色标 {i}:{e} +@msg swatch exchange version {major}.{minor} isn't supported (only 1.x) 不支持色板交换文件版本 {major}.{minor} (仅支持 1.x) +@msg the color book holds more than {ASE_MAX_ENTRIES} colors 色谱包含的颜色超过 {ASE_MAX_ENTRIES} 种 +@msg the color book is cut short 色谱数据不完整 +@msg the color book's names take more than {_1} MB 色谱的名称超过 {_1} MB +@msg the image would be {w} × {h} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {_1} megapixels: lower the scale or resolution 图像将为 {w} × {h} 像素;栅格导出限制为每边 {MAX_RASTER_SIDE} 像素和 {_1} 百万像素:请降低缩放比例或分辨率 +@msg the optimised image can't be read back: {e} 无法回读优化后的图像:{e} +@msg the pixel buffer doesn't match the image size 像素缓冲区与图像尺寸不符 +@msg the preview: {e} 预览:{e} +@msg the request may not read the New Document profiles folder 此请求无权读取新建文档配置文件文件夹 +@msg the shapes are too degenerate to combine 这些形状过于退化,无法合并 +@msg the shapes cross themselves and each other too often to combine 这些形状的自身交叉和相互交叉过多,无法合并 +@msg the swatch exchange file holds more than {ASE_MAX_ENTRIES} colors and groups 色板交换文件包含的颜色和组超过 {ASE_MAX_ENTRIES} 个 +@msg the swatch exchange file is cut short 色板交换文件不完整 +@msg the {_1} plate can't be read: {e} 无法读取 {_1} 印版:{e} +@msg this binary DXF file is damaged or cut short 此二进制 DXF 文件已损坏或不完整 +@msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that 这是 DWG 图纸,无法读取:请在 CAD 应用中将其保存为 DXF,然后打开该文件 +@msg too many classes 字符类过多 +@msg too many groups 分组过多 +@msg too many layers for PSD (at most 32767) 图层过多,PSD 无法容纳 (最多 32767 个) +@msg too many swatch exchange blocks 色板交换文件中的块过多 +@msg unknown colour profile: {0} 未知的颜色配置文件:{0} +@msg {_1} clashes with a built-in profile name {_1} 与内置配置文件名称冲突 +@msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} 像素对 GIF 而言过大 (每边最多 65535 像素):请降低分辨率 +@msg {_1}: no usable transform to/from sRGB {_1}:没有可用的与 sRGB 之间的转换 +@msg {name} is not a CMYK profile {name} 不是 CMYK 配置文件 +@msg {name} is not an RGB profile {name} 不是 RGB 配置文件 +@msg {path}: could not be read {path}:无法读取 +@msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} 像素对 JPEG 而言过大 (每边最多 65535 像素):请降低分辨率 +@msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} 像素对 Targa 而言过大 (每边最多 65535 像素):请降低分辨率 +@msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} 像素对 PSD 而言过大 (每边最多 {MAX_SIDE} 像素):请降低分辨率 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d051e13ad1..9976538c6f 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -667,6 +667,8 @@ font-technology Variable 可變 Crop Image 裁切影像 Image Tools 影像工具 Remove Background… 移除背景… + Draw (Pencil) 繪製 (鉛筆) + Place Image 置入影像 Remove Background 移除背景 Background 背景 Edge Color 邊緣顏色 @@ -2408,6 +2410,7 @@ file-filter Templates 範本 @msg `select` must be true or false `select` 必須是 true 或 false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必須是整數 @msg `{key}` must be a number not below 0 `{key}` 必須是不小於 0 的數字 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 必須是四個不小於 0 的數字:[tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必須是 [x, y, width, height]:四個有限數字,寬度和高度不能為負 @msg `{min_key}` is greater than `{max_key}` `{min_key}` 大於 `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 請至少提供下列其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 924950c1cb..eec2a66831 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -171,14 +171,20 @@ mod tests { app.run("file.save", json!({"path": "/out/b.vectorcraft"})).unwrap(); assert_eq!((app.ui.status.as_str(), &app.ui.status_notes), ("Saved /out/b.vectorcraft", &None)); - // A PDF export of a knockout group and of type in a font that isn't installed. + // A PDF export of a knockout group whose soft masks would draw a brushed stroke, and of type + // in a font that isn't installed. app.run("text.create", json!({"x": 5, "y": 20, "text": "Ab", "font": "Nonexistent Sans Probe"})).unwrap(); - app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); + let lower = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone(); + let upper = app.run("shape.rectangle", json!({"x": 25, "y": 15, "width": 30, "height": 20})).unwrap()["id"].clone(); + app.run("brush.apply", json!({"ids": [upper], "name": "3 pt. Round"})).unwrap(); + app.run("object.setProps", json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap(); + app.run("select.set", json!({"ids": [lower, upper]})).unwrap(); app.run("object.group", json!({})).unwrap(); app.run("transparency.set", json!({"knockout": "on"})).unwrap(); let r = export(&mut app, Some("pdf"), Some("/out/c.pdf".into()), &json!({})).unwrap(); let notes = notes_of(&r); assert_eq!(notes.len(), 2, "{r}"); + assert!(notes.iter().any(|n| n.contains("brushed strokes")), "{r}"); assert_eq!(app.ui.status, exported_with_notes("/out/c.pdf", notes.len(), ¬es[0])); assert_eq!(app.ui.status_notes, Some((app.ui.status.clone(), notes))); } diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index 7b82af2e53..ded320a5aa 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -7,20 +7,40 @@ use serde_json::json; use crate::theme::Tokens; use crate::{VectorcraftApp, menus}; +/// Engine commands the palette leaves out, each with the UI command of the same name that runs it +/// the way the panels and menus do: (engine id, UI command id). The Layers panel's Delete +/// Selection asks before deleting a layer that holds art, and the other UI commands open the +/// command's dialog or file chooser. Scripts and the control channel still run the engine ids. +const WRAPPED: &[(&str, &str)] = &[ + ("layer.delete", "ui.layerDelete"), + ("brush.new", "ui.newBrush"), + ("brush.options", "ui.brushOptions"), + ("symbol.options", "ui.symbolOptions"), + ("svgFilter.import", "ui.importSvgFilter"), + ("text.convertListToText", "type.convertListToText"), +]; + /// Everything the palette searches: (label, command id or `tool:`, shortcut). pub fn items() -> Vec<(String, String, String)> { let mut items = vec![]; - // Aliases kept for older scripts run a command that is listed already. + // Aliases kept for older scripts run a command that is listed already, and a wrapped engine + // command is listed as its UI command. let plugin_commands = vectorcraft_engine::cmd::plugin::command_specs(); - for c in vectorcraft_engine::command_specs().iter().filter(|c| !vectorcraft_engine::cmd::is_alias(c.id)).chain(plugin_commands) { - let path = c.menu.join(" › "); - let label = if path.is_empty() { c.label.to_string() } else { format!("{path} › {}", c.label) }; - items.push((label, c.id.to_string(), menus::shortcut_of(c.id).unwrap_or("").to_string())); + let listed = |id: &str| !vectorcraft_engine::cmd::is_alias(id) && !WRAPPED.iter().any(|(engine, _)| *engine == id); + let labeled = |menu: &[&str], label: &str| { + let path = menu.join(" › "); + if path.is_empty() { label.to_string() } else { format!("{path} › {label}") } + }; + for c in vectorcraft_engine::command_specs().iter().filter(|c| listed(c.id)).chain(plugin_commands) { + items.push((labeled(c.menu, c.label), c.id.to_string(), menus::shortcut_of(c.id).unwrap_or("").to_string())); } // `plugin.dialog` runs nothing without an id: [`build`] lists each filter instead. A script of - // the Scripts folder runs from File › Scripts, by its path. + // the Scripts folder runs from File › Scripts, by its path. A UI command that wraps an engine + // command takes that command's menu path, so a search for the path still finds it. for c in menus::UI_COMMANDS.iter().filter(|c| c.0 != "plugin.dialog" && c.0 != crate::scripts_menu::COMMAND) { - items.push((c.1.to_string(), c.0.to_string(), menus::shortcut_of(c.0).unwrap_or("").to_string())); + let engine = WRAPPED.iter().find(|(_, ui)| *ui == c.0).and_then(|(engine, _)| vectorcraft_engine::find_command(engine)); + let label = engine.map_or_else(|| c.1.to_string(), |e| labeled(e.menu, c.1)); + items.push((label, c.0.to_string(), menus::shortcut_of(c.0).unwrap_or("").to_string())); } for tool in vectorcraft_tools::catalog::all_tools() { items.push((tool.label.to_string(), format!("tool:{}", tool.id), crate::shortcut_editor::tool_shortcut(tool.id).unwrap_or("").to_string())); @@ -40,8 +60,9 @@ pub(crate) struct Entry { shortcut: String, /// The label in the UI language. shown: String, - /// The English label (what agents document), the shown label and the id, lowercased, one per - /// line: a query word (which has no line break) matches within one of them. + /// The English label (what agents document), the shown label and the id (and the id of the + /// engine command a UI command wraps), lowercased, one per line: a query word (which has no + /// line break) matches within one of them. haystack: String, } @@ -71,7 +92,12 @@ fn entries(ctx: &egui::Context, lang: crate::i18n::Lang) -> Arc> { /// item (`plugin:`), whose name is not translated. fn build(lang: crate::i18n::Lang) -> Vec { let entry = |label: String, shown: String, id: String, shortcut: String| { - let haystack = format!("{}\n{}\n{}", label.to_lowercase(), shown.to_lowercase(), id.to_lowercase()); + let mut haystack = format!("{}\n{}\n{}", label.to_lowercase(), shown.to_lowercase(), id.to_lowercase()); + // A UI command that wraps an engine command is also found by the engine command's id. + if let Some((engine, _)) = WRAPPED.iter().find(|(_, ui)| *ui == id) { + haystack.push('\n'); + haystack.push_str(&engine.to_lowercase()); + } Entry { id, shortcut, shown, haystack } }; let mut out: Vec = items() @@ -250,6 +276,73 @@ mod tests { assert_eq!((st.selection.len(), st.selection.key), (2, None), "both objects stay selected, without a key"); } + /// An engine command that a UI command of the same name runs is listed once, as the UI command: + /// the Layers panel's Delete Selection, New Brush…, Brush Options…, Symbol Options…, Import SVG + /// Filter… and Convert to Text each have one result. + #[test] + fn a_wrapped_engine_command_is_listed_once_as_its_ui_command() { + let items = items(); + for (engine, ui) in WRAPPED { + let spec = vectorcraft_engine::command_specs().iter().find(|c| c.id == *engine).unwrap_or_else(|| panic!("{engine} is a command")); + let wrapper = menus::UI_COMMANDS.iter().find(|c| c.0 == *ui).unwrap_or_else(|| panic!("{ui} is a UI command")); + assert_eq!(spec.label, wrapper.1, "{engine} and {ui} share a name"); + assert!(items.iter().all(|(_, id, _)| id != engine), "{engine} is left out"); + let listed: Vec<&str> = items.iter().filter(|(_, id, _)| id == ui).map(|(label, ..)| label.as_str()).collect(); + let path: String = spec.menu.iter().map(|m| format!("{m} › ")).collect(); + assert_eq!(listed, [format!("{path}{}", wrapper.1)], "{ui} is listed once, with {engine}'s menu path"); + } + // A search for Delete Selection, or for its menu path, finds the Layers panel's, and not the + // engine command that deletes without asking; the path translates. + let entries = build(Lang::EN); + for (engine, ui) in WRAPPED { + assert!(entries.iter().any(|e| e.id == *ui && e.haystack.contains(&engine.to_lowercase())), "a search for {engine} finds {ui}"); + } + for query in ["delete selection", "layers delete", "layer.delete"] { + let found: Vec<&str> = + entries.iter().filter(|e| query.split_whitespace().all(|w| e.haystack.contains(w))).map(|e| e.id.as_str()).collect(); + assert!(found.contains(&"ui.layerDelete") && !found.contains(&"layer.delete"), "{query}: {found:?}"); + } + let zh = Lang::from_code("zh-hant").unwrap(); + let shown = build(zh).into_iter().find(|e| e.id == "ui.layerDelete").map(|e| e.shown).unwrap(); + let want = ["Window", "Layers", "Delete Selection"].map(|s| crate::i18n::tr(zh, s)).join(" › "); + assert_eq!(shown, want); + } + + /// Every result a search for Delete Selection gives asks before deleting a layer that holds art: + /// Cancel keeps it, and OK deletes it in one undo step. A script's `layer.delete` still deletes + /// at once. + #[test] + fn a_pick_of_delete_selection_asks_before_deleting_a_layer_with_art() { + let query = "delete selection"; + let picks: Vec = + build(Lang::EN).into_iter().filter(|e| query.split_whitespace().all(|w| e.haystack.contains(w))).map(|e| e.id).collect(); + assert!(!picks.is_empty(), "the search finds Delete Selection"); + let node = |v: &serde_json::Value| vectorcraft_doc::NodeId(v.as_u64().unwrap()); + let exists = |app: &VectorcraftApp, v: &serde_json::Value| app.session.active().unwrap().doc.node(node(v)).is_some(); + let undo = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len(); + let asked = |app: &VectorcraftApp| app.ui.dialog.as_ref().is_some_and(|d| d.kind == crate::dialogs::confirm::KIND); + for pick in &picks { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let layer = app.run("layer.new", json!({})).unwrap()["id"].clone(); + let art = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone(); + app.run("select.none", json!({})).unwrap(); + let before = undo(&app); + run_pick(&mut app, pick); + assert!(asked(&app), "{pick} asks first: {}", app.ui.status); + assert!(exists(&app, &layer) && exists(&app, &art) && undo(&app) == before, "{pick}: nothing is deleted yet"); + crate::dialogs::cancel(&mut app); + assert!(exists(&app, &layer) && exists(&app, &art) && undo(&app) == before, "{pick}: Cancel keeps the layer"); + run_pick(&mut app, pick); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(!exists(&app, &layer) && !exists(&app, &art), "{pick}: OK deletes the layer and its art"); + assert_eq!(undo(&app), before + 1, "{pick}: one undo step"); + app.run("edit.undo", json!({})).unwrap(); + app.run("layer.delete", json!({ "ids": [layer] })).unwrap(); + assert!(app.ui.dialog.is_none() && !exists(&app, &layer), "a script deletes at once"); + } + } + /// #4864: a pick of an Asset Export command, which the palette runs with no params, opens the /// Asset Export panel instead of failing on the missing params or exporting nowhere. #[test] diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index ecaafba983..d003fbcbce 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -1229,20 +1229,40 @@ fn axes_cmd(app: &mut VectorcraftApp, axes: Value, phase: widgets::Live) { } /// The Control bar's type controls while type is selected or edited: the Character link (the -/// Character panel in a popover), the font family and style menus and the Font Size combo. -pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { - let Some((s, _)) = text_style(app) else { return }; - let link = ui - .link(egui::RichText::new(tl!("Character:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline()) - .on_hover_text(tl!("Character options")); +/// Character panel in a popover), the font family and style menus and the Font Size combo. They +/// show whole, with `reserve` left after them, or not at all: it returns false when type is selected +/// or edited and the row has no room for them. +pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) -> bool { + let Some((s, _)) = text_style(app) else { return true }; + if ui.available_width() < control_bar_width(ui) + reserve { + return false; + } + let link = ui.link(control_bar_link_text(ui)).on_hover_text(tl!("Character options")); widgets::popover(&link, link.clicked(), |ui| { ui.set_width(260.0); show_owned(app, ui, crate::font_menu::WheelOwner::ControlPopover(link.id)); }); - font_pickers(app, ui, &s, ("cb-font", "cb-font-style"), (150.0, 96.0)); - size_field(app, ui, "cb-font-size", 96.0); + let [family, style, size] = CONTROL_BAR_WIDTHS; + font_pickers(app, ui, &s, ("cb-font", "cb-font-style"), (family, style)); + size_field(app, ui, "cb-font-size", size); ui.add_space(4.0); crate::chrome::group_separator(ui); + true +} + +/// The Control bar's widths of the font family menu, the font style dropdown and the Font Size combo. +const CONTROL_BAR_WIDTHS: [f32; 3] = [150.0, 96.0, 96.0]; + +/// The text of the Control bar's Character link, as [`control_bar`] draws it. +fn control_bar_link_text(ui: &Ui) -> egui::RichText { + egui::RichText::new(tl!("Character:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline() +} + +/// The room [`control_bar`]'s link, menus and Font Size combo take in the Control bar's row, with +/// the spacing after each. +fn control_bar_width(ui: &Ui) -> f32 { + let spacing = ui.spacing().item_spacing.x; + widgets::text_width(ui, control_bar_link_text(ui)) + CONTROL_BAR_WIDTHS.iter().sum::() + 4.0 * spacing } /// The Contextual Task Bar's widths of the font family menu, the font style dropdown and the diff --git a/crates/ui-egui/src/panels/layers/rows.rs b/crates/ui-egui/src/panels/layers/rows.rs index 9f80d93791..b898ce8bf8 100644 --- a/crates/ui-egui/src/panels/layers/rows.rs +++ b/crates/ui-egui/src/panels/layers/rows.rs @@ -268,6 +268,66 @@ fn disclosure(ui: &Ui, view: &View, n: &Node, (r, x): (egui::Rect, f32), open: b } } +/// The row's name label or inline rename field. Returns (name_end, name_rect). +fn row_name(ui: &mut Ui, view: &View, item: &Row, r: egui::Rect, x: f32, out: &mut Out) -> (f32, egui::Rect) { + let n = &*item.node; + let doc = view.doc; + let t = &view.t; + let xml_id = view.identify_xml_id; + let name = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { "".to_string() } else { n.identify_label(xml_id) }; + let font = egui::FontId::proportional(13.0); + let renaming: Option<(u64, String)> = ui.data(|d| d.get_temp(rename_id())); + let name_rect = egui::Rect::from_min_max(egui::pos2(x - 2.0, r.center().y - 10.0), egui::pos2(r.right() - 44.0, r.center().y + 10.0)); + let name_end = match renaming { + Some((rid, mut buf)) if rid == n.id.0 => { + let field = rename_id().with("field"); + let kept = crate::widgets::own_field_shift_enter(ui, field, &mut buf, &name); + let mut child = ui.new_child(egui::UiBuilder::new().max_rect(name_rect)); + let te = child + .add(egui::TextEdit::singleline(&mut buf).id(field).align(egui::Align2::LEFT_CENTER).desired_width(name_rect.width()).font(font)); + // Focus the field as it opens. Asking every frame took the focus back from Enter, + // Escape or a click elsewhere, so the rename never ended. + if !te.has_focus() && !te.lost_focus() { + te.request_focus(); + } + if te.lost_focus() { + ui.data_mut(|d| d.remove::<(u64, String)>(rename_id())); + if child.input(|i| !i.key_pressed(egui::Key::Escape)) && buf != name { + out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "name": buf}))); + } + } else { + // Shift+Enter renames it and keeps the field open. + if kept && buf != name { + out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "name": buf.clone()}))); + } + ui.data_mut(|d| d.insert_temp(rename_id(), (n.id.0, buf))); + } + name_rect.right() + } + _ => { + let painter = ui.painter().with_clip_rect(name_rect); + // Generated names ("", "") are translated where painted; the stored name stays English. + // XML IDs stay as ids (not translated). + let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { + tl!("").to_string() + } else if xml_id { + name + } else { + painted_name(n, &n.display_name(), crate::i18n::current()) + }; + let text = paint_name(&painter, egui::pos2(x, r.center().y), &shown, font, if item.dim { t.text_dim } else { t.text }, prints_italic(n)); + // A clipping path's name is underlined, a masked object's with a dashed line. + if item.clip_path { + painter.line_segment([text.left_bottom(), text.right_bottom()], Stroke::new(1.0, t.text)); + } else if n.extras().mask.is_some() { + painter.extend(egui::Shape::dashed_line(&[text.left_bottom(), text.right_bottom()], Stroke::new(1.0, t.text), 3.0, 2.0)); + } + text.right() + } + }; + (name_end, name_rect) +} + /// The widgets of row `item`. pub(super) fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) { if let Some(i) = item.guide { @@ -277,8 +337,7 @@ pub(super) fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) { return spine_row(ui, view, item, out); } let n = &*item.node; - let (depth, clip_path, has_children, open, color) = (item.depth, item.clip_path, item.opens, item.open, item.colour); - let doc = view.doc; + let (depth, has_children, open, color) = (item.depth, item.opens, item.open, item.colour); let t = &view.t; let h = view.h; let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click_and_drag()); @@ -309,60 +368,7 @@ pub(super) fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) { th }); // Name (Preferences › Units › Identify Objects By: Object Name or XML ID). - let xml_id = view.identify_xml_id; - let name = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { "".to_string() } else { n.identify_label(xml_id) }; - let font = egui::FontId::proportional(13.0); - let renaming: Option<(u64, String)> = ui.data(|d| d.get_temp(rename_id())); - let name_rect = egui::Rect::from_min_max(egui::pos2(x - 2.0, r.center().y - 10.0), egui::pos2(r.right() - 44.0, r.center().y + 10.0)); - let name_end; - match renaming { - Some((rid, mut buf)) if rid == n.id.0 => { - let field = rename_id().with("field"); - let kept = crate::widgets::own_field_shift_enter(ui, field, &mut buf, &name); - let mut child = ui.new_child(egui::UiBuilder::new().max_rect(name_rect)); - let te = child.add( - egui::TextEdit::singleline(&mut buf).id(field).align(egui::Align2::LEFT_CENTER).desired_width(name_rect.width()).font(font.clone()), - ); - // Focus the field as it opens. Asking every frame took the focus back from Enter, - // Escape or a click elsewhere, so the rename never ended. - if !te.has_focus() && !te.lost_focus() { - te.request_focus(); - } - if te.lost_focus() { - ui.data_mut(|d| d.remove::<(u64, String)>(rename_id())); - if child.input(|i| !i.key_pressed(egui::Key::Escape)) && buf != name { - out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "name": buf}))); - } - } else { - // Shift+Enter renames it and keeps the field open. - if kept && buf != name { - out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "name": buf.clone()}))); - } - ui.data_mut(|d| d.insert_temp(rename_id(), (n.id.0, buf))); - } - name_end = name_rect.right(); - } - _ => { - let painter = ui.painter().with_clip_rect(name_rect); - // Generated names ("", "") are translated where painted; the stored name stays English. - // XML IDs stay as ids (not translated). - let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { - tl!("").to_string() - } else if xml_id { - name.clone() - } else { - painted_name(n, &n.display_name(), crate::i18n::current()) - }; - let text = paint_name(&painter, egui::pos2(x, r.center().y), &shown, font, if item.dim { t.text_dim } else { t.text }, prints_italic(n)); - name_end = text.right(); - // A clipping path's name is underlined, a masked object's with a dashed line. - if clip_path { - painter.line_segment([text.left_bottom(), text.right_bottom()], Stroke::new(1.0, t.text)); - } else if n.extras().mask.is_some() { - painter.extend(egui::Shape::dashed_line(&[text.left_bottom(), text.right_bottom()], Stroke::new(1.0, t.text), 3.0, 2.0)); - } - } - } + let (name_end, name_rect) = row_name(ui, view, item, r, x, out); // Target circle and selection square. let col_x = r.right() - 43.5; ui.painter().line_segment([egui::pos2(col_x, r.top()), egui::pos2(col_x, r.bottom())], Stroke::new(1.0, t.input_border)); @@ -404,12 +410,18 @@ pub(super) fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) { out.actions.push(("ui.layerOptions".into(), json!({"ids": [n.id.0]}))); } } - // Row menu: Collect for Export on this row (or every highlighted row when this is one of them). + // Row menu: context actions on this row (or every highlighted row when this is one of them). let collect_ids: Vec = if view.rows.contains(&n.id) { view.rows.iter().map(|id| id.0).collect() } else { vec![n.id.0] }; + // Right-clicking an unselected row selects it; a highlighted row keeps multiselection. + if resp.secondary_clicked() && !view.rows.contains(&n.id) { + out.actions.push(("layer.setCurrent".into(), json!({"id": n.id.0}))); + out.rows_after_click = Some(vec![n.id]); + } + if resp.secondary_clicked() { + crate::panels::set_row_keys(ui.ctx(), ROW_KEYS, true); + } resp.context_menu(|ui| { - collect_for_export_menu(ui, true, |multiple| { - out.actions.push(("assets.add".into(), json!({ "ids": collect_ids.clone(), "multiple": multiple }))); - }); + row_context_menu(ui, view, n, &collect_ids, out); }); // Dragging rows: the highlighted rows when this is one of them, else this row. Only the primary // button drags; the secondary button opens the row menu, and a drag with it moves nothing. @@ -428,6 +440,114 @@ pub(super) fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) { drop_target(ui, view, n, r, x - 16.0, open, &resp, out); } +/// The right-click context menu of row `n`. +fn row_context_menu(ui: &mut Ui, view: &View, n: &Node, collect_ids: &[u64], out: &mut Out) { + let is_multi = collect_ids.len() > 1; + // "Layer Options…" / "Options for Selection…" / "Options…" + let options_label = if is_multi { + "Options for Selection…" + } else if n.is_layer() { + "Layer Options…" + } else { + "Options…" + }; + if widgets::menu_item(ui, options_label, true, false) { + out.actions.push(("ui.layerOptions".into(), json!({ "ids": collect_ids }))); + ui.close(); + } + // "Duplicate """ or "Duplicate Selection" + let display = { + let raw = painted_name(n, &n.display_name(), crate::i18n::current()); + if raw.chars().count() > 20 { + let truncated: String = raw.chars().take(20).collect(); + format!("{truncated}…") + } else { + raw + } + }; + let dup_label = if is_multi { tl!("Duplicate Selection").to_string() } else { format!("{} \"{display}\"", tl!("Duplicate")) }; + if widgets::menu_item_name(ui, &dup_label, true, false) { + out.actions.push(("layer.duplicate".into(), json!({ "ids": collect_ids }))); + ui.close(); + } + // "Delete """ or "Delete Selection" + let del_label = if is_multi { tl!("Delete Selection").to_string() } else { format!("{} \"{display}\"", tl!("Delete")) }; + let del_enabled = { + let listed: std::collections::HashSet = collect_ids.iter().map(|&id| NodeId(id)).collect(); + !view.doc.layers.iter().all(|l| listed.contains(&l.id)) + }; + if widgets::menu_item_name(ui, &del_label, del_enabled, false) { + out.actions.push(("layer.delete".into(), json!({ "ids": collect_ids }))); + ui.close(); + } + ui.separator(); + // Lock Others / Unlock All Layers + { + let (lock_cmd, lock_params, lock_enabled) = multi_others_action(view.doc, collect_ids, false); + let lock_label = if lock_cmd == "layer.lockOthers" { "Lock Others" } else { "Unlock All Layers" }; + if widgets::menu_item(ui, lock_label, lock_enabled, false) { + out.actions.push((lock_cmd, lock_params)); + ui.close(); + } + } + // Hide Others / Show All Layers + { + let (hide_cmd, hide_params, hide_enabled) = multi_others_action(view.doc, collect_ids, true); + let hide_label = if hide_cmd == "layer.hideOthers" { "Hide Others" } else { "Show All Layers" }; + if widgets::menu_item(ui, hide_label, hide_enabled, false) { + out.actions.push((hide_cmd, hide_params)); + ui.close(); + } + } + ui.separator(); + // Locate Object + if widgets::menu_item(ui, "Locate Object", true, false) { + out.actions.push(("layer.locate".into(), json!({ "id": n.id.0 }))); + ui.close(); + } + ui.separator(); + // Collect for Export + let ids: Vec = collect_ids.to_vec(); + collect_for_export_menu(ui, true, move |multiple| { + out.actions.push(("assets.add".into(), json!({ "ids": ids.clone(), "multiple": multiple }))); + }); +} + +/// Context menu Others action for `ids`: Lock Others / Unlock All Layers or Hide Others / Show All Layers. +/// When any other layer is unlocked (visible), returns `lockOthers` (`hideOthers`) preserving all layers holding +/// `ids`. When all other layers are locked (hidden) already, returns `unlockAll` (`showAll`). +pub(crate) fn multi_others_action(doc: &Document, ids: &[u64], eye: bool) -> (String, Value, bool) { + let own_layers: std::collections::HashSet = ids.iter().filter_map(|&id| doc.layer_of(NodeId(id))).collect(); + let has_other_active = if eye { + doc.layers.iter().any(|l| !own_layers.contains(&l.id) && l.visible) + } else { + doc.layers.iter().any(|l| !own_layers.contains(&l.id) && !l.locked) + }; + let cmd = match (eye, has_other_active) { + (true, true) => "layer.hideOthers", + (true, false) => "layer.showAll", + (false, true) => "layer.lockOthers", + (false, false) => "layer.unlockAll", + }; + let enabled = match (eye, has_other_active) { + (true, true) | (false, true) => true, + (true, false) => any_layer_state(doc, |n| !n.visible), + (false, false) => any_layer_state(doc, |n| n.locked), + }; + let params = if cmd.ends_with("Others") { json!({ "ids": ids }) } else { json!({}) }; + (cmd.into(), params, enabled) +} + +fn any_layer_state(doc: &Document, pred: impl Fn(&Node) -> bool) -> bool { + let mut found = false; + doc.walk(|n| { + if !found && n.is_layer() && pred(n) { + found = true; + } + }); + found +} + /// Alt-clicking row `n`'s eye (`eye`) or lock: Hide Others or Lock Others, every top-level layer /// but the one holding the row; when they all are hidden (locked) already, Show All Layers (Unlock /// All Layers). Each is one undo step. diff --git a/crates/ui-egui/src/panels/layers/tests.rs b/crates/ui-egui/src/panels/layers/tests.rs index 55fa27ce58..c072a45828 100644 --- a/crates/ui-egui/src/panels/layers/tests.rs +++ b/crates/ui-egui/src/panels/layers/tests.rs @@ -2440,3 +2440,195 @@ fn guide_rows_list_by_index_whatever_the_node_order() { let indexes: Vec = rows.iter().filter_map(|r| r.guide).collect(); assert_eq!(indexes, [2, 1, 0]); } + +/// Right-clicking a row that is not highlighted selects it. +#[test] +fn context_menu_right_click_selects_unselected_row() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + app.session.execute("layer.new", &json!({})).unwrap(); + app.session.execute("layer.highlight", &json!({"ids": [], "mode": "set"})).unwrap(); + let ctx = egui::Context::default(); + // Rows top-first: Layer 2, Layer 1. Neither is highlighted. + let (circles, _) = frame(&mut app, &ctx, vec![], false); + assert_eq!(circles.len(), 2); + assert!(rows_of(&app).is_empty()); + // Secondary-click Layer 1's row (bottom row). + let pos = circles[1] - vec2(100.0, 0.0); // name area + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos)], false); + frame(&mut app, &ctx, vec![button_by(pos, true, egui::PointerButton::Secondary)], false); + frame(&mut app, &ctx, vec![button_by(pos, false, egui::PointerButton::Secondary)], false); + // The row is now highlighted. + let highlighted = rows_of(&app); + let layer1 = app.session.active().unwrap().doc.layers[0].id; + assert!(highlighted.contains(&layer1), "right-click selected the unselected row"); + assert!(crate::panels::has_row_keys(&ctx, super::ROW_KEYS), "secondary click claims row keys"); +} + +/// Right-clicking a row that IS highlighted preserves the multiselection. +#[test] +fn context_menu_right_click_preserves_multiselection() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + app.session.execute("layer.new", &json!({})).unwrap(); + app.session.execute("layer.new", &json!({})).unwrap(); + let doc = &app.session.active().unwrap().doc; + let ids: Vec = doc.layers.iter().map(|l| l.id.0).collect(); + // Highlight all 3 layers. + app.session.execute("layer.highlight", &json!({"ids": ids, "mode": "set"})).unwrap(); + let ctx = egui::Context::default(); + let (circles, _) = frame(&mut app, &ctx, vec![], false); + assert_eq!(circles.len(), 3); + // Secondary-click the middle row. + let pos = circles[1] - vec2(100.0, 0.0); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos)], false); + frame(&mut app, &ctx, vec![button_by(pos, true, egui::PointerButton::Secondary)], false); + frame(&mut app, &ctx, vec![button_by(pos, false, egui::PointerButton::Secondary)], false); + // All 3 must still be highlighted. + assert_eq!(rows_of(&app).len(), 3, "multiselection preserved after right-click on a highlighted row"); +} + +/// layer.delete removes the specified layer. +#[test] +fn context_menu_delete_dispatches_layer_delete() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let layer2_id = app.session.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap(); + let before = app.session.active().unwrap().doc.layers.len(); + // Dispatch layer.delete with explicit ids (what the context menu item does). + app.session.execute("layer.delete", &json!({"ids": [layer2_id]})).unwrap(); + let after = app.session.active().unwrap().doc.layers.len(); + assert_eq!(after, before - 1, "layer.delete removes the layer"); +} + +/// layer.duplicate adds a copy. +#[test] +fn context_menu_duplicate_dispatches_layer_duplicate() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let layer_id = app.session.active().unwrap().doc.layers[0].id.0; + let before_layers = app.session.active().unwrap().doc.layers.len(); + let before_undo = app.session.active().unwrap().history.undo.len(); + app.session.execute("layer.duplicate", &json!({"ids": [layer_id]})).unwrap(); + let after = &app.session.active().unwrap(); + assert_eq!(after.doc.layers.len(), before_layers + 1, "layer.duplicate adds a layer"); + assert_eq!(after.history.undo.len(), before_undo + 1, "one undo step"); +} + +/// layer.locate locates a specified object id without requiring a prior canvas selection. +#[test] +fn context_menu_locate_object_locates_by_id() { + let (mut app, _, [a, _]) = two_rects(); + app.session.execute("select.none", &json!({})).unwrap(); + assert!(app.session.active().unwrap().selection.objects.is_empty()); + // Context menu passes the node's id directly to layer.locate. + let result = app.session.execute("layer.locate", &json!({"id": a.0})); + assert!(result.is_ok(), "layer.locate with id succeeds without selection: {:?}", result.err()); + assert_eq!(app.session.active().unwrap().highlighted_rows(), vec![a]); +} + +/// In a 1-layer document, the Delete action in the context menu is disabled when targeting that layer. +#[test] +fn context_menu_delete_disabled_when_every_layer_targeted() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let doc = &app.session.active().unwrap().doc; + assert_eq!(doc.layers.len(), 1); + let layer_id = doc.layers[0].id; + + // Targeting every layer disables delete. + let listed: std::collections::HashSet = [layer_id].into_iter().collect(); + let del_enabled = !doc.layers.iter().all(|l| listed.contains(&l.id)); + assert!(!del_enabled, "Delete must be disabled when every layer is targeted"); + + // Add a second layer: delete is now enabled for one layer. + app.session.execute("layer.new", &json!({})).unwrap(); + let doc = &app.session.active().unwrap().doc; + assert_eq!(doc.layers.len(), 2); + let del_enabled = !doc.layers.iter().all(|l| listed.contains(&l.id)); + assert!(del_enabled, "Delete is enabled when at least one layer remains"); +} + +/// Others actions (Hide Others / Lock Others) retain all multiselected layers. +#[test] +fn context_menu_others_actions_preserve_multiselected_layers() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + app.session.execute("layer.new", &json!({})).unwrap(); + app.session.execute("layer.new", &json!({})).unwrap(); + let doc = &app.session.active().unwrap().doc; + assert_eq!(doc.layers.len(), 3); + let l1 = doc.layers[0].id.0; + let l2 = doc.layers[1].id.0; + + // Hide Others with multiselection [l1, l2] hides only l3. + let (hide_cmd, hide_params, hide_enabled) = super::rows::multi_others_action(doc, &[l1, l2], true); + assert_eq!(hide_cmd, "layer.hideOthers"); + assert!(hide_enabled); + assert_eq!(hide_params["ids"], json!([l1, l2])); + + // Lock Others with multiselection [l1, l2] locks only l3. + let (lock_cmd, lock_params, lock_enabled) = super::rows::multi_others_action(doc, &[l1, l2], false); + assert_eq!(lock_cmd, "layer.lockOthers"); + assert!(lock_enabled); + assert_eq!(lock_params["ids"], json!([l1, l2])); +} + +/// Context menu Others action resolves an object row to its holding top-level layer. +#[test] +fn context_menu_others_actions_object_row_resolves_to_holding_layer() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + // Layer 1 (current) holding a rectangle. + let rect = app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + let rect_id = rect["id"].as_u64().unwrap(); + + // Create Layer 2 and hide it. + let l2 = app.session.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("layer.setProps", &json!({"ids": [l2], "visible": false})).unwrap(); + + let doc = &app.session.active().unwrap().doc; + assert_eq!(doc.layers.len(), 2); + + // Right-clicking the object in Layer 1: Layer 1 is visible, Layer 2 is hidden. + // There are no other visible layers, so the action should be Show All Layers, enabled. + let (hide_cmd, _hide_params, hide_enabled) = super::rows::multi_others_action(doc, &[rect_id], true); + assert_eq!(hide_cmd, "layer.showAll"); + assert!(hide_enabled, "Show All Layers is enabled because Layer 2 is hidden"); + + // Now make Layer 2 visible, but lock it. + app.session.execute("layer.setProps", &json!({"ids": [l2], "visible": true, "locked": true})).unwrap(); + let doc = &app.session.active().unwrap().doc; + // Right-clicking the object in Layer 1: Layer 1 is unlocked, Layer 2 is locked. + // There are no other unlocked layers, so the action should be Unlock All Layers, enabled. + let (lock_cmd, _lock_params, lock_enabled) = super::rows::multi_others_action(doc, &[rect_id], false); + assert_eq!(lock_cmd, "layer.unlockAll"); + assert!(lock_enabled, "Unlock All Layers is enabled because Layer 2 is locked"); +} + +/// Context menu Show/Unlock All Layers are enabled when sublayers are hidden or locked. +#[test] +fn context_menu_others_actions_sublayer_state_enables_all_commands() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let doc = &app.session.active().unwrap().doc; + assert_eq!(doc.layers.len(), 1); + let l1 = doc.layers[0].id.0; + + // Create a sublayer in Layer 1. + let sub = app.session.execute("layer.newSublayer", &json!({"parent": l1})).unwrap()["id"].as_u64().unwrap(); + + // Case 1: Sublayer is hidden while top layer is visible. + app.session.execute("layer.setProps", &json!({"ids": [sub], "visible": false})).unwrap(); + let doc = &app.session.active().unwrap().doc; + let (hide_cmd, _, hide_enabled) = super::rows::multi_others_action(doc, &[l1], true); + assert_eq!(hide_cmd, "layer.showAll"); + assert!(hide_enabled, "hidden sublayer enables Show All Layers"); + + // Case 2: Sublayer is visible, but locked while top layer is unlocked. + app.session.execute("layer.setProps", &json!({"ids": [sub], "visible": true, "locked": true})).unwrap(); + let doc = &app.session.active().unwrap().doc; + let (lock_cmd, _, lock_enabled) = super::rows::multi_others_action(doc, &[l1], false); + assert_eq!(lock_cmd, "layer.unlockAll"); + assert!(lock_enabled, "locked sublayer enables Unlock All Layers"); +} diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index 894b8488f8..c667f01200 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -167,15 +167,19 @@ pub(crate) fn weight_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, weig /// "Stroke"): a click toggles the Stroke panel in a popover under it, and a drag scrubs the weight /// field after it ([`crate::scrub`]). pub(crate) fn link(app: &mut VectorcraftApp, ui: &mut Ui, label: &str) { - let t = Tokens::get(ui.ctx()); // A drag on it scrubs the weight field after it (#4088). - let resp = crate::scrub::link_label(ui, egui::RichText::new(label).size(12.0).color(t.text).underline()).on_hover_text(tl!("Stroke options")); + let resp = crate::scrub::link_label(ui, link_text(ui, label)).on_hover_text(tl!("Stroke options")); widgets::popover(&resp, resp.clicked(), |ui| { ui.set_width(260.0); show(app, ui); }); } +/// The text of the Stroke [`link`] labeled `label`, as it draws it and the Control bar measures it. +pub(crate) fn link_text(ui: &Ui, label: &str) -> egui::RichText { + egui::RichText::new(label).size(12.0).color(Tokens::get(ui.ctx()).text).underline() +} + pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let st = current_stroke(app); @@ -491,12 +495,15 @@ pub fn paint_profile(ui: &Ui, r: Rect, points: Option<&[(f64, f64, f64)]>, color ui.painter().with_clip_rect(r).add(egui::Shape::mesh(mesh)); } +/// The width of the [`profile_dropdown`] button. +pub(crate) const PROFILE_DROPDOWN_WIDTH: f32 = 100.0; + /// The Profile dropdown (Stroke panel, Control bar): the stroke's own silhouette on the button /// (a custom profile too), the built-in profiles and then the saved ones in the list. Returns the /// id or name picked. pub(crate) fn profile_dropdown(app: &VectorcraftApp, ui: &mut Ui, cur: Option<&WidthProfile>) -> Option { let t = Tokens::get(ui.ctx()); - let (r, resp) = ui.allocate_exact_size(vec2(100.0, 24.0), Sense::click()); + let (r, resp) = ui.allocate_exact_size(vec2(PROFILE_DROPDOWN_WIDTH, 24.0), Sense::click()); ui.painter().rect_filled(r, 2, t.input); ui.painter().rect_stroke(r, 2, Stroke::new(1.0, if resp.hovered() { t.text } else { t.input_border }), StrokeKind::Inside); let body = Rect::from_min_max(r.min + vec2(6.0, 5.0), pos2(r.right() - 20.0, r.bottom() - 5.0)); diff --git a/crates/ui-egui/src/tests/document_drag.rs b/crates/ui-egui/src/tests/document_drag.rs index f01314a667..cefe441020 100644 --- a/crates/ui-egui/src/tests/document_drag.rs +++ b/crates/ui-egui/src/tests/document_drag.rs @@ -291,6 +291,54 @@ fn hovering_back_to_source_resumes_move_instead_of_copying() { assert!((bounds.x0 - 150.0).abs() < 0.001 && (bounds.y0 - 150.0).abs() < 0.001, "{bounds:?}"); } +/// Back in the source tab, the move resumes even when coming to the front measured the source's +/// type again in its own composite fonts, which replaces the document without editing it. +#[test] +fn hovering_back_to_a_source_whose_type_is_measured_again_resumes_move() { + let font = |roman: u32| { + json!({ + "name": "CF Tab Drag", + "parts": {"kanji": {"font": "Source Sans 3"}, "roman": {"font": "JetBrains Mono", "size": roman}}, + }) + }; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width":400,"height":300,"name":"Source"})).unwrap(); + app.run("type.compositeFonts.set", json!({"fonts": [font(100)]})).unwrap(); + app.run("text.create", json!({"x": 200, "y": 250, "text": "Ab12", "size": 20, "font": "CF Tab Drag"})).unwrap(); + let id = vectorcraft_doc::NodeId(app.run("shape.rectangle", json!({"x":50,"y":50,"width":100,"height":100})).unwrap()["id"].as_u64().unwrap()); + app.run("file.new", json!({"width":400,"height":300,"name":"Target"})).unwrap(); + app.run("type.compositeFonts.set", json!({"fonts": [font(50)]})).unwrap(); + app.session.set_active(0); + let undo = app.session.active().unwrap().history.undo.len(); + app.select_tool("selection"); + app.ui.task_bar = false; + app.ui.view.rulers = false; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + frame(&mut app, &ctx, 0.0, vec![]); + let tabs = frame(&mut app, &ctx, 0.01, vec![]); + let grab = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(75.0, 80.0)); + frame(&mut app, &ctx, 0.05, vec![Event::PointerMoved(grab), button(grab, true)]); + frame(&mut app, &ctx, 0.1, vec![Event::PointerMoved(grab + egui::vec2(20.0, 10.0))]); + frame(&mut app, &ctx, 0.2, vec![Event::PointerMoved(tabs[1].center())]); + frame(&mut app, &ctx, 0.25, vec![]); + frame(&mut app, &ctx, 0.8, vec![]); + assert_eq!(app.session.active_index(), Some(1)); + let left = app.session.documents()[0].doc.clone(); + frame(&mut app, &ctx, 0.9, vec![Event::PointerMoved(tabs[0].center())]); + frame(&mut app, &ctx, 1.4, vec![]); + assert_eq!(app.session.active_index(), Some(0)); + assert!(!std::sync::Arc::ptr_eq(&app.session.documents()[0].doc, &left), "the source's type is measured again"); + let drop = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(175.0, 180.0)); + frame(&mut app, &ctx, 1.5, vec![Event::PointerMoved(drop)]); + frame(&mut app, &ctx, 1.6, vec![button(drop, false)]); + let st = app.session.active().unwrap(); + assert_eq!(st.selection.objects, vec![id]); + assert_eq!(st.history.undo.len(), undo + 1); + let bounds = st.doc.node(id).unwrap().geometric_bounds().unwrap(); + assert!((bounds.x0 - 150.0).abs() < 0.001 && (bounds.y0 - 150.0).abs() < 0.001, "{bounds:?}"); +} + /// Verify Alt-drag transfers restored originals across tabs without retaining preview duplicates. #[test] fn alt_drag_can_continue_across_multiple_tabs_without_copying_the_source_preview() { @@ -380,6 +428,58 @@ fn source_edits_target_closure_switches_and_focus_loss_cancel_stale_transfers() } } +/// Artwork held over the tab of a document with other assigned color profiles switches to it and +/// drops there: bringing those profiles to the front redraws the source without editing it. +#[test] +fn a_drop_on_a_document_with_other_assigned_profiles_copies_the_artwork() { + const CHILD: &str = "VECTORCRAFT_TEST_DRAG_ASSIGNED_PROFILES"; + if std::env::var_os(CHILD).is_none() { + // Color settings are process-wide: change them in a separate test process. + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "tests::document_drag::a_drop_on_a_document_with_other_assigned_profiles_copies_the_artwork", "--nocapture"]) + .env(CHILD, "1") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"), + "isolated drag test failed:\n{stdout}\n{}", + String::from_utf8_lossy(&output.stderr), + ); + return; + } + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width":400,"height":300,"name":"Source"})).unwrap(); + app.run("shape.rectangle", json!({"x":50,"y":50,"width":100,"height":100})).unwrap(); + app.run("file.new", json!({"width":400,"height":300,"name":"Target"})).unwrap(); + app.run("edit.assignProfile", json!({"cmyk": vectorcraft_color::cms::DEVICE_CMYK})).unwrap(); + app.session.set_active(0); + let source = app.session.active().unwrap().doc.clone(); + app.select_tool("selection"); + app.ui.task_bar = false; + app.ui.view.rulers = false; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + frame(&mut app, &ctx, 0.0, vec![]); + let tabs = frame(&mut app, &ctx, 0.01, vec![]); + let grab = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(75.0, 80.0)); + frame(&mut app, &ctx, 0.05, vec![Event::PointerMoved(grab), button(grab, true)]); + frame(&mut app, &ctx, 0.1, vec![Event::PointerMoved(grab + egui::vec2(20.0, 10.0))]); + assert!(app.session.in_interaction()); + frame(&mut app, &ctx, 0.2, vec![Event::PointerMoved(tabs[1].center())]); + frame(&mut app, &ctx, 0.25, vec![]); + frame(&mut app, &ctx, 0.8, vec![]); + assert_eq!(app.session.active_index(), Some(1)); + let undo = app.session.active().unwrap().history.undo.len(); + let drop = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(175.0, 180.0)); + frame(&mut app, &ctx, 0.9, vec![Event::PointerMoved(drop)]); + frame(&mut app, &ctx, 1.0, vec![button(drop, false)]); + let placed = app.session.active().unwrap().selection.objects.len(); + let steps = app.session.active().unwrap().history.undo.len(); + assert_eq!((placed, steps), (1, undo + 1), "the drop places the artwork"); + assert_eq!(app.session.documents()[0].doc, source); +} + /// Verify tab-strip relayout under a stationary pointer cannot trigger another document switch. #[test] fn scrolling_tabs_under_a_stationary_drag_does_not_cascade_to_other_documents() { diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index ff249bbed4..d4a51799ca 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -522,7 +522,9 @@ fn the_wheel_steps_the_families_in_properties_and_the_control_bar() { type Draw = fn(&mut VectorcraftApp, &mut egui::Ui); let places: [(&str, Draw); 2] = [ ("Properties", |app, ui| crate::panels::properties::type_sections(app, ui)), - ("Control bar", |app, ui| crate::panels::character::control_bar(app, ui)), + ("Control bar", |app, ui| { + crate::panels::character::control_bar(app, ui, 0.0); + }), ]; let at = |f: &str| FontDb::global().menu_family_list().iter().position(|x| x == f); for (place, draw) in places { diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index 9768813a70..30757d3f52 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -536,6 +536,103 @@ fn the_task_bar_follows_the_active_document() { assert!(!locked(&app, rect), "the button doesn't lock the shape"); } +/// A blank canvas, a new document with a selection tool in use, offers Draw (Pencil) and Place +/// Image centered under the active artboard (under the canvas's center when the artboard is out of +/// view), with the handle and More Options but none of a selection's actions or its Fill, Stroke, +/// Recolor Artwork and Lock. Other tools, artwork in the document and a selection each leave the +/// blank canvas actions out; ruler guides do not count as artwork. +#[test] +fn a_blank_canvas_offers_draw_and_place_image() { + let mut app = app(); + let board = app.session.active().unwrap().doc.artboards[0].rect; + // The artboard at 50% in the middle of the window, after the first frame fits it in. + bar(&mut app); + let view = app.view_mut().unwrap(); + (view.center, view.zoom) = (board.center(), 0.5); + for tool in ["selection", "directSelection", "groupSelection"] { + app.run("tool.select", json!({ "tool": tool })).unwrap(); + let (ctx, texts) = bar_texts(&mut app); + assert!(at(&texts, "Draw (Pencil)").x < at(&texts, "Place Image").x, "{tool}: {texts:?}"); + let shown = canvas::task_bar_rect(&ctx).unwrap(); + let xf = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let bottom = xf.quad(board).iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max); + let middle = xf.to_screen(board.center()).x; + assert!(shown.top() > bottom && (shown.center().x - middle).abs() < 2.0, "{tool}: {shown:?} centered under {board:?}"); + assert!(app.canvas_rect.unwrap().contains_rect(shown), "{tool}: {shown:?} on the canvas"); + } + // The artboard out of view: under the canvas's center. + app.view_mut().unwrap().center = board.center() + vectorcraft_geom::Vec2::new(5000.0, 5000.0); + let (ctx, _) = bar(&mut app); + let (shown, middle) = (canvas::task_bar_rect(&ctx).unwrap(), app.canvas_rect.unwrap().center()); + assert!((shown.center().x - middle.x).abs() < 2.0 && shown.top() > middle.y && shown.top() < middle.y + 60.0, "{shown:?} under {middle:?}"); + app.view_mut().unwrap().center = board.center(); + + let tree = crate::widgets::accessibility_tests::tree(|ui| canvas::show(&mut app, ui)); + for label in ["Draw (Pencil)", "Place Image", "More Options"] { + assert_eq!(crate::widgets::accessibility_tests::named(&tree, label).role(), egui::accesskit::Role::Button, "{label}"); + } + let named = |label: &str| tree.iter().any(|n| n.label().is_some_and(|l| l.starts_with(label))); + for label in ["Fill", "Stroke", "Recolor Artwork", "Lock", "Duplicate"] { + assert!(!named(label), "{label} is left out"); + } + + for tool in ["pen", "rectangle", "type", "artboard"] { + app.run("tool.select", json!({ "tool": tool })).unwrap(); + let (_, texts) = bar_texts(&mut app); + assert!(!has(&texts, "Draw (Pencil)") && !has(&texts, "Place Image"), "{tool}: {texts:?}"); + } + + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + let rect = id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 80, "height": 60})).unwrap()); + select(&mut app, &[rect]); + let (_, texts) = bar_texts(&mut app); + assert!(has(&texts, "Duplicate") && !has(&texts, "Draw (Pencil)"), "{texts:?}"); + select(&mut app, &[]); + let (ctx, _) = bar(&mut app); + assert!(canvas::task_bar_rect(&ctx).is_none(), "artwork with nothing selected: no bar"); + select(&mut app, &[rect]); + app.run("edit.clear", json!({})).unwrap(); + app.run("guide.add", json!({"pos": 50})).unwrap(); + assert!(has(&bar_texts(&mut app).1, "Draw (Pencil)"), "a ruler guide is not artwork"); +} + +/// Draw (Pencil) switches to the Pencil tool. Place Image picks files for the Place dialog; +/// canceling the pick leaves the document as it was. +#[test] +fn the_blank_canvas_bar_draws_with_the_pencil_and_places_an_image() { + let mut app = app(); + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + click(&mut app, "Draw (Pencil)"); + assert_eq!(app.session.tool_id(), "pencil"); + + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + app.services.pick_open = Some(Box::new(|_: &crate::FilePick| None)); + click(&mut app, "Place Image"); + assert!(app.ui.dialog.is_none(), "canceled: no Place dialog"); + assert!(app.session.active().unwrap().doc.layers.iter().all(|l| l.children().is_none_or(|c| c.is_empty())), "nothing placed"); + + let path = std::env::temp_dir().join("vectorcraft-task-bar-place.png").to_string_lossy().into_owned(); + let picked = path.clone(); + app.services.pick_open = Some(Box::new(move |_: &crate::FilePick| Some(picked.clone()))); + click(&mut app, "Place Image"); + let d = app.ui.dialog.take().expect("the Place dialog"); + assert_eq!((d.kind.as_str(), &d.fields["files"]), (crate::dialogs::place::KIND, &json!([{ "path": path }]))); +} + +/// On a canvas too narrow for one row, the blank canvas bar's actions wrap onto a second row and the +/// bar stays whole on the canvas. +#[test] +fn the_blank_canvas_bar_wraps_on_a_narrow_canvas() { + let mut app = app(); + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let (texts, r, canvas) = bar_texts_on_screen(&mut app, &ctx, vec2(240.0, 400.0), 3); + assert!(canvas.contains_rect(r), "{r:?} on {canvas:?}"); + let (draw, place) = (at(&texts, "Draw (Pencil)"), at(&texts, "Place Image")); + assert!(place.y > draw.y + 20.0, "Place Image wraps under Draw (Pencil): {draw:?} {place:?}"); +} + /// The Type tools whose bar shows the font and Fill controls. const TYPE_TOOLS: [&str; 6] = ["type", "areaType", "typeOnPath", "verticalType", "verticalAreaType", "verticalTypeOnPath"]; diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 55fb3fbc67..eeb00ceb62 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -571,7 +571,10 @@ pub(crate) fn craft_font_name(f: &vectorcraft_text::CraftFont) -> String { fn add_craft_fonts(fonts: &mut FontDefinitions) { // The name of each face's copy in each family. let mut copies: std::collections::HashMap<(FontFamily, String), String> = std::collections::HashMap::new(); - for f in vectorcraft_text::CRAFT_FONTS.iter().filter(|f| f.is_japanese() || f.is_georgian() || f.scripts.contains(&"Deva")) { + for f in vectorcraft_text::CRAFT_FONTS + .iter() + .filter(|f| f.is_japanese() || f.is_georgian() || f.scripts.contains(&"Deva") || f.scripts.contains(&"Gujr")) + { let name = craft_font_name(f); for (copy, data, families) in crate::ui_fonts::baseline_copies(fonts, &name, &FontData::from_static(f.bytes)) { for family in families { @@ -589,6 +592,7 @@ fn add_craft_fonts(fonts: &mut FontDefinitions) { stack.extend(vectorcraft_text::craft_fonts::japanese_ui_fonts(bold).into_iter().map(copy)); stack.extend(vectorcraft_text::craft_fonts::georgian_ui_fonts().into_iter().map(copy)); stack.extend(vectorcraft_text::CRAFT_FONTS.iter().filter(|f| f.scripts.contains(&"Deva")).map(copy)); + stack.extend(vectorcraft_text::CRAFT_FONTS.iter().filter(|f| f.scripts.contains(&"Gujr")).map(copy)); } } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index fe1dbab127..7684f58942 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -116,15 +116,16 @@ private editing data. VectorCraft: bytes and past 4,096 styles are left out with a warning. A panel with no style written opens with a new document's styles, with a warning. No art style is written on a large canvas. The Brushes panel's Calligraphic brushes, and its Scatter and Art brushes with their art, are written in its order: Scatter and Art brushes in Tints or keeping their - art's colors, Art brushes that stretch their art along the path from left to right at 100% unflipped without - overlap, and art of paths and compound paths with points and groups with members, with one solid fill and one - plain stroke each. Other brushes, + art's colors, Art brushes that run their art from left to right without overlap at a fixed width, with their + width, flips and scale (Stretch Between Guides with guides that leave art between them), and art of paths and + compound paths with points and groups with members, with one solid fill and one plain stroke each. Other brushes, Pattern and Bristle brushes among them, and brushes past the number or the art VectorCraft's reader reads are left out with a warning; no brush is written on a large canvas. A path or compound path with a Calligraphic brush, or - with an Art brush in Tints whose art bends along the path, keeps the brush on its stroke; other brushed strokes are + with an Art brush whose art bends along the path and that doesn't scale its art proportionately, keeps the brush + on its stroke; other brushed strokes are written as their brush's art. A document that uses a new document's brushes writes none until a path keeps one on - its stroke; then the brushes are written, and the file opens without the new document's Pattern and Bristle brushes - and its Art brush with other options. + its stroke; then the brushes are written, and the file opens without the new document's Pattern and Bristle + brushes. Point type, area type (with its rows, columns, gutters and first baseline, threaded frames included) and type on a path are written as text: a text object for each, naming its story in the text document, with the characters, fonts, sizes, leading, tracking, baseline shift, @@ -264,7 +265,7 @@ private editing data. VectorCraft: ### Illustrator's private editing data -The app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping (several clipping paths in a clipping group, a layer or an opacity mask's art clip to their intersection, and a compound path of clipping paths stays one clip; a filled clipping path keeps its fill, which paints under the art it clips, except the path of a mask that covers several layers, whose fill is read as a path of its own in the layer that holds the mask's path, and a compound clipping path whose subpaths paint differently, whose filled subpaths are read as a compound path of their own under the art it clips; clipping paths that would nest the art past the document's 32 levels stop the editing data from being read: #5789), object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint; a stroke of width 0, `0 w`, on a path, a gradient-filled path, legacy type or in an art style comes in as a 0.25 pt hairline, as from PDF: #1671), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), gradient and pattern names in the file up to their first newline (a name that another swatch, or a gradient or pattern defined earlier, already has is numbered "Sky 2"; a name without a newline is kept, and when another swatch has it, the pattern's swatch, or a named color sharing a gradient or pattern swatch's name, is numbered the same way), opacity, blend modes, isolation and knockout, embedded images where their matrix puts them, rotated ones too (#2269), with their alpha channel (CMYK images keep their ink amounts, as they do when a `.ai` opens from its PDF part, and an alpha channel that hides part of a CMYK image becomes a grayscale opacity mask), placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), with the folder the file keeps relative to the document's, where the link is looked for when its path is gone, placed PDF and `.ai` pages as placed documents of the [page and Crop to box](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-other-file-types/adobe-pdf-placement-options.html) their dictionary names (`AI11 PlacedPDFPageIndex`, `AI11 PlacedPDFCropTo`; a value VectorCraft doesn't read, or a binding to an image variable, leaves a linked image), guides, and every artboard where it is, with its center mark, cross hairs, video safe areas and video ruler pixel aspect ratio. VectorCraft reads `CropAreaActive`, an `/Int` in the dictionary that holds the artboard array, as the index of the active artboard in that array, and the file opens with that artboard active; without it, or when it names no artboard that opens, the first artboard is active. It also reads the Document Raster Effects Settings (resolution, color model, background, anti-alias, clipping mask, Add Around Object and Preserve Spot Colors), the author from the header's `%%For` (an EPS's own header when the editing data's has none), swatches for the header's `%%CMYKCustomColor` and `%%RGBCustomColor` colors that the document has none of, and `%%TemplateBox` in older files; a setting with a value of another type or out of range keeps its default. Objects on Path groups open live, with their path, their objects in order along it, Pivot and Rotate; one with a dragged spacing or other options opens as a plain group. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile on a Stroke Offset stroke or written as each side's widths) with Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Offset Path, Outline Object, Outline Stroke, Convert to Shape, Tweak (only when it moves nothing), Pucker & Bloat at 0 or Bas Relief (read from its settings block: [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646)) comes in as itself with them and their saved settings. A path, compound path or group (not a clipping group) whose only effect is Transform (not with copies or Random) or a Free Distort whose corners make a parallelogram comes in as itself with it too, when neither it nor anything in it paints with a gradient or pattern and everything in it is a path, compound path or group with no opacity mask and no effects of its own; Transform that scales, rotates or reflects needs nothing in it to be stroked. Twist as the only effect is read the same way when nothing in the object is stroked: up to 180° either way (a limit VectorCraft sets) on a path or compound path whose segments are all straight, except a compound path with a guide member that reaches past its other members at an angle other than 0°, and up to 90° either way (a limit VectorCraft sets) on another path, compound path or group with no guide in it. One with more, other effects (Zig Zag, a Pucker & Bloat of another amount, a Free Distort with other corners), an effect setting that is not read (an unknown key, a missing or out-of-range value, a Scribble with a color of its own), a variable-width stroke with one centered width array, dashes aligned to corners, a brush other than a Calligraphic or Art one, a Calligraphic brush with settings of its own or an angle that follows the path, or an Art brush other than the ones described next comes in as its drawn look (a group named after it). Objects in a symbol defined before the file's art styles section, as newer files write it, take the same styles as the layers' objects. A path with a Calligraphic brush otherwise comes in as the path, the brush on its stroke at the stroke's weight, which scales the brush (#372), and so does a path with an Art brush, whose weight scales the art across the path, when all of these hold: the brush stretches its art along the path, unflipped, at 100% in Tints; the path's settings are the brush's own, with no entry that isn't read, no variable width and no effects; its stroke isn't a swatch; and the art is paths, compound paths and groups in solid fills, with no strokes, masks or effects, and has some length along the path. A stroke takes the brush the Brushes panel keeps under its name, and one whose name the panel doesn't keep comes in as its drawn look. Type with an Art brush keeps its drawn look under the type. A Calligraphic brush's Pressure, Tilt, Bearing and Rotation come in as those channels (a 6D Art Pen's brushes vary with the pressure, the bearing and the barrel's rotation); its Stylus Wheel varies at random. The file's Calligraphic, Scatter, Art and Bristle brushes replace the default ones in the Brushes panel, in the file's order, also when the file comes in as its PDF part (a brush library has no layers); pattern brushes and brushes it can't read are left out with a note. When the file opens from its layers, the spot colors in the art of each brush that comes in become swatches, and that art's colors count toward the document's color mode, as a pattern's do. The swatches take at most 20,000 color names in all across the brush art, and each brush-art section keeps at most 20,000 names. Each name keeps its first definition, with the color operators' names before the gradient stops' names. A spot color past those limits makes no swatch, and its color still counts toward the color mode. A spot color that the page's art, a pattern or a symbol also defines keeps the value defined there. Type keeps its art style as a live appearance whatever its stack (an Art brush aside), so it can be retyped: each fill and stroke of the style's stack (solid colours, gradients and pattern fills, placed as the drawn look places them) with its own opacity, blend mode and effects, the effects on the whole object, and where the characters paint among them; the effects read are Warp, Offset Path, Outline Object, Outline Stroke, Transform (with copies, Transform Objects, Transform Patterns and Scale Strokes & Effects; not with Random), Free Distort, Pucker & Bloat, Twist, Zig Zag, Tweak (only when it moves nothing, the file holding none of its random outline), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur, Bas Relief (its detail, smoothness and light direction from its settings block, or the effect's defaults when the style holds no settings block; a settings block that isn't read keeps the drawn look) and Convert to Shape, and a style with others, or with an effect setting that isn't read, keeps the type's drawn look. Convert to Shape keeps its Rectangle, Rounded Rectangle or Ellipse, its Absolute or Relative size (a relative size in the file is added to each side) and its corner radius; a relative Ellipse, or a setting that isn't read, keeps the drawn look. The page's drawing of such a style isn't held to look like VectorCraft's where its drawn look lay. Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and its stroke, and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` or an Illustrator EPS from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type (its frame's matrix, which newer files leave out when it is neither turned nor scaled, read as none), area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing (a character attribute a run doesn't set comes from its character style, then the style that one is based on, then the document's defaults); area type keeps its frame's Area Type Options rows and columns, the gutter between rows and the gutter between columns, and its first baseline (Fixed, Cap Height, Leading or x Height) with its minimum; type on a path keeps its frame's Type on a Path effect, Align to Path and Flip (a frame whose options set key 6 to true keeps their defaults), and the first frame's Spacing is fitted from the file's layout, not read from the dialog's value; the distance from the path under Align to Path is measured for each character at its size and vertical scale (in vertical type, its horizontal scale), from the top of its font's em box (Ascender), half the height of the top of its font's H (Center) or the bottom of its font's bounding box (Descender); the fit under Gravity does not reproduce the file's layout, and its glyphs can lie up to 7.4 pt from the positions the file keeps; the page's drawing of it (often in pieces) is left out. Vertical type keeps its orientation, its columns from right to left, and a run set to Tate-chu-yoko keeps that setting. When it makes the type of every text object that shows, the page's type is only compared with it, each text object keeps its own group, frame and layer, and a line break inside a paragraph stays one. Where it can't make a text object's type (a frame of another kind), that text object takes the page's type where the text document puts its lines, or else what is left of the page's type in painting order (no group around it); the page's type where made type draws stays that type's, and hidden type is left out with a note. Text wrap is read too: an object whose dictionary has the wrap options (offset and Invert Wrap), or any graphic of a legacy wraparound group (`*w` … `*W`, before or after its type, with no offset), is a wrap object, and area type below it in its layer flows around it when the file opens, threaded type included (#2763). Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). A `.ai` whose PDF part draws nothing at all (an empty page, with or without those boxes) opens from its editing data alone where that reads too: there is no page art to check its layers against, and the empty page would lose all of it. Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). In a PDF saved with some of its artboards, the type on each page after the first goes to an artboard of that page's size after the previous page's artboard, and only to one that leaves an artboard for each later page; the first page's artboard also leaves one for each later page. When several artboards fit a later page, VectorCraft takes the one whose art draws most like the page, and compares at most 16 artboards for all the later pages together. Symbols are read into the Symbols panel, in the file's symbol list order, with their art, their registration point and the instances in their art, and their instances come in live with their matrix, name, hide, lock, opacity, blending mode, isolation, knockout and opacity mask; a symbol whose art isn't read whole (type, placed files, an image it leaves out or anything else it doesn't read) isn't kept. A file that comes in as its page or PDF part because its editing data has no layers (a symbol library), or because its layers can't be read or used (any of the cases below), gets the symbols its editing data defines before its first layer, in the symbol list's order, while its page art stays as the page draws it. Instances of symbols it can't read, instances with appearance changes of their own (a dynamic symbol's), pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. An unread object on a shown layer whose box can be found instead comes in as a group of the page's objects inside that box, named after it, with the rest of the layers and groups kept and a warning listing what came in as the page's art; the file still comes in as its page when live art or another unread object shares that page art, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry its transparency (#4806). The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5% (away from type, when all the type is made from the text document), where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). A `.ai` is not compared with its page, and its PDF part is not imported, when its text document makes the type of every text object (at least one shows, and none is drawn by the appearance of the layer or group it is in), no object is unread, no placed file has a second placement, no live envelope keeps its drawn art, it declares no large canvas, and its artboards are its pages' trim boxes one for one, in points or in the first page's `UserUnit`: it opens from its layers, with the bleed of its pages' bleed boxes. When they differ because some objects draw differently (an object in another color, or one that only one of them draws), each top-level object of the layers that holds such a difference comes in as a group of the page's objects inside its box and the layers stay, with a warning that names the layer or sublayer of each such object as the Layers panel shows it (`` for one without a name) and how many came in from it; an export or Save As to the file's own path then needs `acknowledgeLoss: true`, and a wrap object replaced this way keeps its wrap options. The file still comes in as its page when only edges or type differ, when a difference lies outside every top-level object, when more than half of the top-level objects or more than 64 hold one, when such an object is or holds type or holds a wrap object inside a group, when the page's type of a text object that stays lies inside such an object's box, when the page's art can't replace such an object for one of the reasons above for an unread object, when matching the differences to the objects would take too much work, or when the result still differs from the page. `editingData: "always"` keeps the objects as read, and placed files at the other point their data gives are tried as well. An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). Its pages still go where the editing data puts the artboards (a grid stays a grid), with the art on each, when it has as many artboards as pages, of the same sizes (#1068); the page of the artboard `CropAreaActive` names is then the active artboard. A `.ai` whose editing data has artboards but no layers also opens on that page under the same check, and its pages stay where they are. A `.ai` whose PDF part's pages are cropped to its art (each page the box of all the art, which is the editing data's bounding box) instead of its artboards opens from its layers, with its own artboards, where they draw as that page does. Its pages are laid out so that art one of them draws past its artboard (notes beside a design, which the part doesn't clip) doesn't lie over the page before it in the row, where it was taken for that page's art: the check against the editing data then found art its layers don't have there and opened the file as its PDF part, without the art outside the artboards. That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. +The app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping (several clipping paths in a clipping group, a layer or an opacity mask's art clip to their intersection, and a compound path of clipping paths stays one clip; a filled clipping path keeps its fill, which paints under the art it clips, except the path of a mask that covers several layers, whose fill is read as a path of its own in the layer that holds the mask's path, and a compound clipping path whose subpaths paint differently, whose filled subpaths are read as a compound path of their own under the art it clips; clipping paths that would nest the art past the document's 32 levels stop the editing data from being read: #5789), object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint; a stroke of width 0, `0 w`, on a path, a gradient-filled path, legacy type or in an art style comes in as a 0.25 pt hairline, as from PDF: #1671), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), gradient and pattern names in the file up to their first newline (a name that another swatch, or a gradient or pattern defined earlier, already has is numbered "Sky 2"; a name without a newline is kept, and when another swatch has it, the pattern's swatch, or a named color sharing a gradient or pattern swatch's name, is numbered the same way), opacity, blend modes, isolation and knockout, embedded images where their matrix puts them, rotated ones too (#2269), with their alpha channel (CMYK images keep their ink amounts, as they do when a `.ai` opens from its PDF part, and an alpha channel that hides part of a CMYK image becomes a grayscale opacity mask), placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), with the folder the file keeps relative to the document's, where the link is looked for when its path is gone, placed PDF and `.ai` pages as placed documents of the [page and Crop to box](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-other-file-types/adobe-pdf-placement-options.html) their dictionary names (`AI11 PlacedPDFPageIndex`, `AI11 PlacedPDFCropTo`; a value VectorCraft doesn't read, or a binding to an image variable, leaves a linked image), guides, and every artboard where it is, with its center mark, cross hairs, video safe areas and video ruler pixel aspect ratio. VectorCraft reads `CropAreaActive`, an `/Int` in the dictionary that holds the artboard array, as the index of the active artboard in that array, and the file opens with that artboard active; without it, or when it names no artboard that opens, the first artboard is active. It also reads the Document Raster Effects Settings (resolution, color model, background, anti-alias, clipping mask, Add Around Object and Preserve Spot Colors), the author from the header's `%%For` (an EPS's own header when the editing data's has none), swatches for the header's `%%CMYKCustomColor` and `%%RGBCustomColor` colors that the document has none of, and `%%TemplateBox` in older files; a setting with a value of another type or out of range keeps its default. Objects on Path groups open live, with their path, their objects in order along it, Pivot and Rotate; one with a dragged spacing or other options opens as a plain group. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile on a Stroke Offset stroke or written as each side's widths) with Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Offset Path, Outline Object, Outline Stroke, Convert to Shape, Tweak (only when it moves nothing), Pucker & Bloat at 0 or Bas Relief (read from its settings block: [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [layered image file format specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646)) comes in as itself with them and their saved settings. A path, compound path or group (not a clipping group) whose only effect is Transform (not with copies or Random) or a Free Distort whose corners make a parallelogram comes in as itself with it too, when neither it nor anything in it paints with a gradient or pattern and everything in it is a path, compound path or group with no opacity mask and no effects of its own; Transform that scales, rotates or reflects needs nothing in it to be stroked. Twist as the only effect is read the same way when nothing in the object is stroked: up to 180° either way (a limit VectorCraft sets) on a path or compound path whose segments are all straight, except a compound path with a guide member that reaches past its other members at an angle other than 0°, and up to 90° either way (a limit VectorCraft sets) on another path, compound path or group with no guide in it. One with more, other effects (Zig Zag, a Pucker & Bloat of another amount, a Free Distort with other corners), an effect setting that is not read (an unknown key, a missing or out-of-range value, a Scribble with a color of its own), a variable-width stroke with one centered width array, dashes aligned to corners, a brush other than a Calligraphic or Art one, a Calligraphic brush with settings of its own or an angle that follows the path, or an Art brush other than the ones described next comes in as its drawn look (a group named after it). Objects in a symbol defined before the file's art styles section, as newer files write it, take the same styles as the layers' objects. A path with a Calligraphic brush otherwise comes in as the path, the brush on its stroke at the stroke's weight, which scales the brush (#372), and so does a path with an Art brush, whose weight scales the art across the path, when all of these hold: the brush is in Tints or None, at a fixed width from 1% to 1,000%, stretched to the path or stretched between guides that leave art between them, flipped along or across or not ([Art brush options](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf), printed page 193), with `Spacing` 0, `Adjustment` 1 and `FoldAdjustment` 1 (VectorCraft's own reading of these numbers; a brush that scales its art proportionately comes into the Brushes panel with that option, and its paths keep their drawn look); the path's settings are the brush's own (apart from the lowest and highest width of a fixed width and the guides' lengths without Stretch Between Guides, which don't draw), with no entry that isn't read, no variable width and no effects; a stroke in Tints isn't a swatch; and the art is paths, compound paths and groups in solid fills, with no strokes, masks or effects, and has some length along the path. A stroke takes the brush the Brushes panel keeps under its name, and one whose name the panel doesn't keep comes in as its drawn look. Type with an Art brush keeps its drawn look under the type. A Calligraphic brush's Pressure, Tilt, Bearing and Rotation come in as those channels (a 6D Art Pen's brushes vary with the pressure, the bearing and the barrel's rotation); its Stylus Wheel varies at random. The file's Calligraphic, Scatter, Art and Bristle brushes replace the default ones in the Brushes panel, in the file's order, also when the file comes in as its PDF part (a brush library has no layers); pattern brushes and brushes it can't read are left out with a note. When the file opens from its layers, the spot colors in the art of each brush that comes in become swatches, and that art's colors count toward the document's color mode, as a pattern's do. The swatches take at most 20,000 color names in all across the brush art, and each brush-art section keeps at most 20,000 names. Each name keeps its first definition, with the color operators' names before the gradient stops' names. A spot color past those limits makes no swatch, and its color still counts toward the color mode. A spot color that the page's art, a pattern or a symbol also defines keeps the value defined there. Type keeps its art style as a live appearance whatever its stack (an Art brush aside), so it can be retyped: each fill and stroke of the style's stack (solid colours, gradients and pattern fills, placed as the drawn look places them) with its own opacity, blend mode and effects, the effects on the whole object, and where the characters paint among them; the effects read are Warp, Offset Path, Outline Object, Outline Stroke, Transform (with copies, Transform Objects, Transform Patterns and Scale Strokes & Effects; not with Random), Free Distort, Pucker & Bloat, Twist, Zig Zag, Tweak (only when it moves nothing, the file holding none of its random outline), Drop Shadow, Inner Glow, Outer Glow, Feather, Round Corners, Scribble, Gaussian Blur, Bas Relief (its detail, smoothness and light direction from its settings block, or the effect's defaults when the style holds no settings block; a settings block that isn't read keeps the drawn look) and Convert to Shape, and a style with others, or with an effect setting that isn't read, keeps the type's drawn look. Convert to Shape keeps its Rectangle, Rounded Rectangle or Ellipse, its Absolute or Relative size (a relative size in the file is added to each side) and its corner radius; a relative Ellipse, or a setting that isn't read, keeps the drawn look. The page's drawing of such a style isn't held to look like VectorCraft's where its drawn look lay. Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and its stroke, and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` or an EPS with editing data from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type (its frame's matrix, which newer files leave out when it is neither turned nor scaled, read as none), area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing (a character attribute a run doesn't set comes from its character style, then the style that one is based on, then the document's defaults); area type keeps its frame's Area Type Options rows and columns, the gutter between rows and the gutter between columns, and its first baseline (Fixed, Cap Height, Leading or x Height) with its minimum; type on a path keeps its frame's Type on a Path effect, Align to Path and Flip (a frame whose options set key 6 to true keeps their defaults), and the first frame's Spacing is fitted from the file's layout, not read from the dialog's value; the distance from the path under Align to Path is measured for each character at its size and vertical scale (in vertical type, its horizontal scale), from the top of its font's em box (Ascender), half the height of the top of its font's H (Center) or the bottom of its font's bounding box (Descender); the fit under Gravity does not reproduce the file's layout, and its glyphs can lie up to 7.4 pt from the positions the file keeps; the page's drawing of it (often in pieces) is left out. Vertical type keeps its orientation, its columns from right to left, and a run set to Tate-chu-yoko keeps that setting. When it makes the type of every text object that shows, the page's type is only compared with it, each text object keeps its own group, frame and layer, and a line break inside a paragraph stays one. Where it can't make a text object's type (a frame of another kind), that text object takes the page's type where the text document puts its lines, or else what is left of the page's type in painting order (no group around it); the page's type where made type draws stays that type's, and hidden type is left out with a note. Text wrap is read too: an object whose dictionary has the wrap options (offset and Invert Wrap), or any graphic of a legacy wraparound group (`*w` … `*W`, before or after its type, with no offset), is a wrap object, and area type below it in its layer flows around it when the file opens, threaded type included (#2763). Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). A `.ai` whose PDF part draws nothing at all (an empty page, with or without those boxes) opens from its editing data alone where that reads too: there is no page art to check its layers against, and the empty page would lose all of it. Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). In a PDF saved with some of its artboards, the type on each page after the first goes to an artboard of that page's size after the previous page's artboard, and only to one that leaves an artboard for each later page; the first page's artboard also leaves one for each later page. When several artboards fit a later page, VectorCraft takes the one whose art draws most like the page, and compares at most 16 artboards for all the later pages together. Symbols are read into the Symbols panel, in the file's symbol list order, with their art, their registration point and the instances in their art, and their instances come in live with their matrix, name, hide, lock, opacity, blending mode, isolation, knockout and opacity mask; a symbol whose art isn't read whole (type, placed files, an image it leaves out or anything else it doesn't read) isn't kept. A file that comes in as its page or PDF part because its editing data has no layers (a symbol library), or because its layers can't be read or used (any of the cases below), gets the symbols its editing data defines before its first layer, in the symbol list's order, while its page art stays as the page draws it. Instances of symbols it can't read, instances with appearance changes of their own (a dynamic symbol's), pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. An unread object on a shown layer whose box can be found instead comes in as a group of the page's objects inside that box, named after it, with the rest of the layers and groups kept and a warning listing what came in as the page's art; the file still comes in as its page when live art or another unread object shares that page art, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry its transparency (#4806). The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5% (away from type, when all the type is made from the text document), where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). A `.ai` is not compared with its page, and its PDF part is not imported, when its text document makes the type of every text object (at least one shows, and none is drawn by the appearance of the layer or group it is in), no object is unread, no placed file has a second placement, no live envelope keeps its drawn art, it declares no large canvas, and its artboards are its pages' trim boxes one for one, in points or in the first page's `UserUnit`: it opens from its layers, with the bleed of its pages' bleed boxes. When they differ because some objects draw differently (an object in another color, or one that only one of them draws), each top-level object of the layers that holds such a difference comes in as a group of the page's objects inside its box and the layers stay, with a warning that names the layer or sublayer of each such object as the Layers panel shows it (`` for one without a name) and how many came in from it; an export or Save As to the file's own path then needs `acknowledgeLoss: true`, and a wrap object replaced this way keeps its wrap options. The file still comes in as its page when only edges or type differ, when a difference lies outside every top-level object, when more than half of the top-level objects or more than 64 hold one, when such an object is or holds type or holds a wrap object inside a group, when the page's type of a text object that stays lies inside such an object's box, when the page's art can't replace such an object for one of the reasons above for an unread object, when matching the differences to the objects would take too much work, or when the result still differs from the page. `editingData: "always"` keeps the objects as read, and placed files at the other point their data gives are tried as well. An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so live objects that only the editing data holds arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (the PDF part leaves it out), so it doesn't open; the import says so in a note (#472). Its pages still go where the editing data puts the artboards (a grid stays a grid), with the art on each, when it has as many artboards as pages, of the same sizes (#1068); the page of the artboard `CropAreaActive` names is then the active artboard. A `.ai` whose editing data has artboards but no layers also opens on that page under the same check, and its pages stay where they are. A `.ai` whose PDF part's pages are cropped to its art (each page the box of all the art, which is the editing data's bounding box) instead of its artboards opens from its layers, with its own artboards, where they draw as that page does. Its pages are laid out so that art one of them draws past its artboard (notes beside a design, which the part doesn't clip) doesn't lie over the page before it in the row, where it was taken for that page's art: the check against the editing data then found art its layers don't have there and opened the file as its PDF part, without the art outside the artboards. That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard. A run of type in the editing data whose baseline direction is 1 opens with Standard Vertical Roman Alignment (https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html) on, which is VectorCraft's own mapping: its half-width characters stand upright in vertical type, and horizontal type keeps the mark with no change to its layout. A run whose baseline direction is 2 or 3 (Tate-chu-yoko), or that names none in the run, its character style or the document's defaults, opens with it off. The scripting reference lists 1, 2 and 3 as the Standard, vertical rotated and Tate-chu-yoko baseline directions (https://web.archive.org/web/20170621180804/https://wwwimages.adobe.com/content/dam/Adobe/en/devnet/illustrator/sdk/CC2015_3/Illustrator%20VBScript%20Reference.pdf, page 256). A Scatter brush in the editing data whose last 15 numbers start with 1 comes into the Brushes panel in Tints. Each brush art section is read without its frame: an unnamed, unpainted rectangle with no settings of its own at the art's bounds (within 1/1000 pt), closed, or open with its first corner again at the end. A wider frame stays in the art, and an art group with a state or look of its own (hidden, locked, isolated, knockout, an opacity, a blend mode, a name, a mask, a note or an appearance) stays a group. diff --git a/docs/gaps.md b/docs/gaps.md index 201823b983..8aea143a47 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -34,6 +34,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-14 | minor | G12: Layers panel row context menu (Options…, Duplicate, Delete, Lock Others/Show All/Hide Others/Unlock All, Locate Object, Collect for Export) with selection handling and undo tracking. Scores and estimates unchanged. | | 2026-11-13 | minor | PDF import: preserve Type 3 font text runs as named groups ``, or as live text in `TextAs::Text` mode if `/ToUnicode` exists on non-colour fonts (`TextAs::Outlines` produces grouped outlines) (#6740); scores and estimates unchanged. | | 2026-11-08 | minor | #3380, deletion only: preserve gradient placement and exact surviving geometry; consume owned subpaths with a shared original-node snapshot; consolidate status/evidence and document command/helper contracts. Compound behavior and the existing M2 status entry are retained. Endpoint highlighting remains open; scores and estimates unchanged. | | 2026-11-07 | minor | #3380 (deletion only): independent surviving path objects; transformed appearance, stacking, selection, cubic handles and Undo/Redo covered by `delete_path_pieces`; compound semantics retained. Endpoint highlighting remains open; scores and estimates unchanged. | @@ -212,10 +213,12 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi weight, where the object's brush settings are the brush's own and its angle is on the page (#372, from a user's file of 6D Art Pen strokes: all 455 of its brushed styles), and a Calligraphic brush's Tilt, Bearing and Rotation (codes 4, 5 and 6) come in as those channels instead of Random. Others keep their drawn look. - Paths with an Art brush open the same way when the brush stretches art of solid-filled paths along the path, - unflipped, at 100% in Tints, and the path has none of its own settings, variable width, effects or a swatch - stroke. Other Art brushes (None or another colorization, another width, Proportional, between guides, flipped, - art with strokes, gradients or patterns) and type with an Art brush keep the drawn look. + Paths with an Art brush open the same way when the brush bends art of solid-filled paths along the path, and + the path has none of its own settings, variable width, effects or a swatch stroke in Tints. The brush may be + in None or Tints, at a fixed width, stretched to the path or between guides, and flipped along or across. + Other Art brushes (Tints and Shades or Hue Shift, a width that follows the pen, Scale Proportionately, guides + that leave no art between them, a `Spacing`, `Adjustment` or `FoldAdjustment` other than 0, 1 and 1, art with + strokes, gradients or patterns) and type with an Art brush keep the drawn look. - **Done (2026-11-12):** Explicitly requested editing data retains a single artboard larger than a capped 14,400 pt PDF page (#6865); the default import and other size mismatches remain unchanged. Synthetic importer regression; scores unchanged. - **Missing:** faithful layer and group structure from the editing data in real files (#951: 109 layers instead @@ -499,12 +502,12 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi - **Done:** Hyphenation › Default Language is the language of new type, unless Document Setup chose one, and each run hyphenates in its own language (Character panel). English and eleven Indian languages have break points; other languages stay whole apart from the Exceptions list (#394, #4947). +- **Done (2026-11-14):** Layers panel row context menu: right-clicking a layer, group, or object row shows a context menu (Options…, Duplicate, Delete, Lock Others / Unlock All, Hide Others / Show All, Locate Object, Collect for Export) with proper selection behavior and Undo/Redo tracking. Tests in `crates/ui-egui/src/panels/layers/tests.rs`. - **Missing:** 2 Preferences options stored but not acted on (#394), each waiting on a subsystem of its own: Plug-ins & Scratch Disks › Primary and Secondary (documents are kept in memory; nothing pages to disk), and Clipboard Handling › Legacy vector clipboard with its Preserve Paths / Preserve Appearance and Overprints choice (Illustrator's own clipboard flavour, which has no public specification). Also Properties - panel per context, context menus in the panels that have none, Layers row menu commands beyond Collect for - Export, and menus on guide and blend spine rows in the Layers panel. + panel per context, context menus in the panels that have none, and menus on guide and blend spine rows in the Layers panel. - **Impact:** settings that silently do nothing erode trust. - **Estimate:** 13–20 h. diff --git a/docs/image-trace-quality.md b/docs/image-trace-quality.md index 40f6fe43b9..4765a2d779 100644 --- a/docs/image-trace-quality.md +++ b/docs/image-trace-quality.md @@ -1240,8 +1240,10 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab ## Centre lines -Measured 2026-10-17 with maximal-disc pruning and junction merging, against base `3e44478f`. -Earlier preset and cost tables retain their previous measurements. Regenerate only this class: +Measured 2026-11-13 against `1e4e3534`, with Create Strokes deciding stroke or fill per feature. +Two runs gave identical rows in every column but ms, which varies between runs. Earlier preset and +cost tables retain their previous measurements. +Regenerate only this class: ```text TRACE_QUALITY_CLASS=centre-lines TRACE_QUALITY_REPORT=centre-lines.md cargo test --release -p vectorcraft-trace --test quality quality_report -- --ignored --nocapture @@ -1252,9 +1254,13 @@ branches shorter than `r + max(2, 0.5 × min(2r, Stroke))` are pruned, shortest self-loops inside the junction disc are removed; overlapping junctions and geometrically split crossings merge, and runs join through degree-two nodes. Simplification is bounded to 32 rounds; nodes and branches are each capped at 200,000. An over-budget blob stays with the fill path. -Whole-blob selection, ink refinement, cap detection and curve fitting retain their current rules. +A blob wider than Stroke keeps its wide part as a fill, and its thin features are traced as +strokes. The mixed-width body scene measures that split on its own: one disc of radius 24 px with +three arms of 2, 3 and 5 px that start inside it, Stroke 6 px, and no other ink. Its found % covers +the arms outside the disc, its fill→fill % covers the disc, and its paths column counts both kinds. +Ink refinement, cap detection and curve fitting retain their current rules. -Code-drawn scenes only: 23 scenes, each anti-aliased (8×8 coverage samples) and hard (that coverage thresholded at 0.5). Coordinates, widths and scene Stroke values are fixed; junction/letter/crossing coordinates include the 0.27/0.41 px offset. Create Fills and Strokes are on, Stroke is the scene's limit (13 px, mix 6 px, varying 12 px), Black and White threshold 128, Noise 4, Ignore White on, other settings at their defaults. Today's tracer has no minimum-length setting (the dashed scenes specify 0 for future use). +Code-drawn scenes only: 24 scenes, each anti-aliased (8×8 coverage samples) and hard (that coverage thresholded at 0.5). Coordinates, widths and scene Stroke values are fixed; junction/letter/crossing/mixed-width body coordinates include the 0.27/0.41 px offset. Create Fills and Strokes are on, Stroke is the scene's limit (13 px, mix and mixed-width body 6 px, varying 12 px), Black and White threshold 128, Noise 4, Ignore White on, other settings at their defaults. Today's tracer has no minimum-length setting (the dashed scenes specify 0 for future use). Distances use dense exact centre samples (≤0.25 px), emitted curves sampled at ≤0.5 px control-polygon steps, and nearest-point searches capped at 30 px. Found is the fraction of true samples within 1.5 px; width errors use output samples within 2 px of a true stroke. Samples inside exact fills are excluded. Topology is recovered from emitted subpath endpoints and intersections, merging coincident positions within 0.05 px; an endpoint contributes one arm, an interior two. Junction matching radius is local true width + 3 px; ends use half-width + 4 px. Each match is used once. Free ends exclude true junctions, corners, other strokes and fills; unmatched emitted ends are spurs. Junctions/ends within 2 px of fills are excluded. End error is distance, with signed error along the outward tangent in parentheses. Coverage is a union of painted output at pixel centres (0.02 px flattening tolerance, actual emitted caps), compared with the exact swept shape at pixel centres, for both input variants. Area error is signed relative to exact area. Anchors counts stroke anchors, including all compound subpaths; paths counts stroked + filled paths. Time covers tracing only, one scene at a time. Profile-dependent columns are unavailable (–). @@ -1304,8 +1310,10 @@ Distances use dense exact centre samples (≤0.25 px), emitted curves sampled at | letters w3 | hard | 7+0 | 0.33 / 0.92 | 0.34 / 0.71 | 99.0 | 0.06 / 0.20 | – | – | | letters w5 | aa | 7+0 | 0.27 / 1.49 | 0.29 / 1.31 | 95.9 | 0.02 / 0.20 | – | – | | letters w5 | hard | 7+0 | 0.40 / 1.48 | 0.41 / 1.29 | 96.2 | 0.05 / 0.20 | – | – | -| mix | aa | 3+1 | 0.10 / 0.21 | 12.89 / 30.00 | 57.3 | 0.03 / 0.14 | – | – | -| mix | hard | 3+1 | 0.22 / 0.43 | 12.95 / 30.00 | 57.3 | 0.08 / 0.15 | – | – | +| mix | aa | 6+3 | 0.09 / 0.19 | 0.15 / 0.28 | 99.9 | 0.02 / 0.14 | – | – | +| mix | hard | 6+3 | 0.24 / 0.52 | 0.28 / 0.56 | 99.8 | 0.07 / 0.20 | – | – | +| mixed-width body | aa | 3+1 | 0.09 / 0.27 | 0.15 / 0.31 | 100.0 | 0.05 / 0.14 | – | – | +| mixed-width body | hard | 3+1 | 0.18 / 0.42 | 0.22 / 0.42 | 100.0 | 0.05 / 0.12 | – | – | | varying | aa | 5+0 | 0.13 / 0.28 | 0.19 / 0.31 | 99.5 | 1.89 / 4.13 | – | – | | varying | hard | 5+0 | 0.20 / 0.51 | 0.24 / 0.52 | 99.7 | 1.90 / 4.22 | – | – | @@ -1313,56 +1321,58 @@ Distances use dense exact centre samples (≤0.25 px), emitted curves sampled at | scene | input | junctions ok / wrong deg / missed / extra | junction pos err | ends found | end err (signed) | spurs | IoU uniform / profile | area err uniform / profile | fill→fill % | anchors | width pts | ms | |---|---|---|---|---|---|---|---|---|---|---|---|---| -| lines w1 | aa | 0 / 0 / 0 / 76 | – | 43/48 | 0.71 (-0.69) | 88 | 0.790 / – | -6.2% / – | – | 186 | – | 4.6 | -| lines w1 | hard | 0 / 0 / 0 / 0 | – | 42/48 | 1.50 (-1.42) | 142 | 0.644 / – | -25.8% / – | – | 184 | – | 3.6 | -| lines w2 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.32 (-0.20) | 0 | 0.914 / – | -0.3% / – | – | 71 | – | 4.3 | -| lines w2 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.38 (-0.10) | 0 | 0.808 / – | -2.0% / – | – | 58 | – | 4.2 | -| lines w3 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.26 (-0.17) | 0 | 0.931 / – | -3.0% / – | – | 64 | – | 4.4 | -| lines w3 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.46 (-0.24) | 0 | 0.881 / – | -2.5% / – | – | 50 | – | 4.3 | -| lines w4 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.25 (-0.18) | 0 | 0.964 / – | -0.6% / – | – | 70 | – | 4.6 | -| lines w4 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.40 (-0.15) | 0 | 0.906 / – | -0.1% / – | – | 54 | – | 4.5 | -| lines w6 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.30 (-0.18) | 0 | 0.965 / – | -0.3% / – | – | 65 | – | 5.1 | -| lines w6 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.45 (-0.12) | 0 | 0.923 / – | 1.4% / – | – | 55 | – | 4.9 | -| lines w8 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.37 (-0.20) | 0 | 0.967 / – | -1.0% / – | – | 66 | – | 5.4 | -| lines w8 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.46 (-0.18) | 0 | 0.934 / – | -0.3% / – | – | 53 | – | 5.4 | -| lines w10 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.35 (-0.16) | 0 | 0.969 / – | -0.7% / – | – | 66 | – | 5.9 | -| lines w10 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.50 (-0.12) | 0 | 0.942 / – | -0.1% / – | – | 52 | – | 5.9 | -| lines w12 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.40 (-0.16) | 0 | 0.969 / – | -0.6% / – | – | 70 | – | 6.4 | -| lines w12 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.54 (-0.11) | 0 | 0.949 / – | -0.2% / – | – | 53 | – | 6.3 | -| curves w2 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.18 (-0.13) | 0 | 0.921 / – | -0.6% / – | – | 34 | – | 4.7 | -| curves w2 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.22 (-0.05) | 0 | 0.821 / – | -2.4% / – | – | 29 | – | 4.7 | -| curves w4 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.56 (-0.42) | 0 | 0.949 / – | -0.8% / – | – | 35 | – | 5.1 | -| curves w4 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.66 (-0.38) | 0 | 0.904 / – | -0.5% / – | – | 30 | – | 5.2 | -| curves w8 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 2.24 (-2.01) | 0 | 0.956 / – | -0.9% / – | – | 42 | – | 5.9 | -| curves w8 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 1.97 (-1.67) | 0 | 0.936 / – | -0.5% / – | – | 31 | – | 5.9 | -| circles | aa | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 32 | 0.964 / – | -0.9% / – | – | 71 | – | 7.4 | -| circles | hard | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 28 | 0.888 / – | -0.7% / – | – | 52 | – | 8.0 | -| junctions w2 | aa | 7 / 0 / 0 / 0 | 1.17 | 23/23 | 0.32 (-0.16) | 0 | 0.889 / – | 6.1% / – | – | 84 | – | 3.7 | -| junctions w2 | hard | 7 / 0 / 0 / 0 | 1.17 | 23/23 | 0.49 (-0.15) | 0 | 0.771 / – | 4.9% / – | – | 59 | – | 3.6 | -| junctions w4 | aa | 7 / 0 / 0 / 1 | 1.62 | 23/23 | 0.33 (-0.26) | 0 | 0.919 / – | 5.0% / – | – | 100 | – | 4.1 | -| junctions w4 | hard | 7 / 0 / 0 / 1 | 1.62 | 23/23 | 0.65 (-0.32) | 0 | 0.850 / – | 7.0% / – | – | 81 | – | 4.0 | -| junctions w8 | aa | 7 / 0 / 0 / 2 | 2.76 | 23/23 | 0.43 (-0.27) | 0 | 0.937 / – | 2.7% / – | – | 93 | – | 4.7 | -| junctions w8 | hard | 7 / 0 / 0 / 1 | 2.76 | 23/23 | 0.80 (-0.35) | 0 | 0.913 / – | 2.5% / – | – | 77 | – | 4.7 | -| crossings w3 | aa | 5 / 0 / 0 / 2 | 0.25 | 20/20 | 0.27 (-0.18) | 0 | 0.935 / – | -1.1% / – | – | 74 | – | 2.8 | -| crossings w3 | hard | 5 / 0 / 0 / 5 | 0.25 | 20/20 | 0.41 (-0.17) | 0 | 0.791 / – | -7.6% / – | – | 69 | – | 2.8 | -| crossings w6 | aa | 5 / 0 / 0 / 1 | 0.25 | 20/20 | 0.35 (-0.17) | 0 | 0.925 / – | -3.6% / – | – | 112 | – | 3.3 | -| crossings w6 | hard | 5 / 0 / 0 / 4 | 0.25 | 20/20 | 0.51 (-0.20) | 0 | 0.872 / – | -2.0% / – | – | 81 | – | 3.4 | -| dashed w2 | aa | 0 / 0 / 0 / 0 | – | 62/62 | 0.28 (-0.20) | 0 | 0.927 / – | -2.6% / – | – | 64 | – | 2.0 | -| dashed w2 | hard | 0 / 0 / 0 / 0 | – | 62/62 | 0.33 (-0.11) | 0 | 0.926 / – | -3.6% / – | – | 67 | – | 1.9 | -| dashed w4 | aa | 0 / 0 / 0 / 0 | – | 58/58 | 0.30 (-0.20) | 0 | 0.952 / – | -0.1% / – | – | 61 | – | 2.1 | -| dashed w4 | hard | 0 / 0 / 0 / 0 | – | 58/58 | 0.37 (-0.08) | 0 | 0.944 / – | -1.5% / – | – | 60 | – | 2.1 | -| letters w3 | aa | 7 / 0 / 0 / 6 | 1.47 | 17/17 | 0.62 (-0.47) | 1 | 0.923 / – | -0.9% / – | – | 114 | – | 3.3 | -| letters w3 | hard | 7 / 0 / 0 / 3 | 1.47 | 17/17 | 0.38 (-0.21) | 1 | 0.859 / – | -0.6% / – | – | 81 | – | 3.3 | -| letters w5 | aa | 6 / 1 / 0 / 6 | 2.10 | 17/17 | 0.99 (-0.83) | 0 | 0.929 / – | -1.7% / – | – | 124 | – | 3.7 | -| letters w5 | hard | 6 / 1 / 0 / 8 | 2.10 | 17/17 | 1.05 (-0.79) | 0 | 0.887 / – | -2.0% / – | – | 100 | – | 3.7 | -| mix | aa | 0 / 0 / 0 / 0 | – | 6/7 | 0.24 (-0.17) | 0 | 0.986 / – | 0.1% / – | 99.7 | 11 | – | 6.5 | -| mix | hard | 0 / 0 / 0 / 0 | – | 6/7 | 0.57 (-0.46) | 0 | 0.969 / – | -0.0% / – | 99.1 | 11 | – | 6.4 | -| varying | aa | 0 / 0 / 0 / 0 | – | 10/10 | 2.03 (-1.08) | 0 | 0.735 / – | 4.8% / – | – | 22 | – | 5.2 | -| varying | hard | 0 / 0 / 0 / 0 | – | 10/10 | 1.92 (-0.94) | 0 | 0.736 / – | 5.2% / – | – | 22 | – | 5.3 | +| lines w1 | aa | 0 / 0 / 0 / 76 | – | 43/48 | 0.71 (-0.69) | 88 | 0.790 / – | -6.2% / – | – | 186 | – | 8.0 | +| lines w1 | hard | 0 / 0 / 0 / 0 | – | 42/48 | 1.50 (-1.42) | 142 | 0.644 / – | -25.8% / – | – | 184 | – | 6.8 | +| lines w2 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.32 (-0.20) | 0 | 0.914 / – | -0.3% / – | – | 71 | – | 7.7 | +| lines w2 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.38 (-0.10) | 0 | 0.808 / – | -2.0% / – | – | 58 | – | 7.2 | +| lines w3 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.26 (-0.17) | 0 | 0.931 / – | -3.0% / – | – | 64 | – | 8.0 | +| lines w3 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.46 (-0.24) | 0 | 0.881 / – | -2.5% / – | – | 50 | – | 7.7 | +| lines w4 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.25 (-0.18) | 0 | 0.964 / – | -0.6% / – | – | 70 | – | 7.8 | +| lines w4 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.40 (-0.15) | 0 | 0.906 / – | -0.1% / – | – | 54 | – | 7.6 | +| lines w6 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.30 (-0.18) | 0 | 0.965 / – | -0.3% / – | – | 65 | – | 8.4 | +| lines w6 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.45 (-0.12) | 0 | 0.923 / – | 1.4% / – | – | 55 | – | 8.2 | +| lines w8 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.37 (-0.20) | 0 | 0.967 / – | -1.0% / – | – | 66 | – | 9.8 | +| lines w8 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.46 (-0.18) | 0 | 0.934 / – | -0.3% / – | – | 53 | – | 9.1 | +| lines w10 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.35 (-0.16) | 0 | 0.969 / – | -0.7% / – | – | 66 | – | 10.1 | +| lines w10 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.50 (-0.12) | 0 | 0.942 / – | -0.1% / – | – | 52 | – | 9.8 | +| lines w12 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.40 (-0.16) | 0 | 0.969 / – | -0.6% / – | – | 70 | – | 11.0 | +| lines w12 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.54 (-0.11) | 0 | 0.949 / – | -0.2% / – | – | 53 | – | 10.7 | +| curves w2 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.18 (-0.13) | 0 | 0.921 / – | -0.6% / – | – | 34 | – | 7.6 | +| curves w2 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.22 (-0.05) | 0 | 0.821 / – | -2.4% / – | – | 29 | – | 7.5 | +| curves w4 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.56 (-0.42) | 0 | 0.949 / – | -0.8% / – | – | 35 | – | 8.3 | +| curves w4 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.66 (-0.38) | 0 | 0.904 / – | -0.5% / – | – | 30 | – | 8.4 | +| curves w8 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 2.24 (-2.01) | 0 | 0.956 / – | -0.9% / – | – | 42 | – | 10.7 | +| curves w8 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 1.97 (-1.67) | 0 | 0.936 / – | -0.5% / – | – | 31 | – | 9.8 | +| circles | aa | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 32 | 0.964 / – | -0.9% / – | – | 71 | – | 11.3 | +| circles | hard | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 28 | 0.888 / – | -0.7% / – | – | 52 | – | 11.9 | +| junctions w2 | aa | 7 / 0 / 0 / 0 | 1.17 | 23/23 | 0.32 (-0.16) | 0 | 0.889 / – | 6.1% / – | – | 84 | – | 6.3 | +| junctions w2 | hard | 7 / 0 / 0 / 0 | 1.17 | 23/23 | 0.49 (-0.15) | 0 | 0.771 / – | 4.9% / – | – | 59 | – | 6.1 | +| junctions w4 | aa | 7 / 0 / 0 / 1 | 1.62 | 23/23 | 0.33 (-0.26) | 0 | 0.919 / – | 5.0% / – | – | 100 | – | 6.9 | +| junctions w4 | hard | 7 / 0 / 0 / 1 | 1.62 | 23/23 | 0.65 (-0.32) | 0 | 0.850 / – | 7.0% / – | – | 81 | – | 6.8 | +| junctions w8 | aa | 7 / 0 / 0 / 2 | 2.76 | 23/23 | 0.43 (-0.27) | 0 | 0.937 / – | 2.7% / – | – | 93 | – | 7.9 | +| junctions w8 | hard | 7 / 0 / 0 / 1 | 2.76 | 23/23 | 0.80 (-0.35) | 0 | 0.913 / – | 2.5% / – | – | 77 | – | 7.7 | +| crossings w3 | aa | 5 / 0 / 0 / 2 | 0.25 | 20/20 | 0.27 (-0.18) | 0 | 0.935 / – | -1.1% / – | – | 74 | – | 4.6 | +| crossings w3 | hard | 5 / 0 / 0 / 5 | 0.25 | 20/20 | 0.41 (-0.17) | 0 | 0.791 / – | -7.6% / – | – | 69 | – | 4.5 | +| crossings w6 | aa | 5 / 0 / 0 / 1 | 0.25 | 20/20 | 0.35 (-0.17) | 0 | 0.925 / – | -3.6% / – | – | 112 | – | 5.6 | +| crossings w6 | hard | 5 / 0 / 0 / 4 | 0.25 | 20/20 | 0.51 (-0.20) | 0 | 0.872 / – | -2.0% / – | – | 81 | – | 5.4 | +| dashed w2 | aa | 0 / 0 / 0 / 0 | – | 62/62 | 0.28 (-0.20) | 0 | 0.927 / – | -2.6% / – | – | 64 | – | 3.7 | +| dashed w2 | hard | 0 / 0 / 0 / 0 | – | 62/62 | 0.33 (-0.11) | 0 | 0.926 / – | -3.6% / – | – | 67 | – | 3.7 | +| dashed w4 | aa | 0 / 0 / 0 / 0 | – | 58/58 | 0.30 (-0.20) | 0 | 0.952 / – | -0.1% / – | – | 61 | – | 3.9 | +| dashed w4 | hard | 0 / 0 / 0 / 0 | – | 58/58 | 0.37 (-0.08) | 0 | 0.944 / – | -1.5% / – | – | 60 | – | 3.8 | +| letters w3 | aa | 7 / 0 / 0 / 6 | 1.47 | 17/17 | 0.62 (-0.47) | 1 | 0.923 / – | -0.9% / – | – | 114 | – | 5.3 | +| letters w3 | hard | 7 / 0 / 0 / 3 | 1.47 | 17/17 | 0.38 (-0.21) | 1 | 0.859 / – | -0.6% / – | – | 81 | – | 5.2 | +| letters w5 | aa | 6 / 1 / 0 / 6 | 2.10 | 17/17 | 0.99 (-0.83) | 0 | 0.929 / – | -1.7% / – | – | 124 | – | 5.9 | +| letters w5 | hard | 6 / 1 / 0 / 8 | 2.10 | 17/17 | 1.05 (-0.79) | 0 | 0.887 / – | -2.0% / – | – | 100 | – | 5.7 | +| mix | aa | 0 / 0 / 0 / 0 | – | 7/7 | 0.25 (-0.18) | 0 | 0.988 / – | 0.2% / – | 99.9 | 22 | – | 10.2 | +| mix | hard | 0 / 0 / 0 / 0 | – | 7/7 | 0.56 (-0.46) | 0 | 0.956 / – | -0.4% / – | 98.9 | 22 | – | 10.3 | +| mixed-width body | aa | 0 / 0 / 0 / 0 | – | 3/3 | 0.23 (-0.05) | 0 | 0.985 / – | 0.3% / – | 99.3 | 9 | – | 4.4 | +| mixed-width body | hard | 0 / 0 / 0 / 0 | – | 3/3 | 0.27 (-0.03) | 0 | 0.972 / – | -0.6% / – | 98.3 | 8 | – | 3.7 | +| varying | aa | 0 / 0 / 0 / 0 | – | 10/10 | 2.03 (-1.08) | 0 | 0.735 / – | 4.8% / – | – | 22 | – | 9.4 | +| varying | hard | 0 / 0 / 0 / 0 | – | 10/10 | 1.92 (-0.94) | 0 | 0.736 / – | 5.2% / – | – | 22 | – | 8.7 | -**aa totals**: junctions 44 correct, 1 wrong degree, 0 missed, 94 extra (of 45); ends 682 of 688; spurs 121. +**aa totals**: junctions 44 correct, 1 wrong degree, 0 missed, 94 extra (of 45); ends 686 of 691; spurs 121. -**hard totals**: junctions 44 correct, 1 wrong degree, 0 missed, 22 extra (of 45); ends 681 of 688; spurs 171. +**hard totals**: junctions 44 correct, 1 wrong degree, 0 missed, 22 extra (of 45); ends 685 of 691; spurs 171. **Scope of the improvement:** correct junction degrees rise from 30/45 to 44/45 in each variant; missed junctions fall from two to zero. Extra junctions fall from 104 to 94 aa and 27 to 22 hard. diff --git a/docs/localization-parity.md b/docs/localization-parity.md index ca5906af9e..9080695a8e 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -20,23 +20,26 @@ English) in-app help, and the docs are English. **Native review:** every catalog was written clean-room from the English meaning, most of it by agents (`bflatastic`), with community contributors adding or correcting (German by `dwetscher`, Japanese and Traditional -Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker; the other catalog headers ask for one. +Chinese by `imachus`). Georgian and the original Gujarati catalog are recorded as reviewed by native speakers; the contributor approved all 387 later Gujarati additions on 2026-11-14. + +> **Gujarati status (2026-11-14, #6613):** 4,673 catalog rows: the original 4,286 remain unchanged; all 387 additions are contributor-reviewed and approved, with 11 wording corrections. Status/error messages remain partial, and the pinned CI/release fonts and web startup list contain no Gujarati face; web selection stays unavailable until a Gujarati interface face is bundled. Scores and estimates unchanged. ## The twelve key languages | Language | Code | UI strings translated | Dialogs / tooltips / messages | Script support | Native review | Status | To `full` (h) | |---|---|---|---|---|---|---|---| | English | `en` | source, 100% | all | Latin | — | full | — | -| Simplified Chinese (Mandarin) | `zh-hans` | 5,102 rows, 791 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | all (3,439 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 40 missing messages for it) | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) | +| Simplified Chinese (Mandarin) | `zh-hans` | 5,102 rows, 791 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | all (3,554 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 55 missing messages for it, which open #7116, #7211, #7330, #7378 and #7405 translate) | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) | | Spanish | `es` | 4,009 / 4,020 (99.7%) | all (604 messages) | Latin | no | full | — (review) | | Hindi | `hi` | 8,782 rows (2026-11-13): 3,014 `@msg`, 47 `@msgplural`, 41 `@plural`; no gaps in the untranslated report | menus, dialogs, tooltips and scanned messages; contributor-approved loanword terminology; agent wording pass | UI and document text shape with HarfRust; installed desktop Devanagari fallback, or a `Deva` craft-fonts face; web font still needed | contributor terminology review; 2026-11-13 wording pass on messages and longer labels, with a glossary-term test; wider proofread welcome | complete catalog, machine-assisted draft | native review, layout and web font | +| Gujarati | `gu` | 4,673 rows (2026-11-14): original 4,286 unchanged; all 387 additions contributor-reviewed and approved | menus and dialogs; status/error messages partial | installed Gujarati fonts on desktop; optional `Gujr` craft-fonts fallback; no Gujarati face in the pinned CI/release fonts or web startup list | original catalog reviewed by a native speaker; all 387 later additions approved by the contributor, with 11 wording corrections | contributor-reviewed catalog | remaining messages and a bundled Gujarati web font | | Arabic | `ar` | 0 | none | document text: bidi layout and Paragraph Direction; interface: no right-to-left layout, no Arabic shaping in egui | no | none | 25–40 | -| French | `fr` | 4,020 / 4,020 (100%) | all | Latin | no | full | — (review) | +| French | `fr` | 9,166 rows, 3,436 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 178 missing messages for it | all | Latin | no | full | — (review) | | Portuguese | `pt-br` | 9,260 rows, 3,445 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 39 missing messages for it; one/other plurals | all | Latin | contributor is a Brazilian Portuguese speaker; further native review welcome | full | — (review) | | Indonesian | `id` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one plural form | dialogs and tooltips all; 750 message rows (French has 796) | Latin | no | full | — (review) | | Japanese | `ja` | 4,004 / 4,020 (99.6%) | all | CJK UI font as above; Japanese IME; vertical type partial; Japanese crop marks and composition settings | no | full | — (review) | | German | `de` | 3,952 / 4,020 (98%; menus, literals and messages complete when counted) | all (3,433 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 48 missing messages for it) | Latin | contributor-written, not reviewed | full | — (review) | -| Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | all | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) | +| Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | all (3,438 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 39 missing messages for it) | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) | | Vietnamese | `vi` | 4,886 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form; 4,046 plain + 18 `@plural` + 28 contextual + 794 `@msg` | all | Latin with Vietnamese diacritics (stacked marks attach in precomposed, decomposed and keyboard forms, #4491; glyph tests pass) | contributor is a Vietnamese speaker; AI-assisted draft, further native review welcome | full | — (review) | **Historical 12-language score: ~82%** (equal weights: 100 + 85 + 100 + 0 + 0 + 100 + 100 + 100 + 100 + 98 + 100 + 100, over 12). @@ -97,6 +100,9 @@ font for the web build. | Date | Change | Summary | |---|---|---| +| 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. | +| 2026-11-12 | minor | Gujarati (`gu`): reviewed by a native speaker; zero/one singular plurals (CLDR); Gujarati craft-fonts faces join the UI fonts; strings added on main since branching translated (4,286 rows) | +| 2026-11-11 | minor | Gujarati (`gu`): 4,235-row machine-assisted interface catalog, one/other plurals, desktop Gujarati font fallback and a glyph-coverage filter; status and error messages not yet translated; native review pending | | 2026-11-06 | minor | Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged | | 2026-10-30 | minor | Bosnian (`bs`, Latin, Ijekavian): complete catalog of 5,841 rows, every `bs-*` locale, one/few/other plurals; AI-assisted, not yet native-reviewed | | 2026-10-29 | minor | Vietnamese text: letters with two marks (Ể, Ứ, Ự) attach in precomposed, decomposed and keyboard forms, and a font lacking the marks falls back for the whole letter (#4491); scores unchanged | diff --git a/docs/mcp.md b/docs/mcp.md index 6174d8b359..d12c7491fc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -835,9 +835,13 @@ them sets what the graph stores. While Global Edit is on, a path directly in a s match (a graph's paths are matches only while the graph is isolated) has what changed on it stored for its series: the paint, weight, opacity and blend mode of the topmost visible fill and stroke, and the object's opacity and blend mode. `addon.arrange.randomizeOpacity` and `addon.arrange.sort` with `apply: "opacity"` store the object opacity, with the -blend mode, of the frontmost path of a series whose opacity they change. A gradient is stored with its placement, and -every path of the series takes that placement when the graph is drawn again. The graph does not store a stroke's -dashes, caps, joins, arrowheads, width profile or brush. These commands do not store the colors they give a graph's axes and labels, which are drawn again +blend mode, of the frontmost path of a series whose opacity they change. A gradient is stored with its placement relative +to the box of the path it was stored from (a stroke's box is the path's bounds grown by half the weight), and every path +of the series, the legend swatch included, takes it at the same place on its own box when the graph is drawn again, also +after the data changes or the graph moves. The graph saves this as `fillUnitBox` and `strokeUnitBox` in its series +paints. A gradient stored from a path with no width or no height is drawn at the place in the document where it was +stored. So is a gradient a graph stored without these flags. The graph does not store a stroke's dashes, caps, joins, +arrowheads, width profile or brush. These commands do not store the colors they give a graph's axes and labels, which are drawn again with the paint they had. `text.setFormat` without `start` and `end`, on a graph or on its labels, stores the character and paragraph attributes @@ -1305,7 +1309,9 @@ Drive a tool like a mouse: Edit a gradient with the Gradient tool's annotator (the bar runs along the vector; stop chips sit 10 px under it, midpoint diamonds 7 px above): a click on the bar adds a stop, dragging a chip moves it (drag it off the bar to -delete it, hold Alt to copy it), dragging the end handle changes the vector. The selected stop +delete it, hold Alt to copy it), dragging the end handle changes the vector. Both ends of a vector drawn on the art, +the start of one a click applies, and the dragged start and end handles snap as drawn points do; with Shift, the end +or the handle keeps to 45° steps and snaps along them. The selected stop (`gradient.selectStop`) takes Delete and ←/→: ```json @@ -1798,6 +1804,13 @@ commands fail and change nothing, and `appearance.removeItem` removes nothing. A the item last. A text `start` or `end` is past the end of the text. `object.slice.divide` refuses such a `rows` or `columns`, `recolor.reduce` keeps each color in its own row for such a `colors`, and `layer.setProps` refuses such a `color`. `object.setBounds` reads a `reference` past 8 as 8, as `object.transformEach` does. +An index or place given as a value that is not a whole number of 0 or more, such as `"3"`, `2.5`, `-1` or `true`, is +refused with `` `anchor` must be a whole number ``, naming the key: `anchor` of `path.setHandle`, `path.moveAnchor` and +`text.setPathHandle`, `segment` of `path.insertAnchor`, `to` of `artboard.reorder`, `library.moveGroup` and +`graphicStyle.move`, `subpath` of `path.appendAnchor`, `path.close` and `path.reverse`, and `artboard` of +`document.rulerOrigin`, `document.setRulerOrigin` and `document.resetRulerOrigin`. A required one left out or null is +reported as missing (`missing anchor`). Left out or null, `subpath` takes the command's default and `artboard` is 0. +Nothing changes and no undo step is recorded. The blend, envelope and gradient mesh commands refuse a string or another value that is not a number with the same message, as in `` `steps` must be a number, not "8" ``, and change nothing: `steps`, `distance` and `value` in `object.blend.make` and `object.blend.options`; `bend`, `h`, `v`, `horizontalDistortion` and `verticalDistortion` in @@ -2158,7 +2171,7 @@ VectorCraft's own, and `vc.api.has("dollar")` is true where they are available: | `build`, `version` | VectorCraft's version | | `buildDate` | `null` | | `decimalPoint` | `","` for a locale whose language writes a decimal comma (such as `de`, `fr`, `ru`), otherwise `"."` | -| `engineName` | `"VectorCraft"` | +| `engineName` | the name `#targetengine` gave the engine the script runs in, or `"VectorCraft"` without that line | | `error` | the last `Error` object the run threw, caught or not, or `null` before one | | `flags` | `0`, or `512` while localization is on; only `0` and `512` can be set, and setting `512` turns `$.localize` on | | `gc()` | does nothing; the interpreter collects its own garbage | @@ -2166,7 +2179,7 @@ VectorCraft's own, and `vc.api.has("dollar")` is true where they are available: | `global` | the global object | | `hiresTimer` | the microseconds since the run last read it, or since the run started; `0` on the web | | `level`, `memCache` | `0`; setting `memCache` throws | -| `line`, `stack` | the line of the code that reads it, counted in the file that code is in (the script, a file it included or a file `$.evalFile` ran, in this run or an earlier run of its named engine); the running code, outermost first, one a line as `name@file:line` (`[global]` for the script's own code, `[anonymous]` for a function without a name; the file as `$.fileName` gives it, or the run's name for a script given as `source`), at most 64 lines. Code that `eval` or `Function` compiled keeps the line of the source they compiled, in the file `[eval]`; code whose place a named engine no longer keeps is at line `0` of `[unknown]` | +| `line`, `stack` | `$.line` is the line of the code that reads it, counted in the file that code is in (the script, a file it included or a file `$.evalFile` ran, in this run or an earlier run of its named engine). `$.stack` lists the running code, outermost first, one frame a line as `name@file:line`: `name` is `[global]` for code outside a function (in the script, in a file it included, or in code that `$.evalFile` or `eval` ran) and `[anonymous]` for a function without a name, and `file` and `line` are the file and line that code is in, so `file` is the file `$.fileName` gives in that code. It lists the innermost 64 frames; when there are more, a first line `…` stands for the ones left out. In code from a script given as `source`, `file` is that run's name, while `$.fileName` is empty. Code that `eval` or `Function` compiled keeps the line of the source they compiled, in the file `[eval]`, while `$.fileName` there is the file being run; code whose place a named engine no longer keeps is at line `0` of `[unknown]`, and `$.fileName` there is empty | | `locale` | the interface's language as a locale such as `de` or `pt_BR` (`en` when the interface follows the system); a script can set `ll` or `ll_RR`, and `null` or `""` sets the interface's again | | `localize` | `false`; while `true`, `String(object)` of a localization object gives its text for `$.locale` | | `os` | `Windows`, `macOS`, `Linux`, `FreeBSD`, `Unix` or `Web`, without a version | @@ -2510,8 +2523,12 @@ neutral, the default). In a knockout group each child hides what it covers of th pass the enclosing group's setting to their children, off groups never knock out. `knockoutShape: true` makes the object's opacity and opacity mask scale how much it knocks out. `transparency.togglePageKnockoutGroup` and `transparency.togglePageIsolatedBlending` (`{value?}`) treat the whole page as a knockout or isolated group; they are -saved with the document and undoable, and `transparency.info` reports them. PDF and SVG write knockout groups as -soft-masked groups with the same look (the PDF export reports a warning). +saved with the document and undoable, and `transparency.info` reports them. PDF writes a knockout group as a PDF +knockout group, which PDF import reads back as one. Where an element knocks out less than the area it paints +(`knockoutShape: true` with opacity or a mask, or translucent paint), PDF draws each element through one soft mask of +the overlapping elements above it, as SVG does for every knockout group of translucent art. A group whose masks would +hold more than 65,536 objects (PDF) or shapes (SVG), or that has more than 2,048 elements, is written as a PDF knockout +group or a plain SVG group instead, and the export reports a warning. Groups are not isolated unless Isolate Blending is on: a blend mode inside a group with opacity, a blend mode, an opacity mask, knockout or a clip reaches the art below the group, on screen and in PDF (a knockout group's elements @@ -4798,7 +4815,10 @@ type, with the image under it. A group's or layer's own fills and strokes become members are expanded too. A symbol instance whose topmost fill or topmost stroke is visible, has an opacity above 0 and has a geometry effect becomes a group of its art: that fill's effects reshape the filled shapes of the art and that stroke's effects reshape its stroked shapes, which keep their colors, as the canvas draws them (VectorCraft's own -rule); the instance's other fills and strokes are dropped. The result is `{ids}`; when an image in the art can't be decoded, the images made from +rule); the instance's other fills and strokes are dropped. A raster effect on that fill or stroke draws on the filled +or stroked shapes of the art, and expanding makes it an image as for other objects; with no geometry effect, the +instance stays an instance over a shadow or glow image, and a blur, feather or inner glow replaces it with the image. +The result is `{ids}`; when an image in the art can't be decoded, the images made from raster effects do not show it, and the result also carries the `warning` that Flatten Transparency gives. A path's strokes stay strokes, as the [Expand objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/expand-objects.html) help @@ -6596,8 +6616,8 @@ number, naming the key, and place nothing; null is the same as leaving the key o `edit.copy` keeps the copied objects with the document resources they use: image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used; never the built-in [Registration] swatch), the gradient swatches their gradients came from, graphic styles the objects are linked to, character and paragraph styles and -brushes. Width profiles live in the preferences, so they need no copying. Every `edit.paste*` command (also `edit.pasteWithoutFormatting`, which leaves -text styles behind) brings them into the active document in the same undo step: an identical resource is reused, +brushes, with the swatches, patterns and text styles a graph keeps for its series and labels. Width profiles live in the preferences, so they need no copying. Every `edit.paste*` command (also `edit.pasteWithoutFormatting`, which leaves +text styles behind, those of graph labels included) brings them into the active document in the same undo step: an identical resource is reused, a missing one added, and one of the same name that differs is added renamed ("Mark 2"; a copy an earlier paste added is reused), which the pasted objects follow. Linked colours show the document's swatch at their tint (a Lab spot colour as its Spot Colors option says). The result reports `{ids, added, merged, renamed: [{kind, from, @@ -10511,9 +10531,13 @@ give the same result there. A paragraph whose characters differ in line metrics scale, or an inline graphic taller than the text) is composed with Every-line in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line that reaches where the frame is -narrower, or beside a wrap object, gets the room there. Such a paragraph falls back to Single-line when the frame has -no room for all of it, or when the search for its breaks passes VectorCraft's own limits (1,000,000 lines tried and -2,000,000 baseline steps for one paragraph, 2,000,000 lines and 4,000,000 steps for all the paragraphs of a frame). +narrower, or beside a wrap object, gets the room there. When the frame has no room for all of such a paragraph, the +lines it holds are still composed with the whole paragraph: VectorCraft measures the lines past the frame's end as +wide as the span of the last line the frame holds, less their indents. Those lines overflow, or go on in the next +threaded frame, which composes them again at its own widths. Such a paragraph falls back to Single-line when the +frame has no room for its first line, or when the search for its breaks passes VectorCraft's own limits (1,000,000 +lines tried and 2,000,000 baseline steps for one paragraph, 2,000,000 lines and 4,000,000 steps for all the +paragraphs of a frame). The UI command `type.lineComposer` (Cmd+Alt+Shift+C, in remote mode `press_key {key: "C", mods: {cmd: true, alt: true, shift: true}}` or `invoke_menu`) sets `singleLine` when every paragraph of the selected type (or every paragraph the Type tool's selection touches) uses Every-line, else `everyLine`, and returns the composer it set. @@ -11074,6 +11098,33 @@ whole points and Escape cancels. A drag keeps a Contour's own corners and miter options `corners` and `miterLimit` (`tool.setOption`). A border with a gap is two Contours of one shape (the gap first, so it ends up in front), baked with `effect.expandAppearance`, then `object.pathfinder.minusFront`. +## 3D Box and shapes add-on + +The `addon.shapes.*` commands draw rotatable 3D boxes (`addon.shapes.box3d`) and rectangles, ellipses, polygons, stars and +spirals that keep their settings (`addon.shapes.rectangle`, `ellipse`, `polygon`, `star`, `spiral`), and +`addon.shapes.configure` changes those settings on the selected shapes (or `ids`). In a drawing command, an omitted or +null value takes the default in the table. In `configure`, an omitted or null value keeps each shape's setting. A value +outside its range is an error that names the parameter and the range, and the command then draws or changes nothing. +The ranges are VectorCraft's own: + +| Parameter | Commands | Range (default) | +|---|---|---| +| `width`, `height` | `rectangle`, `ellipse`, `box3d` | pt, 0 or more | +| `radius` | `polygon`, `spiral` | pt, 0 or more | +| `radius` (corners) | `rectangle`, `configure` | pt, 0 or more (0); `configure` fits a larger radius to half each rectangle's shorter side | +| `radii` | `rectangle`, `configure` | four numbers, 0 or more: `[tl, tr, br, bl]` | +| `radius1` | `star` | pt, 0 or more | +| `radius2` | `star` | 0.01 to 1 times `radius1` (half of it) | +| `sides`, `points` | `polygon`, `star`, `configure` (`sides`) | a whole number from 3 to 1000 (6 sides, 5 points) | +| `rounded`, `randomized` | `polygon`, `star`, `configure` | −10 to 10 (0) | +| `spokeRatio` | `configure` | 0.01 to 1 | +| `turns` | `spiral`, `configure` | 0.01 to 1024 (3) | +| `divergence` | `spiral`, `configure` | 0 to 1000 (1) | +| `innerRadius` | `spiral`, `configure` | 0 to 0.999 of the radius (0) | +| `arcMode` | `rectangle`, `ellipse`, `polygon`, `star`, `configure` | `whole`, `pie` (or `slice`), `chord`, `arc` (or `open`) (`whole`) | + +A `configure` call that changes no shape records no undo step and returns `{updated: 0}`. + ## Cutting Tools add-on Cutting Tools is a built-in add-on that prepares art for cutting. Its commands are `addon.cutting.bridges`, diff --git a/docs/roadmap.md b/docs/roadmap.md index 74dac90ec8..9fa76dc027 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -12,6 +12,8 @@ > **PDF import status (2026-11-14, #6957):** Outlined Indic glyphs retain their full stroke properties; translucent glyphs stay separate, and a soft mask attaches to each glyph separately. Scores and estimates unchanged. +> **Gujarati status (2026-11-14, #6613):** 4,673 catalog rows: the original 4,286 remain unchanged; all 387 additions are contributor-reviewed and approved, with 11 wording corrections. Status/error messages remain partial, and the pinned CI/release fonts and web startup list contain no Gujarati face; web selection stays unavailable until a Gujarati interface face is bundled. Scores and estimates unchanged. + ## Feature submission evidence M4: Explicitly requested editing data keeps a single artboard larger than its 14,400 pt capped PDF page (#6865). Scores and estimates unchanged. @@ -128,6 +130,8 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V Hindi interface follow-up (2026-11-06): the catalog and zero/one plural rule are implemented; native wording review, layout checks and a web Devanagari font remain. [Notes](localization-hi.md). +Gujarati interface follow-up (2026-11-14): the catalog and zero/one plural rule are implemented. The original 4,286 rows remain unchanged; all 387 additions are contributor-reviewed and approved, with 11 wording corrections. Status/error messages remain partial, and a bundled Gujarati web font remains to be supplied. + ## Current focus 1. **Interaction-fidelity pass** (G1) by tool, from [ui-parity.md](ui-parity.md): Pen and Curvature, Direct @@ -169,6 +173,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed) | ⬜ (new ruler-guide Alt orientation implemented; existing-guide Escape cancellation regression-tested; broader pass remains) | 60–90 | | — | Hardening at scale (big-file corpus, fuzzing, cross-platform + browser QA) | 🟡 PngSuite integrity and bounded fetching verified (#1729); real-world corpus/manual QA remain | 50–80 | | M14 follow-up | Color Picker layout (#2008) | ✅ Compact controls, centered header, stacked actions, aligned fields and screen sampling; narrow windows scroll vertically | Included above | +| M14 follow-up | Gujarati interface (#6613) | 🟡 Zero/one singular plurals and desktop font fallback; 4,673 contributor-reviewed rows, including 387 approved additions with 11 wording corrections; status/error messages remain partial and pinned fonts lack a Gujarati face | Included above | | | **Total to full parity** (one agent; from [target-app-parity.md](target-app-parity.md)) | | **~360–590** | Milestone estimates predate the 2026-10-10 re-measure for M1–M14; the area table in @@ -202,6 +207,8 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. | +| 2026-11-12 | minor | Gujarati (`gu`): 4,286-row machine-assisted catalog reviewed by a native speaker, zero/one singular plurals, desktop font fallback and optional Gujarati craft-fonts fallback; all 72 translation tests pass; a web font and about 2,600 untranslated messages remain; scores and estimates unchanged | | 2026-11-12 | minor | Explicit editing-data import keeps a single artboard beyond a capped 14,400 pt PDF page (#6865), with a warning; automatic mode stays unchanged. Scores and estimates unchanged. | | 2026-11-12 | minor | M7: Glyphs panel applies feature-valued alternates and unencoded glyphs while keeping Arabic joining (#5514). Scores and estimates unchanged. | | 2026-11-12 | minor | M14: numeric fields keep the caret when a focus click is released more than 1 pt from its press; a tap within 1 pt still selects all (#6659, follow-up to #1326); estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ac6664f3aa..4e8fdb6de7 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -14,6 +14,8 @@ > **PDF import status (2026-11-14, #6957):** Outlined Indic glyphs retain their full stroke properties; translucent glyphs stay separate, and a soft mask attaches to each glyph separately. Scores and estimates unchanged. +> **Gujarati status (2026-11-14, #6613):** 4,673 catalog rows: the original 4,286 remain unchanged; all 387 additions are contributor-reviewed and approved, with 11 wording corrections. Status/error messages remain partial, and the pinned CI/release fonts and web startup list contain no Gujarati face; web selection stays unavailable until a Gujarati interface face is bundled. Scores and estimates unchanged. + ## Feature submission evidence EPS embedded fonts, 2026-11-09 (#6023–#6067): the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined Unicode CMaps set as type) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. Evidence: `crates/eps/src/import/tests_composite.rs`, `tests_type42.rs`, `tests_cid.rs`, `tests_type1.rs`; details in [file-format-parity.md](file-format-parity.md). @@ -624,6 +626,7 @@ since 2026-10-01. - **`.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. +- **EPS writes a repeated freeform gradient's image once:** a freeform gradient whose image samples the same on more than one fill (copies of a shape moved with their points, or one shape in a symbol's instances) has its samples written once in the setup, as a repeated image's are, and a fill whose image differs (other colors, another size) gets its own. A freeform fill drawn once is written where it is drawn, as before, and a fill whose placement cannot be drawn, or that arrives when the page is already at the 256 MB limit, is no longer sampled first (eps `tests::a_freeform_gradients_image_drawn_twice_writes_its_samples_once`, `tests::freeform_gradients_sampled_differently_dont_share_an_image`, `tests::a_field_taken_back_at_the_page_limit_leaves_no_image_behind`). 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. - **Fitted gradients turn with the Transform effect:** a gradient fitted to the object (not placed with the Gradient tool) turns, reflects and stretches with the object when its Transform effect rotates, reflects or scales it unevenly, as Object › Transform already does: the gradient is fixed to its fit on the geometry the effect takes, then follows the effect and the effects after it as a placed gradient does. Under moves and even scales it draws as a refit does, and a Transform effect that scales the object to nothing across leaves it fitted, with Scale Strokes & Effects or Transform Patterns on too, and on each copy. This holds on the canvas and in Object › Expand of the fill, for an object's own effects, a fill's or stroke's own, and a group's, where the gradients fitted to its paths, to its own fill, to the groups inside it and to its images' and symbol instances' own fills each fix to their own object's fit. Existing documents with such a Transform effect on fitted-gradient art draw differently: before, the gradient refit to the turned outline. The Gradient tool still shows a fitted gradient's vector at its unturned fit. How gradients follow the effect is VectorCraft's own rule; what the effect does to the object is described in [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html) (effects `bake::tests::a_fitted_gradient_turns_reflects_and_stretches_with_a_transform_effect`, `bake::tests::a_transform_effect_that_scales_to_nothing_leaves_a_fitted_gradient_fitted`, `group::tests::a_groups_transform_turns_the_gradients_fitted_to_its_members_and_its_own_fill`; render `objectfx::a_fitted_gradient_turns_and_reflects_with_its_transform_on_the_canvas`; engine `objexpand::a_fitted_gradient_turned_by_a_transform_expands_as_the_canvas_draws_it`). Scores and hours unchanged. @@ -644,6 +647,7 @@ since 2026-10-01. - **Control bar docks to the top or the bottom, or floats:** the Control bar's panel menu, at the bar's right end, has Dock to Top and Dock to Bottom, the edge in use checked and neither while the bar floats. Dragging the bar's gripper and releasing it within 24 points of the top (under the menu bar) or the bottom of the window docks it at that edge, with a band along that edge while it moves; releasing it farther from both edges floats the bar there, with its outline under the pointer while it moves, and a floating bar's gripper moves it or docks it again (`window.control.dock {edge: "top"|"bottom"|"float"}`), as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). Escape cancels a gripper drag. The 24-point zones, the floating bar's 800-point width and first position, a release outside the window leaving the bar where it was, a window too short for a floating bar docking it at the nearer edge, the floating bar staying inside a window that shrinks, and keeping the edge and the floating position with the workspace, with older workspaces docking the bar at their edge, are VectorCraft's own rules. Regression tests: `the_control_bar_panel_menu_docks_it_to_the_top_or_the_bottom`, `dragging_the_control_bar_gripper_docks_it_at_the_nearer_edge`, `dragging_the_control_bar_gripper_away_from_both_edges_floats_it`, `floating_control_bar_moves_and_redocks_from_each_edge`, `floating_control_bar_menu_docks_to_either_edge`, `control_bar_drag_escape_restores_placement`, `control_bar_drag_release_outside_window_keeps_previous_placement`, `floating_control_bar_stays_reachable_in_tiny_and_resized_windows`, `the_control_bar_dock_command_takes_top_bottom_or_float` and `menus_stop_above_a_control_bar_docked_at_the_bottom` in `crates/ui-egui/src/chrome/tests.rs`, `saved_workspaces_restore_the_control_bar_edge` and `saved_workspaces_restore_floating_control_bar_position` in `crates/ui-egui/src/workspaces.rs`. - **Control bar panel menu turns groups of options on or off:** the Control bar's panel menu lists the bar's groups of options (Snapping, Anchor Points, Tool Options, Object Options, Image Options, Fill and Stroke, Opacity, Graphic Style, Character, Paragraph, Vertical Alignment, Align and Distribute, Transform, Artboard Options and Document Options) above Dock to Top and Dock to Bottom, each checked while it is on; a click turns that group off or on (`window.control.options {group, visible?}`), and a group that is off stays out of the bar even while its tool or object is selected, as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). The list of groups and what each holds, every group on to start, a group that is off leaving its room to the groups after it, and keeping the groups with the preferences and the workspace are VectorCraft's own rules. Regression tests: `the_control_bar_panel_menu_turns_each_group_on_or_off`, `the_control_bar_keeps_its_label_and_menu_with_every_group_off`, `the_control_bar_panel_menu_scrolls_in_a_short_window`, `the_control_bar_options_command_turns_a_group_on_or_off`, `groups_that_are_off_leave_their_room_to_the_later_groups`, `the_artboard_tool_s_control_bar_follows_its_two_groups` and `the_anchor_points_group_holds_convert_and_anchors` in `crates/ui-egui/src/chrome/tests.rs`, `the_image_options_group_holds_the_image_and_trace_controls` in `crates/ui-egui/src/tests/place.rs`, `the_character_and_paragraph_groups_hold_the_type_controls` in `crates/ui-egui/src/panels/paragraph/tests.rs`, `saved_workspaces_restore_the_control_bar_groups` in `crates/ui-egui/src/workspaces.rs`. - **Control bar separators stand only between groups:** the Control bar draws a separator between two groups of options only when a control follows it in the bar. When the groups after a separator are off in the panel menu, do not apply to the selection or the tool, or have no room in the window, the bar ends with the last control and no separator; two separators with nothing between them show as one. Each separator keeps its room, so the groups keep their places. This is VectorCraft's own rule. Regression test: `no_separator_is_left_at_the_end_of_the_control_bar` in `crates/ui-egui/src/chrome/tests.rs`. +- **Fill and Stroke and Opacity stay inside a narrow Control bar:** the Control bar leaves out a Fill and Stroke, Opacity or Graphic Style control it has no room for, whole, instead of drawing it past the panel menu button or under it. The bar draws its controls from left to right, each only when the room left still holds it, so a narrower control can still show after a wider one that was left out: the Opacity link with its field can show when the Stroke link with its weight field did not fit, the Style link with its chip can show when the Opacity link with its field did not, and with nothing selected Preferences can show when Document Setup did not fit. With something selected, the Fill and Stroke, Opacity, Graphic Style and Character controls show only while the Transform link still has room after them, so the bar keeps the link and leaves those controls out first. The Character controls show whole or not at all, and while they are left out the Paragraph, vertical alignment and Align controls after them are left out too. The Snapping menu, the selection's label, the Convert and Anchors groups, the isolation button, Document Setup and Preferences also show only while they fit, and a group separator only while it has room. This is VectorCraft's own rule. Regression tests: `the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window`, `a_narrow_control_bar_shows_the_style_link_without_the_opacity_field`, `a_narrow_control_bar_shows_preferences_without_document_setup` and `the_control_bar_shows_the_inline_transform_fields_only_when_they_fit` in `crates/ui-egui/src/chrome/tests.rs`, `the_control_bar_trims_its_paragraph_section_after_the_controls_to_its_right` in `crates/ui-egui/src/panels/paragraph/tests.rs`. - **Pattern-only backquote drags:** With Selection move, bounding-box scale or rotate, or the Rotate, Reflect, Scale or Shear tool, backquote held at mouse release applies the drag matrix to selected pattern paint while object geometry stays in place. The key may be pressed after mouse press; if released before the mouse button, the drag commits as an ordinary transform. Objects without pattern paint stay still; stroke-only patterns and patterned objects in mixed selections take the same matrix. Regression tests: `backquote_move_uses_release_state_and_one_commit` and `backquote_box_scale_and_rotate_use_pattern_matrix_at_release` in `select/tests.rs`, `backquote_drag_previews_and_commits_pattern_only_for_each_transform_tool` in `xform/transform.rs`, `selection_backquote_drag_changes_pattern_fill_and_stroke_without_moving_mixed_art` and `selection_backquote_drag_changes_stroke_only_pattern` in `tests/pattern.rs`, `egui_backtick_drag_uses_key_state_at_release` in `canvas/tests.rs`, and `synthetic_pointer_backquote_keeps_solid_object_in_place` in `control/tests.rs`. Scores and hours unchanged. - **Existing-guide Escape cancellation (G1):** Escape clears a pending guide press or drag through Selection and Direct/Group Selection, cancelling a begun preview. Eight app-input regressions cover both axes, move/copy, temporary selection-tool borrowing from idle Pen, preselected-guide document/interaction-start selection and Undo/Redo/saved-state preservation, later on-canvas motion/release, Shift-press cancellation, and ordinary-release/Enter/idle controls. Historical focused checks on `8ab574b7` recorded five expected RED failures with three passing controls, then all eight GREEN tests and the Direct Selection Escape control passed. All nine also passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. The original 1280×960 headless app capture from `b076025a` was reviewed for one vertical Selection guide after Escape; source review found its scene unchanged on `a0671777`. Scores and hours unchanged. - **Escape in more tool drags:** Escape during an Anchor Point drag, a Mesh tool drag, a Direct Selection drag of a mesh point, a corner widget or the brackets of type on a path, or a Shape Builder drag cancels it: the art goes back to how the press found it, no undo step is added, the release does nothing, and the tool stays chosen. Escape during a Measure tool drag brings back the measurement before it. Regression tests: `escape_cancels_*` in `draw2/anchor/tests.rs`, `meshedit.rs`, `direct/tests.rs`, `builder/tests.rs` and `xform/measure.rs`, and `escape_cancels_an_anchor_point_mesh_shape_builder_or_measure_drag` in `tests/toolescape.rs`. @@ -671,7 +675,7 @@ since 2026-10-01. - **Artboard tab-switch follow-up:** changing the active document through its tab aligns the Artboard tool with the displayed board before Properties Delete; clicking the already-active tab preserves multiselection and Move/Scale Artwork options. Three headless `chrome::tests::artboard_document_tab_*` regressions cover actual tab A → B → A clicks followed by Properties Delete, surviving boards and guides, preserved artwork/symbols and Undo/Redo; controls cover same-tab multiselection, Move/Scale Artwork options and other tools. Historical focused validation on `dbd2497b` recorded one expected RED failure with two passing controls, then all three GREEN tests passed. All three also passed in historical full CI on `a0671777`. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. - **Artboard clean active-tab close (2026-10-15):** with the Artboard tool active, closing a clean active tab restores the surviving document's displayed board before Properties Delete. Nine targeted checks passed: three existing tab regressions, one clean close/Delete regression and five existing unsaved-document controls. The close regression checks the correct surviving board, preserved artwork/symbols and object selection, affected guides, one Delete edit, saved state and Undo/Redo. Three reviewed original full-app headless captures show the active tab before closing, the surviving document's shown board after closing, and the correct survivor after Properties Delete, using the actual tab × and Delete controls. - **Colour emoji with bitmap glyphs:** `sbix` (Apple Color Emoji) and `CBDT` (Noto Color Emoji) glyphs draw as their largest strike's PNG, placed with the type and unpainted by its fill; PDF writes them in Type 3 glyphs or as images, SVG as images with outlined text, EPS and legacy `.ai` as images, and Create Outlines makes embedded images. Synthetic test fonts in `crates/text/src/test_fonts.rs`; tests `crates/text/src/tests/bitmaps.rs`, `crates/render/src/tests/bitmapglyphs.rs`, `crates/svg/tests/it/colour_glyphs.rs`, `pdf::tests::colour_bitmap_glyphs_export_as_images`, the `eps` `colour_bitmap_glyphs_*` tests and `tests_pathops::create_outlines_turns_bitmap_glyphs_into_images`. COLR and OpenType SVG fonts remain. -- **COLR colour fonts:** v0 and v1 glyphs draw as vector layers (solid colours, linear and radial gradients, clips, blend modes, the foreground colour from the run's fill) on the canvas, as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as Type 3 text in PDF, and as editable groups with Create Outlines; sweep gradients and Porter-Duff compositing draw the glyph as an image. Measured by `tests_colr`, `render::tests_colrglyphs`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colr_glyphs_*`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups`. Scores unchanged (estimated). +- **COLR colour fonts:** v0 and v1 glyphs draw as vector layers (solid colours, linear and radial gradients, including ones that repeat or reflect, clips, blend modes, the foreground colour from the run's fill) on the canvas, as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as Type 3 text in PDF, and as editable groups with Create Outlines; sweep gradients, Porter-Duff compositing and radial gradients that repeat or reflect and start at a circle draw the glyph as an image. Measured by `tests_colr`, `render::tests_colrglyphs`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colr_glyphs_*`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups`. Scores unchanged (estimated). - **OpenType SVG colour fonts:** each glyph's SVG document is read by the SVG importer (registered by the engine) into vector art in the run's colours, then drawn, exported and outlined as COLR glyphs are; PDF real text writes them as text that draws nothing and draws their art where they sit. Measured by `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`. Scores unchanged (estimated). - **Create Outlines keeps the type object's own appearance:** Type › Create Outlines and Object › Expand put the type object's own fills and strokes (the Appearance panel's items around its Characters row) on paths of the whole outline, below or above the glyph paths as that row places them, with one gradient across every glyph, so the outlines draw what the type drew; `object.expand.info` reports those strokes, pattern fills and gradient fills. Outlined characters keep their stroke and fill (the help page, ); a type object's appearance is separate from its characters' (the help page, ); where the object's items go is VectorCraft's own rule. Measured by `tests::pathops::create_outlines_keeps_the_objects_own_fills_and_strokes`, `tests::objexpand::create_outlines_draws_the_type_objects_own_paint`, `tests::objexpand::type_objects_own_stroke_pattern_and_gradient_are_reported`, `tests::objexpand::type_without_an_outline_reports_no_own_stroke_or_fill`, `tests::objexpand::a_stroke_under_an_unpainted_stroke_is_reported_and_outlined` and `tests::objexpand::expand_keeps_the_type_objects_own_paint`. Scores unchanged (estimated). - **Composite fonts that two documents define differently:** when two open documents each define a composite font (Type › Composite Fonts…) by the same name with different parts, each document draws and exports its type in its own definition: in a tiled or floating window drawn while the front document draws, when it is placed in a document that defines the name differently, and in an export made while another document is in front. Evidence: `tests::composite::nested_loans_of_one_name_use_each_documents_definition` and `threads_lending_one_name_at_once_use_their_own_definitions` (`crates/text`), `tests::fontchange::a_placed_document_draws_its_own_definition_of_a_name_the_page_defines` (`crates/render`). Scores unchanged (estimated). @@ -812,7 +816,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Scripts: an error `$.evalFile` throws for a file it cannot run is reported at that file's line and column only while no `catch` has taken it. Caught and thrown again later, in the script or in another file, it is reported where it was thrown again, and a new error whose message holds its text is reported where that error was thrown. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`. - Scripts: an uncaught error that one or more `finally` blocks pass on unchanged, in the script or in a function it called, is reported at the line and column where it was thrown, in `script.run`'s report and the Script Console. An error a `finally` block throws is reported at that throw, and one its `return`, `break` or `continue` discards is not reported. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`. - Scripts: an error a `finally` block passes on is still reported where it was thrown when the block's allocations collect garbage under a small heap limit, and a `finally` block that `return`, `break` or `continue` leaves drops the saved location of its error as it leaves, so a collection no longer counts that code as live. VectorCraft's own rule. `crates/js/src/host_tests.rs`. - - Scripts: a run reads its preprocessor lines before any of it runs: `#include` and `//@include` put another file in place of the line, looked for beside the including file, in the `#includepath` folders and in `JSINCLUDE`; `#script` names the run; `#target` and `#strict off` are taken, `#strict on` turns on strict write checks (below), `#targetengine` names the engine (below), and `#engine` is refused. `$.evalFile` runs a file with the same globals and returns its last value, and `$.fileName` and `$.includePath` give the current script's file and the include folders. A script run from a file reads these files only in its folder and below, without `..` names or links that lead out, and an error in one is reported at its own file, line and column ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/), [the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/); `crates/engine/src/tests/script_include.rs`). + - Scripts: a run reads its preprocessor lines before any of it runs: `#include` and `//@include` put another file in place of the line, looked for beside the including file, in the `#includepath` folders and in `JSINCLUDE`; `#script` names the run; `#target` and `#strict off` are taken, `#strict on` turns on strict write checks (below), `#targetengine` names the engine (below), and `#engine` is refused. `$.evalFile` runs a file with the same globals and returns its last value, `$.fileName` gives the file of the code that reads it (the file being run for code that `eval` or `Function` compiled), and `$.includePath` the include folders. A script run from a file reads these files only in its folder and below, without `..` names or links that lead out, and an error in one is reported at its own file, line and column ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/), [the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/); `crates/engine/src/tests/script_include.rs`). - Scripts: `#targetengine name` (and `//@targetengine name`) runs the script in a named engine that lasts for the session, and a later script that names it sees the globals and functions earlier ones left ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/)); `$.engineName` gives the name ([the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: a script without the line and `#targetengine main` get a new engine each run, the line comes once in the script itself before its code, a session keeps 8 named engines with 64 MiB of heap each, an error or a stop keeps the globals the run set while the documents go back, kept host functions act for the run that calls them, `script.engines` lists the engines and `script.engine.drop` drops one, and the web build has none. `crates/engine/src/tests/script_engines.rs`. - Scripts: `$.fileName` is a read-only name of the script file in use ([the help page](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: it names the file of the code that reads it, so a function a file included with `#include` or run with `$.evalFile` defined, or one an earlier run of a named engine kept, gives its own file and not the calling file; code from a script given as `source`, and kept code whose place the engine dropped, gives an empty name, and code that `eval` or `Function` compiled gives the file being run. `crates/engine/src/tests/script_include.rs`, `crates/engine/src/tests/script_engines.rs`. - Scripts: a runtime error is shown at the line that was running ([the help page](https://extendscript.docsforadobe.dev/extendscript-toolkit/debugging-in-the-toolkit/)). VectorCraft's own rules: an error in code an earlier run of a named engine compiled, such as a function it defined, is reported at that code's file, line and column (the earlier run's script, a file it included or a file its `$.evalFile` ran), while the report and its console line keep the calling run's name; a script given as text is named by its run's name, and each engine keeps these places for 8 MiB of source maps, the oldest going first. An error in code whose place is gone, or in code that `eval` or `Function` compiled, is reported without a line and column, and the code still runs. `crates/engine/src/tests/script_engines.rs`. @@ -861,6 +865,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - A click on another object moves the contextual task bar under it without hiding it for the press; it gives way only to a drag (#4681). - With one image selected, the contextual task bar offers Image Trace, which traces the image at once with the Default preset (the Image Trace panel is left as it was), and Crop Image; with an Image Trace object it offers Expand, which keeps only the traced paths. This follows Illustrator's bar: for a selected image it includes Image Trace, Retype and Crop Image, and traced artwork can be expanded from it ([Contextual Task Bar announcement](https://community.adobe.com/announcements-651/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580)). Offset Path and Simplify, which don't apply to an image, aren't offered for one. Not done: Retype, which the app doesn't have. - With the Artboard tool, the contextual task bar sits under the selected artboards and offers Resize (Artboard Options for the active artboard), Duplicate (each selected artboard, in one undo step, the copies then selected), Rearrange All (with two or more artboards) and Export (Export for Screens with the selected artboards checked). This follows Illustrator's bar: while the Artboard tool is in use, it has options for resizing, duplicating and exporting artboards (the [Contextual Task Bar announcement](https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580); Rearrange and Export on the bar: [Managing Artboards in Illustrator](https://creativepro.com/managing-artboards-in-illustrator/)). + - On a blank canvas the contextual task bar offers Draw (Pencil), which switches to the Pencil tool, and Place Image, which picks files for the Place dialog, as the help page lists for a new document (). VectorCraft's own rules: the bar shows them while a selection tool is in use in a document with no artwork (layers and guides only), sits under the active artboard (under the canvas's center when the artboard is out of view), and leaves out the Fill, Stroke, Recolor Artwork and Lock controls. Not done: Generate Vectors. Evidence: `a_blank_canvas_offers_draw_and_place_image`, `the_blank_canvas_bar_draws_with_the_pencil_and_places_an_image` and `the_blank_canvas_bar_wraps_on_a_narrow_canvas` in `crates/ui-egui/src/tests/taskbar.rs`. - The contextual task bar offers more next steps by selection (): an open path Join and Edit Path (the Direct Selection tool), a closed path Edit Path, a compound path Edit Path and Release Compound Path, a clipping group Edit Clipping Path and Release Clipping Mask instead of Ungroup and Isolate, an image selected with vector art Make Clipping Mask instead of Unite, point or area type Convert To Area Type or Convert To Point Type beside Create Outlines, and type on a path, for which the overview names an item to add an effect and rearrange the text, Type on a Path Options (`type.pathOptions`; its options: ) beside Create Outlines. Evidence: `crates/ui-egui/src/tests/taskbar.rs`. - While a Type tool (Type, Area Type, Type on a Path and their vertical versions) adds or edits type, the contextual task bar shows the font family menu, the font style menu, the Font Size combo and the Fill chip (); the Touch Type tool shows no bar, as the same page describes. Type selected with a selection tool shows the same three font controls beside its Create Outlines, conversion and Type on a Path items (). The controls act as the Character panel's do: on the characters a Type tool selects, else on every selected type object, with the size left blank where the sizes differ. VectorCraft's own rules: a Type tool's bar leaves out the object actions, the Stroke chip, Recolor Artwork and Lock; a Type tool shows no bar before it adds type or with other art selected; type selected with other art shows no font controls; the font controls go on a second row when one row would be wider than the canvas, and narrow to fit a canvas narrower than they are; while a Type tool edits type, the Fill chip shows the edited characters' fill ("?" where it differs). Evidence: `the_type_tools_show_font_and_fill_controls`, `the_type_tools_show_no_bar_without_type`, `selected_type_shows_font_controls_beside_its_actions`, `the_task_bars_font_size_sets_what_the_character_panel_would`, `the_type_tools_fill_chip_activates_the_fill`, `the_type_tools_bar_is_named_and_stays_on_the_canvas`, `selected_types_font_controls_take_a_second_row_on_a_narrow_canvas` and `the_type_tools_fill_chip_shows_the_edited_characters_fill` in `crates/ui-egui/src/tests/taskbar.rs`. - The help page describes the contextual task bar as one horizontal row () and gives no rule for a canvas narrower than that row. VectorCraft's own rule: the bar stays whole on the canvas. When its actions don't fit in one row, they wrap, in order, onto rows under the row with the handle, the chips and More Options; an action wider than any row, or one that would need a row past the canvas's height, is listed in More Options instead; when the chips don't fit beside the handle and More Options, they take rows after the actions; with no row to spare for the font controls, the bar leaves them out; a canvas too small for the handle and More Options shows no bar. Evidence: `a_task_bars_actions_wrap_on_a_narrow_canvas` and `a_task_bar_action_wider_than_the_canvas_goes_in_more_options` in `crates/ui-egui/src/tests/taskbar.rs`. @@ -887,6 +892,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - A toolbar made with New Toolbar docks in a lane along the window's left or right edge when its title bar is dropped on that edge, or with Dock to Left and Dock to Right in the title bar's context menu. Toolbars docked at one edge stack from the top, each with its share of the lane's height, and the lane is as wide as its widest toolbar; a lane too short for its stack wraps it into more columns, up to half the window's width, and past that the toolbars show only their title bars, which the mouse wheel scrolls through when even they do not fit. Dragging the title bar out of the lane, or Float Toolbar, floats it again, and Escape during the drag puts it back. Hiding and renaming keep its place, and the workspace keeps it. New toolbars still start floating. The lanes and the stacking are VectorCraft's own rules (#1759; `window.toolbar.dock`, `window.toolbar.float`; `toolbars_dock_and_float_by_their_commands`, `docked_toolbars_stack_in_lanes_along_the_window_edges`, `a_title_bar_dropped_on_an_edge_docks_the_toolbar`). - Window › Toolbars › Keep Tools Beside the Canvas, a VectorCraft layout option that is off by default, docks the Tools panel between left-side panels and the canvas instead of at the window's left edge; the canvas's left edge then also docks a floating Tools panel, and workspaces keep the setting. Scores are unchanged (`window.toolsBesideCanvas`; `docked_tools_keep_the_window_edge_unless_kept_beside_the_canvas`, `tools_redock_at_the_targets_of_the_chosen_placement`). - Hindi (2026-11-06): Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged. [Terminology and limitations](localization-hi.md). + - Gujarati (2026-11-14, #6613): `gu` has 4,673 catalog rows, zero/one singular plurals, installed desktop font fallback and support for optional `Gujr` craft-font faces. The original 4,286 rows remain unchanged; all 387 additions are contributor-reviewed and approved, with 11 wording corrections. Status/error messages remain partial; the pinned fonts and web startup list have no Gujarati face, so web selection remains unavailable until one is bundled. Current-head validation is recorded in the PR; scores and estimates unchanged. - 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. @@ -904,10 +910,11 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The counts that Package's status and question, the folder search of Missing Fonts and Find Linked Files, the modified and missing linked file prompts and the CSS Properties panel's notes show use the interface language's plural forms instead of a fixed "(s)" form. Evidence: `the_missing_links_note_uses_each_count_form` in `crates/ui-egui/src/dialogs/tests_package.rs`, `the_modified_links_question_uses_each_count_form` in `crates/ui-egui/src/dialogs/missing_links.rs`, `search_counts_use_each_count_form` in `crates/ui-egui/src/dialogs/missing_fonts/tests.rs` and `the_unnamed_objects_note_uses_each_count_form` in `crates/ui-egui/src/panels/tests_css_properties.rs`. The status bar's messages for relinked, replaced, updated and embedded images, broken links, linked files that can't be read or changed on disk, images that can't be decoded, added and unavailable fonts, copied CSS, downloads and exports with their notes name one item in the singular and more in the plural, in English and in each language's plural forms (`counted_status_messages_take_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`). The status bar's messages for documents opened or saved with notes and for discarded or restored recovered documents, and the Global Edit options' Results line, do the same (`open_save_and_recovery_messages_take_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`); the warnings for linked images and placed documents output as their previews and the errors for a page past a placed file's last page or artboard name their counts in the singular or the plural in English. - - The Simplified Chinese catalog (`crates/ui-egui/src/i18n/zh-hans.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,439 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report lists 40 Simplified Chinese messages: the composite font, justification order and Blob Brush connect end errors that open #7211, #7116 and #7330 translate, and two messages that no catalog has a row for yet. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. + - The Simplified Chinese catalog (`crates/ui-egui/src/i18n/zh-hans.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,554 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins, including the image export, color profile, color book, swatch exchange file, DXF import and Revolve errors. The report lists 55 Simplified Chinese messages: the composite font, SVG filter, read root, graph design, justification order and Blob Brush connect end errors that open #7211, #7378, #7405, #7116 and #7330 translate. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. - Edit › Undo and Edit › Redo, in the in-window and native menus, name the step in the interface language as the History panel lists it (`panels::history::step_name`), so when the catalog has no plain row for a step named after an effect, such as Feather, the menu shows the translation of the effect's menu row without its ellipsis (`undo_and_redo_name_the_step_as_the_history_panel_does`). - The History panel translates the undo step names the engine, the tools and the shell write as literals: the untranslated report (`untranslated_report`) lists any a complete catalog lacks, and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis. - The German catalog (`crates/ui-egui/src/i18n/de.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,433 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report lists 48 German messages; 38 of them are the composite font, select-a-path and justification order errors that open #7211, #7214 and #7116 translate. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. + - The Korean catalog (`crates/ui-egui/src/i18n/ko.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,438 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report lists 39 Korean messages; they are the composite font, select-a-path, justification order and `path.freehand` connect errors that open #7211, #7214, #7116 and #7330 translate. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. - The History panel menu has Step Backward and Step Forward, which run Edit › Undo and Edit › Redo and are disabled when there is no step to undo or redo, and Set History Limit…, which opens Preferences at Performance, where History States is set. - The History panel menu's Clear History (`edit.clearHistory`) removes every step to undo and to redo and leaves the document as it is; it is not an undo step itself, and it is disabled when there is no step to clear. - The New Document from State button at the bottom of the History panel (`edit.newDocumentFromState`) makes a new document, which becomes active, from the state the panel has selected ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/undo-edits-and-manage-design-history.html)). VectorCraft's own rules: the new document takes the next Untitled-N name, is marked unsaved, has nothing selected and has no steps to undo or redo, and the source document and its steps stay as they are. @@ -1112,6 +1119,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur `path.curvatureEdit`, `path.removeAnchor`, `path.convertAnchor`, `path.reshapeSegment`, `path.split`, `path.setHandle`, `path.moveAnchor` and `path.insertAnchor` refuse a `subpath` that is not a whole number of 0 or more, and `path.split` refuses such an `anchor` or `segment`; without a `subpath` they edit subpath 0. A value given that is not a whole number, such as `"1"` or `1.5`, is refused with `` `subpath` must be a whole number ``, naming its key, as are a required index of the Appearance panel and Smart Pen commands, `sourceDocument` of `edit.dropSelection` and `index` and `to` of `document.activate` and `document.move`; a required one left out or null is reported as missing (`tests::params::a_malformed_required_integer_is_not_reported_as_missing`). `path.reshapeSegment` refuses a delta of 1e9 pt or longer, as `object.blend.spine.reshapeSegment` does, and `path.convertAnchor` and `path.freehand` refuse a point whose x or y is 1e9 pt or more in size. `path.reshapeSegment`, `object.blend.spine.reshapeSegment`, `path.convertAnchor`, `path.curvatureEdit`, `path.insertAnchor`, `path.moveAnchors`, `path.reshape`, `path.close` and the Pencil, Curvature, Knife, Eraser, Path Eraser, Smooth, Join and Blob Brush commands refuse a number, point or handle given as text, such as "10", and leave the document unchanged; `path.convertAnchor` refuses an `x` without a `y`, or a `y` without an `x`. + `path.setHandle`, `path.moveAnchor`, `text.setPathHandle` (`anchor`), `path.insertAnchor` (`segment`), `artboard.reorder`, `library.moveGroup`, `graphicStyle.move` (`to`), `path.appendAnchor`, `path.close`, `path.reverse` (`subpath`) and the ruler origin commands (`artboard`) refuse a value that is not a whole number of 0 or more, such as `"3"` or `2.5`, with `` `anchor` must be a whole number ``, naming the key, instead of reporting it as missing or reading it as the default subpath or artboard; a null `artboard` is artboard 0 (`tests::params::a_malformed_index_is_not_missing_or_its_default`). `path.close` and `path.setHandle` (`independent`), `path.moveAnchors` (`patterns`), the Pencil (`closed`, `keepSelected`, `fill`), Curvature (`closed` and a point's `corner`), `path.curvatureEdit` (`corner`), `path.reshapeSegment` (`perpendicular`) and the Blob Brush (`keepSelected`, `mergeOnlySelection`, `merge`) refuse a value other than true, false or null, such as "true", and leave the document unchanged. Direct Selection's marquee selects the frame anchors of area type that lie inside it, and a click on a frame anchor selects that one; Shift toggles them. The selected frame anchors show filled. A drag of one of them moves all of them, the arrow keys nudge them, and the type reflows at its size (`text.reshapeArea`, which takes `items` to move the anchors of several frames in one step), as [Create text](http://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html) and [Selecting objects](http://web.archive.org/web/20211230015528/https://helpx.adobe.com/illustrator/using/selecting-objects.html) describe. When the selection also holds other objects, path anchors or ruler guides, or when another change to the document or the selection follows the pick, the type is selected whole. A press on a selected object's centre point (Attributes panel › Show Center) selects the object whole, and a drag from it moves the object as one Move, held by that point (#4891; test `the_centre_point_selects_and_moves_the_object_whole`). @@ -1124,7 +1132,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Blend spine Anchor Point tool (#2544):** on a selected blend, dragging a spine corner makes symmetric Smooth handles; the tool displays and picks existing spine handles when switched to, drags one independently or retracts it on click, and clicking a point with handles retracts both. The spine is edited live without moving its key objects. Windows native canvas checks and screenshots reviewed: symmetric and independent handles, corner/retraction, Shift constraints and perpendicular reshaping, tool switching, key preservation, Undo/Redo, save/reopen, hidden handles and overlapping Path/Blend priority (PR #2643). Behaviour reference: [public path-editing documentation](https://helpx.adobe.com/illustrator/using/adjust-path-segments.html). - **Blend steps of brushed strokes:** the steps interpolate the pen pressure, tilt, bearing and rotation recorded with the Paintbrush, so a Calligraphic, Scatter or Art brush that varies with the pen changes gradually from one key to the next. A key without a recording counts as half pressure, half tilt and 0°, as brushes read it, and angles turn the shorter way round. When the blend starts a key's path at another anchor (an open path run from its end, a closed one turned to another start), the key's recordings, width profile and arrowheads turn with the path. Different brushes still switch halfway (VectorCraft's own rule; strokes blend their options, as [Blended objects overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/apply-transparency-and-blending/blended-objects-overview.html) describes; `crates/doc/src/blend.rs`, `pen_pressure_tilt_and_angles_interpolate_for_brushes`, `stroke_profiles_turn_with_a_key_path_started_elsewhere`). - **Path editing (#494):** the handles shown are the handles that drag: Direct Selection drags those of a path selected as a whole (as the Pen leaves it), the Anchor Point tool and Pen+Alt no longer grab handles that Show handles when multiple anchors are selected hides, a handle shorter than the tolerance leaves its anchor to be picked, and the anchor tools pick within Selection & Anchor Display › Tolerance. A dragged segment keeps a smooth anchor at either end smooth. With Direct Selection, the Pen, the Curvature and the anchor tools, a path selected as a whole shows all its anchors selected, and the Control bar and the Properties panel offer Convert (corner, smooth) for them too; Remove and Cut wait for anchors direct-selected (#504). A handle drag gathers its snap targets once per drag (it took 21 ms per pointer move on a 20,000-path document, now 1 ms; `vectorcraft-cli perf` measures handle and segment drags on a 5,000-anchor path). Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533), and removes the merged anchors in one pass over the path, so a long run of them no longer moves the anchors after it once per merge (`cmd::path::tests::a_long_run_before_many_other_anchors_merges_in_one_pass`); Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J, the "Create corner or smooth join" shortcut of [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)) moves two open path ends to their midpoint and joins them there, a corner or smooth point as its dialog's Points choice asks, in one undo step. - - **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. A series starts in greyscale; Group Selection's fill and stroke, the paint, weight, opacity and blend mode of a mark's top visible fill or stroke row after an Appearance command or a graphic style (each given a mark or its series picked with Group Selection), the Eyedropper, Default Fill and Stroke or Swap Fill and Stroke, the paint or stroke weight a stroke weight, gradient or recolor command changes there, and a mark's object opacity and blend mode, are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. A series that draws Filled Lines bands keeps no stroke paint or weight: its bands are drawn again with their 0.25 pt black outline. Graph Type's Override Calculated Values (both `axisMin` and `axisMax`) uses exactly that value-axis range, split into the tick count (5 when automatic). Line graphs take Edge-to-Edge Lines (off by default: the points sit at the centres of their categories; graphs saved before the option keep their edge-to-edge lines), and radar graphs follow Mark Data Points and Connect Data Points. Blank cells (an empty CSV cell or `null`) are values left out, not zeros: columns, bars and stacked segments skip them, lines and scatter lines break around them and the value axis ignores them, while area, pie and radar draw them as 0; a number in straight quotes is a label, and the data window writes it back quoted. Graph types combine: Graph Type on series picked with Group Selection (or `series` indexes) draws them as their own type, with column, stacked column, line and area mixing on the vertical value axis and bar and stacked bar on the horizontal one; stacked columns take one slot of the cluster, lines draw in front of columns and areas behind. The value axis goes on the left, the right or both sides (column, stacked column, line, area and scatter graphs; bar graphs keep theirs along the bottom), and Separate Scales gives the series assigned to the right axis a scale of their own with its own tick values. Each value axis takes tick marks of no, short or full (across the plot) length and a count per division, and a prefix and suffix for its numbers; the category axis takes tick marks too (none by default), at or between its labels. Graph designs: Object › Graph › Design lists the designs with a preview of the selected one, and New Design (the selected art), Rename, Delete Design, Paste Design (a copy back to edit) and Select Unused, and Marker draws the data points and legend swatches of line, scatter and radar series with a design, scaled so its backmost object fills the default marker's square. The Appearance, effect and graphic style commands do not edit a design's art inside a series, which the graph draws again from the design. Graph Data transposes rows and columns and switches a scatter graph's x and y, and a `|` in a category or series label starts a new line. Pie graphs take Legends in Wedges, Position (Even, Ratio sized by area, Stacked on one centre) and Sort (None, All, First). Graph Type takes column and cluster widths up to 1000% (overlapping past 100%) with First Row and First Column in Front (First Row orders each series' categories; across series the series order decides, so series stay one group each for Group Selection), draws filled lines, adds drop shadows behind columns, bars, lines and wedges, and puts the legend in rows across the top. Text formatted with the Character, Paragraph, Appearance, Transparency or Graphic Styles controls or the Eyedropper, on the whole graph or on labels picked with Group Selection, is kept per kind of label (value axis numbers, category labels, legend labels) through the graph being drawn again, the paint, weight, opacity and blend mode of a label's topmost visible fill and stroke rows and its object opacity and blend mode included. Object › Graph › Data opens a worksheet: cells picked with a click and edited above the sheet (Tab moves right, Enter down, the arrows between cells), Import Data from a tab-separated text file, Transpose, Switch x/y, Cell Style (decimals shown, column width), Revert and Apply. `graph.setData` returns and takes the data as CSV with each value written exactly, not rounded to two decimals, so applying the returned CSV changes nothing (#1485). + - **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. A series starts in greyscale; Group Selection's fill and stroke, the paint, weight, opacity and blend mode of a mark's top visible fill or stroke row after an Appearance command or a graphic style (each given a mark or its series picked with Group Selection), the Eyedropper, Default Fill and Stroke or Swap Fill and Stroke, the paint or stroke weight a stroke weight, gradient or recolor command changes there, and a mark's object opacity and blend mode, are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo; a kept gradient takes the same place on each mark's own box. A series that draws Filled Lines bands keeps no stroke paint or weight: its bands are drawn again with their 0.25 pt black outline. Graph Type's Override Calculated Values (both `axisMin` and `axisMax`) uses exactly that value-axis range, split into the tick count (5 when automatic). Line graphs take Edge-to-Edge Lines (off by default: the points sit at the centres of their categories; graphs saved before the option keep their edge-to-edge lines), and radar graphs follow Mark Data Points and Connect Data Points. Blank cells (an empty CSV cell or `null`) are values left out, not zeros: columns, bars and stacked segments skip them, lines and scatter lines break around them and the value axis ignores them, while area, pie and radar draw them as 0; a number in straight quotes is a label, and the data window writes it back quoted. Graph types combine: Graph Type on series picked with Group Selection (or `series` indexes) draws them as their own type, with column, stacked column, line and area mixing on the vertical value axis and bar and stacked bar on the horizontal one; stacked columns take one slot of the cluster, lines draw in front of columns and areas behind. The value axis goes on the left, the right or both sides (column, stacked column, line, area and scatter graphs; bar graphs keep theirs along the bottom), and Separate Scales gives the series assigned to the right axis a scale of their own with its own tick values. Each value axis takes tick marks of no, short or full (across the plot) length and a count per division, and a prefix and suffix for its numbers; the category axis takes tick marks too (none by default), at or between its labels. Graph designs: Object › Graph › Design lists the designs with a preview of the selected one, and New Design (the selected art), Rename, Delete Design, Paste Design (a copy back to edit) and Select Unused, and Marker draws the data points and legend swatches of line, scatter and radar series with a design, scaled so its backmost object fills the default marker's square. The Appearance, effect and graphic style commands do not edit a design's art inside a series, which the graph draws again from the design. Graph Data transposes rows and columns and switches a scatter graph's x and y, and a `|` in a category or series label starts a new line. Pie graphs take Legends in Wedges, Position (Even, Ratio sized by area, Stacked on one centre) and Sort (None, All, First). Graph Type takes column and cluster widths up to 1000% (overlapping past 100%) with First Row and First Column in Front (First Row orders each series' categories; across series the series order decides, so series stay one group each for Group Selection), draws filled lines, adds drop shadows behind columns, bars, lines and wedges, and puts the legend in rows across the top. Text formatted with the Character, Paragraph, Appearance, Transparency or Graphic Styles controls or the Eyedropper, on the whole graph or on labels picked with Group Selection, is kept per kind of label (value axis numbers, category labels, legend labels) through the graph being drawn again, the paint, weight, opacity and blend mode of a label's topmost visible fill and stroke rows and its object opacity and blend mode included. Object › Graph › Data opens a worksheet: cells picked with a click and edited above the sheet (Tab moves right, Enter down, the arrows between cells), Import Data from a tab-separated text file, Transpose, Switch x/y, Cell Style (decimals shown, column width), Revert and Apply. `graph.setData` returns and takes the data as CSV with each value written exactly, not rounded to two decimals, so applying the returned CSV changes nothing (#1485). An Eyedropper pick that includes transparency and leaves a mark's top fill or stroke row without paint, or removes that row when the source has no row of that kind, where that row had paint before, also replaces the opacity and blend mode the graph keeps for that kind of row in the series; a row that had no paint before the pick leaves them as they are. What Global Edit gives a mark from another match (in an isolated graph) is kept on the graph the same way, and so is the object opacity, with its blend mode, that Randomize Opacity and Sort Objects change on marks. - **Graph column designs:** Object › Graph › Column (`graph.column`) draws the columns, bars or stacked segments and the legend swatch of the series picked with Group Selection with a graph design: Vertically Scaled stretches it to each column's length, Uniformly Scaled scales it both ways and centers it, Repeating stacks a copy for each Each Design Represents value and chops or scales the last one (For Fractions), and Sliding keeps the part above the design's guide line in proportion and stretches the part below. The dialog lists the designs with a preview; the choice is saved with the graph and follows the design's rename and delete. @@ -1183,6 +1191,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Filter button at the right of Search All keeps the rows that are of an object kind ticked under By Object (paths, shapes, text, images, effects, groups, linked files, guides) and in a state ticked under By Layer (locked, hidden), with the rows that hold them; a section with nothing ticked takes every row. The filter combines with the search, the button is lit while a filter is on, and Clear All in its popup turns the filter off (`ui.layersFilter`). The filter lasts the session. The panel menu's Save Selection submenu has Save Selection…, Update Selection and Edit Selection…, enabled and run as in the Select menu, and lists the document's saved selections: choosing one selects its objects again (`select.recall`). Deleting rows from the Layers panel (the trash button, rows dragged onto it, the panel menu's Delete Selection, and Delete or Backspace on highlighted rows) asks "Delete this layer and its artwork?" when a layer or sublayer among them holds art, hidden, locked and nested art included, and "Delete the selected items and their artwork?" when several rows are deleted; OK deletes the rows it asked about in one undo step, and Cancel keeps them (`ui.layerDelete`). The help page's shortcut list names the Layers panel as the place where deleting asks for confirmation ([Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). An empty layer, a layer with only guides, and groups or objects alone are deleted without a question, and `layer.delete` itself does not ask (`panels::layers` tests `deleting_a_layer_that_holds_art_asks_first`, `delete_selection_asks_only_for_layers_with_art`). + The Command Palette lists Window › Layers › Delete Selection once, as the Layers panel's `ui.layerDelete`, so a pick asks the same question; it does not list `layer.delete`, which scripts run without a question. New Brush…, Brush Options…, Symbol Options…, Import SVG Filter… and Convert to Text are also listed once each, as the UI command that opens the dialog or file chooser (`palette` tests `a_wrapped_engine_command_is_listed_once_as_its_ui_command`, `a_pick_of_delete_selection_asks_before_deleting_a_layer_with_art`). `object.setProps` with `visible: false` or `locked: true` deselects every selected object that is then hidden or locked, the key object included, as the eye and lock columns do. Ctrl/Cmd+Alt-clicking an eye switches the other layers to Outline and the row's layer to Preview, or switches all layers back to Preview when the others are in Outline (`layer.outlineOthers`, `layer.previewAll`). Ctrl/Cmd+Alt-clicking New Layer adds a layer directly below the current layer, at its level (`layer.new {below: true}`), and Ctrl/Cmd+Alt-clicking New Sublayer adds a sublayer under the contents of the layer it goes into (`layer.newSublayer {bottom: true}`), each in one undo step. The reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists these clicks for placing a layer below the selected layer and a new sublayer at the bottom of the layer list. @@ -1263,6 +1272,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The solids of one Extrude & Bevel (both kinds) or Inflate effect on one object, its fill's, its strokes' and a group's members' together, keep up to 163,840 faces (VectorCraft's own limit). Each solid is built in turn; once their faces pass the limit, no later solid is built, the object draws its source art, and applying or editing the effect, Export 3D Object and Expand as Wireframes report that the 3D surface is too complex. Tests `solids::tests::an_objects_solids_count_their_faces_together`, `a_groups_solids_together_past_the_limit_draw_the_source`, `a_fill_and_strokes_together_past_the_limit_draw_the_source`. - Extrude & Bevel (both kinds) and Rotate keep the art's curves in the faces they draw, export and expand: caps and the edges of walls and bevels where they meet a cap are cubics with smooth anchors where the art's are, fitted within 0.001 pt in perspective and within the sampling tolerance on a bevel's inset. An edge of a twisted wall that is cut into pieces, a piece of curve that does not fit after 8 halvings, and the curves past 1,048,576 for one solid stay straight (VectorCraft's own values; [three-d.md](three-d.md#what-differs-from-the-public-documentation)). Tests `solid::tests::a_cap_keeps_the_curves_of_the_art`, `a_cap_in_perspective_fits_the_projected_curves`, `a_beveled_cap_and_its_walls_share_their_curves`, `twisted_beveled_tiny_and_degenerate_caps_keep_their_curves`. - Export 3D Object (Asset Export's menu): an object with Extrude & Bevel, Revolve, Inflate or Rotate becomes a 3D asset whose Format is USDA (the default), USDZ, glTF or OBJ, and Export writes each of its solids as a mesh with a material in its paint's color (`assets.add3D`, `assets.set3DFormat`, `assets.export3D`; [export 3D vector artwork](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/export-3d-vector-artwork.html)). + - Export 3D Object builds each Extrude & Bevel (both kinds) and Inflate solid once: the same build gives the faces that count toward the object's limit and the faces of its mesh, and the meshes match those of a separate build bit for bit. Tests `mesh::tests::a_meshed_solid_is_built_once`, `an_extruded_solid_and_its_mesh_match_the_two_built_apart`, `inflated_solids_and_their_meshes_match_the_two_built_apart`, `solids::tests_mesh::one_build_exports_the_meshes_built_apart`, `a_stroke_past_the_limits_is_left_out_of_the_export`. - Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Open ends have continuous caps by default, shading is smooth with one to ten colored lights, and intersecting surfaces' visibility is approximate ([revolve.md](revolve.md)). - Revolve takes graphics on its swept side, shaded with the object's light ([the archived reference, page 185](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=185)). By VectorCraft's own rule a graphic wraps around the axis at its own size at every distance from it, narrows toward the silhouette, follows the profile along its length and is hidden where the side faces away (`threeD.mapGraphic`; `revolve::tests_graphics`, `tests::side_faces_lie_on_the_unrolled_surface_and_caps_do_not`, `map_graphic_puts_a_graphic_on_the_side_of_a_revolve`). - Revolve takes type, as its glyph outlines, and groups or layers, whose members sweep about one axis as [the help page](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html) describes; a surface can have up to 16,384 profile points, and curves take fewer samples when a surface would pass 163,840 faces (#3617). @@ -1310,7 +1320,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Mouse wheel over the font family field (#5253):** In the Character and Properties panels and the Control bar, turning the mouse wheel over the closed font family field applies the next family to the selected type (the previous one when turned up), keeping the style when the family has it, else its closest. Each notch is one undo step; leaving the field or Escape keeps the family, and Undo takes it back. The steps use the font menu's order at the first turn and keep it until the pointer leaves the field or the text selection changes, so recent-font updates do not reshuffle later turns. That order follows the font filters every font menu shares (one chosen in the Glyphs panel's menu or a dialog's applies too) but ignores its search text, puts recent families first, and stops at the first and last family. A turn with a modifier key is left to the panel and the canvas. Behavior reference: the [Illustrator CS6 reference](https://help.adobe.com/archive/en/illustrator/cs6/illustrator_reference.pdf#page=10) describes editing hovered dropdown controls with the mouse wheel; [Find and apply fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-and-apply-fonts.html) names the family field's places. Applying it to the font field, the order, the ends and the undo steps are VectorCraft's own rules. Tests: `crates/ui-egui/src/tests/font_menu.rs`, `crates/ui-egui/src/font_menu/tests.rs`, `crates/ui-egui/tests/font_wheel.rs`. - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, Text Flow by rows or by columns (#4978), vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`), and an anchor of the path of type on a path outside perspective, the type reflowing along it (`text.reshapePath`, #3980). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. The Character panel's Variable Font sliders set each axis (see Fonts). Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Cmd+Shift+C (Align center) works on Windows and Linux too (#1940). A double-click on area type's bottom middle handle turns on Auto Size (#1940). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches back to the tool that was active before type editing began (kept while clicks move editing to other text), or to the Selection tool when there was none (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). Each reference shows an icon of the height it measures before its name, in the open list and in the closed field (#6304): a frame around a letter for Em Box, a bar from the baseline to the top of a capital for Cap Height and of a lowercase x for x-Height, and a frame inset in the em square for ICF Box. With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html). - The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. Such a paragraph is broken with the Single-line Composer when the frame has no room for all of it or when the search passes VectorCraft's own limits. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html). + The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. When the frame cannot hold all of such a paragraph, the lines it holds are still composed with the whole paragraph, the lines past the frame's end measured as wide as the last line the frame holds (VectorCraft's own rule); they overflow or go on in the next threaded frame, which composes them again. Such a paragraph is broken with the Single-line Composer when the frame has no room for its first line or when the search passes VectorCraft's own limits. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html), and for text that flows on between threaded objects, [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html). A text wrap object's Offset may be negative as well as positive ([the help page](https://help.adobe.com/archive/en_US/illustrator/cs4/illustrator_cs4_help.pdf#page=307)). A negative Offset (to -1000 pt) shrinks the wrap object by that distance, measured from each edge itself and from the edges of its holes, so lines beside it, above it and below it run that far into it, and an object narrower than twice the distance no longer moves text aside. With Invert Wrap, a negative Offset keeps the text that far inside the object. These are VectorCraft's own rules for the shape of the shrunk object. Text Wrap Options, `object.textWrap.make` and `object.textWrap.options` and the MCP `text_wrap` tool take the negative value, and layout uses it (`crates/text/src/layout/region.rs` tests, `cmd::textwrap::tests::text_wrap_options_negative_offset_reflows_and_round_trips`). In area type of a shape other than a rectangle, each line's whole band, ascent to descent, stays inside the frame: where an edge pinches in within the band, the line ends at the narrowest point, and a line runs beside a hole that starts and ends within its band, not across it. In area type of any shape, a line also stays clear of a text wrap object that reaches into any part of its band, and inside an Invert Wrap object over its whole band (`crates/text/src/layout/region.rs` tests). In area type whose frame has several contours, a line runs across an edge two contours share only when their edges lie exactly on one line, so a gap between contours keeps lines from crossing it, even a gap one unit in the last place wide near the edge of the canvas; an edge divided by an anchor exactly on it still lets lines cross (`layout::region::separate_slanted_contours_keep_a_four_ulp_gap_near_canvas_limit`, `shared_slanted_edge_still_joins_near_canvas_limit`, `area_type_sets_no_line_across_a_four_ulp_gap_between_slanted_contours`). @@ -1454,6 +1464,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Create Strokes uses exact squared Euclidean distances and Guo–Hall thinning in bounded per-blob crops. Maximal-disc pruning, split-crossing merges and degree-two joins simplify its skeleton graph. The whole-blob width rule and ink fitting remain; 46 code-drawn inputs measure ends and junctions in [`image-trace-quality.md`](image-trace-quality.md#centre-lines). Junction degrees measure 44/45 correct in each input variant; remaining spurs are recorded. This is a measured skeleton improvement; the Image Trace score, missing per-feature selection and width controls, and estimates remain unchanged. - Create Strokes decides stroke or fill per feature, as the [help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/image-trace-panel-options.html) describes the Strokes width: the widest feature of the image that can become a stroke. A shape wider than Stroke somewhere keeps as its area every disc wider than Stroke that fits in it, and each piece outside those discs that reaches out of them at least one and a half times its own width (and 3 px) is traced as a stroked center line, its end at the area carried to the cut so the line's cap closes the join. Corners and edge slivers stay in the area. Which pieces count as features is VectorCraft's own rule. Each line still has one width; without Fills, the area is a 1 px outline as before. Tests: `thin_features_of_a_wide_shape_are_strokes_and_its_body_a_fill`, `wide_shapes_without_thin_features_stay_one_fill` and `distances_to_seeds_equal_brute_force` in `vectorcraft-trace`. The Image Trace score and estimates are unchanged. + - The ignored centre-lines class measures that split on a mixed-width body scene: one disc with arms of 2, 3 and 5 px, at Stroke 6 px, traces as three stroked paths and one filled path in both the anti-aliased and hard input, with 100.0% of the arms' center samples found and 99.3% (anti-aliased) and 98.3% (hard) of the disc's pixels filled ([quality notes](image-trace-quality.md#centre-lines)). Test: `mixed_width_body_is_one_shape_traced_as_a_fill_and_strokes` in `vectorcraft-trace`. The Image Trace score and estimates are unchanged. Object › Live Paint › Merge adds the other selected paths and Live Paint groups to the bottom selected Live Paint group ([Live Paint groups](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/live-paint-groups.html)), which keeps its id, the paint of its faces and edges, and its name, lock, visibility, transparency, appearance, opacity mask and graphic style link. Object › Live Paint › Make with a Live Paint group selected, and a Live Paint Bucket click on a selected path beside a selected Live Paint group, add the paths to that group the same way. Make, Merge and the Live Paint Bucket take paths, compound paths and groups of them; with type, an image or another object among the selection they report that it has to be converted to paths first and change nothing, as type and raster images can't go into a Live Paint group directly ([Create Live Paint groups](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html)); before, such an object went among the group's hidden sources and vanished from the canvas. Evidence: `tests_build::live_paint_refuses_type_and_images_and_changes_nothing`. With the Live Paint Bucket selected, Alt-click on vector art samples its fill, stroke and stroke weight into the paint defaults ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)). On a placed image it samples the image pixel as a solid fill with no stroke. Alt+Shift-click samples only the color under the pointer, including an intermediate gradient color or image pixels, into the active fill or stroke ([Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). Neither click paints or changes the selection or selected art, and the next Bucket click paints with the sampled paint. Evidence: `tests_build::live_paint_bucket_alt_sample_paints_next_face_without_changing_selection`. Double-clicking the Live Paint Bucket opens Live Paint Bucket Options ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)): Paint Fills (on) and Paint Strokes (off) set whether a click paints faces, edges or both, Highlight turns the outline of the face or edge under the pointer on or off, and Color and Width set it (red and 4 px by default). Faces are outlined at the Width and edges at half of it, as the page says faces are highlighted with a thick line and edges with a thin line. With both on, a click paints the edge within 4 px of the pointer, else the face, and Shift switches to Paint Fills only; with one on, Shift switches to the other. The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintBucket"}`). Evidence: `builder::tests::{bucket_options_choose_what_a_click_paints, bucket_options_set_the_highlight, bucket_highlights_an_edge_thinner_than_a_face}`, `dialogs::live_paint_bucket::tests::double_clicking_the_tool_opens_its_options_and_ok_sets_them`. @@ -1519,7 +1530,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Swatches made from a shown color in a CMYK document:** the Color panel menu's Create New Swatch, an Alt-click on the Swatches panel's New Swatch button, the New Swatch dialog for a gradient, a Fill or Stroke proxy or the Gradient panel's thumbnail dropped on the Swatches panel (or Alt-dropped on a swatch of its kind), and the Gradient panel's Add to Swatches and Save to Swatches keep each color's model, as the Color Guide's and Color Themes' saved colors do: an RGB object's color gives an RGB swatch, and a gradient's RGB stops stay RGB. - **Sample Color from Screen:** an eyedropper button in the Color Picker and `ui.sampleScreenColor` (which can be given a key in Keyboard Shortcuts) pick a color anywhere on the screen with the system's sampler: `NSColorSampler` on macOS, the XDG desktop portal's [PickColor](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Screenshot.html) on Linux, and the EyeDropper API in browsers that have it. The color becomes the fill or the stroke as one undo step, or the Color Picker's color, converted from display sRGB into the working RGB space. On Linux, a session without a portal that has PickColor, a portal that does not return from the call within 25 seconds (VectorCraft's own limit) or leaves the bus before it answers, and a portal that ends the request another way each show a status message and end the wait. Windows, the BSDs and other browsers have no sampler yet; the button is gray there. - **Stroke visibility (#1253):** adding paint to a previously unpainted zero-width stroke restores the existing 1 pt default (`PaintDefaults::default`, `Appearance::set_stroke`), for the selected appearance item, the new-art defaults and Swap Fill/Stroke. Positive widths are kept; recoloring a painted zero-width stroke keeps its explicit zero. A real Eyedropper-to-new-shape sequence reproduces the zero-width inheritance. Five engine regressions cover creation, defaults, targeted items, preserved widths and one-step Undo/Redo (`crates/engine/tests/it/stroke_visibility.rs`). - - **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles snap and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh. + - **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles, both ends of a vector drawn on the art and the start of a clicked one snap as other drawn points do (path segments, construction guides and View › Snap to Glyph's guides included) and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh. - The Gradient panel's Delete Stop sits at the right end of the ramp, level with it, so a panel too short to show the stop fields still shows it; it deletes the selected stop (never below two), as dragging a stop down off the ramp does (#5403). - Conic gradients, VectorCraft's own gradient type: the stops sweep once around the center from the vector, clockwise on the page (a negative aspect, or a reflection, turns the sweep the other way), with a Conic button in the Gradient panel's Type row and the Gradient tool's extent ellipse and aspect handle; they follow transforms, Expand into wedges or a mesh, open from `.af` files, and export to PDF as a function-based shading that PDF import reads back as a conic gradient; SVG, EPS and legacy `.ai` files get solid wedges, and CSS a `conic-gradient`. - Diamond gradients, VectorCraft's own gradient type: the stops run from the center out to a square with a corner at the end of the vector (the aspect stretches it into a rhombus), with a Diamond button in the Gradient panel's Type row and the Gradient tool's square and aspect handle; they follow transforms, Expand into nested squares or a mesh, and export to PDF, SVG, EPS and legacy `.ai` as four linear gradients, one for each quarter of the square (an image in PDF and SVG for translucent stops, a dithered fill, the Perceptual method other than spot color shadings in PDF, or a square with no area), and on type characters in SVG as a radial gradient; PDF import reads a function-based shading whose color is the same along each square about its origin as a diamond gradient. @@ -1565,6 +1576,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Outline Object on an image's own fill or stroke follows the outline of the image's opaque area, and the image's frame when the image has no transparency. Path effects apply to a fill or stroke added to a bitmap object ([archived reference manual, page 561](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=561)), and with Outline Object such a stroke follows the boundary of the art when the image has transparent areas and the image's frame when it has none ([CreativePro article on strokes for placed images](https://web.archive.org/web/20250815214529/https://creativepro.com/add-strokes-placed-images-illustrator/)). VectorCraft's own rules: the opaque area is the pixels at least 50% opaque, and its outline is traced as Image Trace's Black and White mode traces at threshold 128 and fitted with curves. Evidence: `tests::imageoutline` (vectorcraft-effects), `opaque::tests` (vectorcraft-trace), `tests::containers::an_images_stroke_with_outline_object_follows_its_opaque_area` and `tests::containers::an_images_outline_object_stroke_exports_selects_and_measures_along_its_opaque_area` (vectorcraft-engine), `tests::objectfx::an_images_outline_object_stroke_follows_new_pixels_under_the_same_key` (vectorcraft-render). A geometry effect on one of type's own fills or strokes reshapes the whole outline that the fill or stroke paints, on the canvas and in exports; PDF, SVG, EPS and the other vector exports write such type as paths. On type that holds inline graphics, those effects are drawn and exported only when the type has a geometry effect or perspective of its own. A geometry effect on a symbol instance's fill reshapes the filled shapes of the symbol's art and leaves their strokes as they are; one on the instance's stroke reshapes the stroked shapes and leaves their fills as they are. The shapes keep their colors. Only the instance's topmost fill and topmost stroke count, and only while they are visible with an opacity above 0. The canvas and Object › Expand Appearance use the same art, SVG and PDF export bake the same shapes, and Expand Appearance turns the instance into an editable group in one undo step. A group's own fills and strokes in the art take the effects too. An instance inside the art passes the effects on to its own art, and a dynamic symbol's instance reshapes the fills and strokes it paints on the art. How the effects map onto the symbol's art is VectorCraft's own rule. The help page describes effects on one fill or stroke, and the help page describes Expand Appearance (engine `expand::a_symbol_instance_fill_effect_reshapes_its_filled_art_and_expands`, `a_symbol_instance_stroke_effect_reshapes_its_stroked_art_and_expands`, `a_stroke_effect_reshapes_a_stroke_a_dynamic_instance_adds`, `a_fill_effect_reshapes_the_fill_of_a_group_in_the_art`, `only_the_topmost_visible_instance_fill_reshapes_the_art`, `a_nested_symbol_instance_takes_the_outer_fill_effect`, `export_bakes_a_symbol_instance_fill_effect`, `a_fill_effect_on_an_instance_of_a_symbol_that_holds_itself_ends`). Existing documents with a geometry effect on a symbol instance's fill or stroke draw differently: before, the effect drew nothing. + A raster effect (Drop Shadow, Outer Glow, Gaussian Blur and the other effects that paint pixels) on a symbol instance's topmost fill draws on each filled shape of the symbol's art, and one on its topmost stroke draws on each stroked shape, after any geometry effects listed before it, under the same rules as geometry effects: only a visible fill or stroke with an opacity above 0 counts, a group's own fills and strokes in the art take the effect, and an instance inside the art passes it on. Each shape gets its own shadow or glow, which is VectorCraft's own rule. The canvas, Use Preview Bounds and the drawn bounds, SVG and PDF export draw it, and Object › Expand Appearance is enabled for such an instance and makes the effect an image at the document's raster effects resolution, in one undo step (engine `expand::a_symbol_instance_fill_shadow_shadows_its_filled_art`, `a_symbol_instance_stroke_shadow_shadows_only_its_stroked_art`, `a_symbol_instance_fill_shadow_follows_the_fill_offset_before_it`, `a_nested_symbol_instance_takes_the_outer_fill_shadow`, `export_draws_a_symbol_instance_fill_shadow`). Existing documents with a raster effect on a symbol instance's fill or stroke draw differently: before, the effect drew nothing. The Appearance panel's inline effect editor labels each parameter as the effect dialogs' generic fields do, without the colon ("Max. Radius", "Channel 1", "Create New Fields by"). Roughen's and Zig Zag's Points, Offset Path's Joins, Inner Glow's Source and the menus of Radial Blur, Smart Blur, Sprayed Strokes, Glass, Mezzotint, Halftone Pattern, Grain, Texturizer and De-Interlace are dropdowns with the dialog's labels that send the dialog's values, and a plug-in's choice is a dropdown of the plug-in's values. Revolve's edge, Extrude & Bevel's bevel, every Warp style's orientation, Rasterize's Color Model, Background and Anti-aliasing and the channel of Levels and Curves are dropdowns too, with the labels their own dialogs show. `effect.apply` and `effect.setParams` refuse a Levels or Curves `channel` other than `rgb`, `red`, `green` or `blue` (or `r`, `g` and `b`, in any letter case). Object › Expand Appearance keeps each stroke of a path a stroke (weight, caps, joins, dashes, arrowheads, alignment and width profile) on a copy of the path with the geometry effects baked in, and Object › Expand… is dimmed, with a tooltip, while the selection has appearance attributes, so a stroked line with Zig Zag becomes a stroked zig-zag line and only Expand then outlines it, the two steps the [Expand objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/expand-objects.html) help page describes (#3089). Brushed strokes still become their brush art; `object.expand` still bakes and outlines in one step for agents. @@ -1573,6 +1585,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Transparency:** three-state Knockout Group (on, neutral, off), Opacity & Mask Define Knockout Shape, Page Isolated Blending and Page Knockout Group, on screen and in SVG/PDF. SVG export writes a knockout group of opaque art plainly, without masks, so shapes meeting edge to edge leave no seam; translucent, blended, masked or brushed art knocks out through masks. Blend modes match the standard formulas on screen, in SVG and in PDF, and non-isolated groups blend with the art below. Flatten Transparency with presets, a dialog and a Flattener Preview panel. Flattening for files without transparency (PDF/X-1a, PDF/X-3) in a CMYK document keeps the inks where it rasterizes, as under an image with see-through pixels: the images it makes hold ink amounts, not screen colours the file would separate again, so a C100 M6 Y41 K3 panel stays C100 M6 Y41 K3 there (#2160, `engine/src/cmd/fileio/tests_pdfx.rs`). On the canvas, the renderer no longer draws the picture of the art below back as an image each time it copies the backdrop of a non-isolated group that blends: it keeps that picture and draws the art that follows over it. At 2880 × 1800, 50 translucent groups with Multiply members take 65 ms a frame instead of 299 ms on 8 threads (`vectorcraft-cli bench`), and the frames have the same pixels. Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports. + PDF export writes a knockout group as one PDF knockout group (`/K true`, with `/I true` when the group isolates; ISO 32000-1, 11.4.6.3), so a knockout group of any size opens again in VectorCraft as a knockout group, and the form nesting of the file does not grow with the number of elements. When an element's opacity or mask defines its knockout shape, or it paints translucently, each element is drawn through one soft mask of the overlapping elements above it instead; past 65,536 objects in a group's masks (or 2,048 elements) the group is a PDF knockout group, with a warning that its translucent elements knock out fully. SVG export draws each element of a knockout group through one mask of the overlapping elements above it, and the masks refer to one copy of each element's shape, so the elements stay siblings and the file nests no deeper with more elements; past 65,536 shapes in a group's masks (or 2,048 elements) the group is written as a plain group, with a warning. Evidence: `pdf/src/tests/blend.rs`, `svg/tests/it/blend_export.rs`, `pdf/tests/gradient_forms.rs` (a gradient in a knockout group reads back in place). - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; ten generated style libraries (including Game HUD, Status Effects, Painted and Sketched, whose seven styles each repaint a solid fill with one watercolor, dry brush, pastel, charcoal or ink effect, and Textures) save and load as `.vcstyles`. The built-in Textures graphic style library (`graphicStyle.addFromLibrary {library: "textures"}`) has eight styles that paint their fills and strokes with the texture effects: Fine Grain (Grain), Canvas Weave and Burlap Sack (Texturizer), Cracked Glaze (Craquelure), Mosaic Floor (Mosaic Tiles), Quilted Squares (Patchwork), Leaded Glass (Stained Glass) and Damp Paper (Water Paper). The help page names new graphic styles for textures ([What's new](https://helpx.adobe.com/illustrator/desktop/new-features/whats-new.html)) without naming them, so the styles, their names and their settings are VectorCraft's own (`tests::stylelib::textures_styles_apply_their_texture_effects_and_keep_their_params_through_save_and_open`). In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one. @@ -1646,6 +1659,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A gap between glyphs of two sizes is measured against the smaller one, so a word set beside a much larger one with no space glyph between them (a browser's margin between two sizes, as in `≤ 4` and a small `hours`) keeps its word space (#3623). SVG import's style cascade, used for hidden objects, non-scaling strokes and type, tests each element only against the `