From b67a08dc0a41996578672184022777249ef10561 Mon Sep 17 00:00:00 2001 From: ncic <1848265+ncic@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:29:09 +0100 Subject: [PATCH 01/44] Pencil: shows where a stroke continues, connects and closes The continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears an open end of another selected path it connects to. The straight-line cursor becomes a short level line. A stroke continuing a path, a straight line too, shows the closing ring within Close Paths When Ends Are Within of the path's other end, and the release closes it; path.freehand takes the tool's closed for extend instead of its own four-Fidelity distance. --- crates/engine/src/cmd/draw2/commands.rs | 2 +- crates/engine/src/cmd/draw2/freehand.rs | 19 ++++-- crates/engine/src/tests/draw2.rs | 25 ++++++++ crates/tools/src/cursor.rs | 12 ++-- crates/tools/src/cursors.toml | 10 +++- crates/tools/src/draw2/gesture.rs | 77 +++++++++++++++++++----- crates/tools/src/draw2/gesture/tests.rs | 80 ++++++++++++++++++++++--- crates/ui-egui/src/cursors.rs | 12 ++-- crates/ui-egui/src/cursors/tests.rs | 17 ++++-- docs/mcp.md | 11 +++- docs/target-app-parity.md | 5 +- docs/ui-parity.md | 5 +- 12 files changed, 217 insertions(+), 58 deletions(-) diff --git a/crates/engine/src/cmd/draw2/commands.rs b/crates/engine/src/cmd/draw2/commands.rs index 400e13baab..e6f88a31e2 100644 --- a/crates/engine/src/cmd/draw2/commands.rs +++ b/crates/engine/src/cmd/draw2/commands.rs @@ -11,7 +11,7 @@ pub fn specs() -> Vec { "Pencil", [], None, - "{points: [[x,y]…] or [[x,y,pressure]…] or [[x,y,pressure,tiltX,tiltY,rotation]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic and Scatter brushes with Pressure variation; a point without one presses fully. Pen tilt in degrees, each −90..90, x positive leaning right and y leaning down the page, and barrel rotation in degrees clockwise: the stroke keeps how far the pen leaned, which way and how far its barrel turned, for brushes with Tilt, Bearing and Rotation variation; null for what a point lacks), fidelity?: pt (1.5; from 0.000015625 (0.01 / 640) to 100 / 0.0313 (about 3194.89), the Pencil's and the Paintbrush's 0.01 to 100 px Fidelity at the highest and the lowest zoom; a value outside that range is an error. The tools send their Fidelity in pixels divided by the zoom, kept within the range), fit?: bool (true; false: the points are joined by straight segments, as the Pencil previews a stroke with Live curve fitting off), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: a new path leaves nothing selected, and a continued or redrawn one is deselected while the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, connect?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end; with connect, the stroke ends on that end of another path's last open subpath and joins it on, in one path with the extended path's appearance (the other path is removed when nothing of it is left); redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it", + "{points: [[x,y]…] or [[x,y,pressure]…] or [[x,y,pressure,tiltX,tiltY,rotation]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic and Scatter brushes with Pressure variation; a point without one presses fully. Pen tilt in degrees, each −90..90, x positive leaning right and y leaning down the page, and barrel rotation in degrees clockwise: the stroke keeps how far the pen leaned, which way and how far its barrel turned, for brushes with Tilt, Bearing and Rotation variation; null for what a point lacks), fidelity?: pt (1.5; from 0.000015625 (0.01 / 640) to 100 / 0.0313 (about 3194.89), the Pencil's and the Paintbrush's 0.01 to 100 px Fidelity at the highest and the lowest zoom; a value outside that range is an error. The tools send their Fidelity in pixels divided by the zoom, kept within the range), fit?: bool (true; false: the points are joined by straight segments, as the Pencil previews a stroke with Live curve fitting off), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: a new path leaves nothing selected, and a continued or redrawn one is deselected while the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, connect?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end, and closes it when `closed` is true (left out, when the stroke ends within four Fidelities of the path's other end, 6 to 400 pt); with connect, the stroke ends on that end of another path's last open subpath and joins it on, in one path with the extended path's appearance (the other path is removed when nothing of it is left); redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it", has_doc, freehand ), diff --git a/crates/engine/src/cmd/draw2/freehand.rs b/crates/engine/src/cmd/draw2/freehand.rs index 31e184e195..1ec591f2ff 100644 --- a/crates/engine/src/cmd/draw2/freehand.rs +++ b/crates/engine/src/cmd/draw2/freehand.rs @@ -204,11 +204,18 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result { } None => None, }; - // An extending stroke that ends within four Fidelities of the path's other end closes it: - // at least 6 pt and at most 400 pt, whatever the Fidelity it is fitted at. + // `closed`, when given, says whether the stroke closes the path: the Pencil and the + // Paintbrush send it, by their Close Paths When Ends Are Within and Alt at the release. + // Left out, an extending stroke that ends within four Fidelities of the path's other end + // closes it: at least 6 pt and at most 400 pt, whatever the Fidelity it is fitted at. let close_tol = (tol * 4.0).clamp(6.0, 400.0); - let closing = connect.is_none() && sp.anchors.len() >= 2 && pts.last().is_some_and(|p| p.distance(first) <= close_tol); - if closing && let Some(l) = pts.last_mut() { + let near_first = pts.last().is_some_and(|p| p.distance(first) <= close_tol); + let wants_close = if p.get("closed").is_some_and(|v| !v.is_null()) { closed } else { near_first }; + let closing = connect.is_none() && sp.anchors.len() >= 2 && wants_close; + // A stroke ending near the other end ends on it; one ending farther away closes with a + // straight segment back to it. + let ends_on_first = closing && near_first; + if ends_on_first && let Some(l) = pts.last_mut() { *l = first; } if let Some((_, _, y, _)) = &connect @@ -225,13 +232,15 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result { add.remove(0); } sp.anchors.extend(add); - if closing + if ends_on_first && sp.anchors.len() > 2 && let Some(l) = sp.anchors.pop() && let Some(a0) = sp.anchors.first_mut() { a0.h_in = l.h_in; sp.closed = true; + } else if closing && !ends_on_first && sp.anchors.len() > 2 { + sp.closed = true; } for a in &mut sp.anchors { *a = Anchor::with_handles(a.p, a.h_in, a.h_out); diff --git a/crates/engine/src/tests/draw2.rs b/crates/engine/src/tests/draw2.rs index f29df7ff23..89393c0f4c 100644 --- a/crates/engine/src/tests/draw2.rs +++ b/crates/engine/src/tests/draw2.rs @@ -4852,6 +4852,31 @@ fn pencil_extend_closes_within_400_pt_at_any_fidelity() { } } +/// `closed`, when given, decides whether a stroke continuing an open path closes it (the tools +/// send it by Close Paths When Ends Are Within): false leaves it open even ending on the other end; +/// true closes one ending 50 pt away with a straight segment back, the stroke's end kept, and one +/// ending 2 pt away on the other end itself. +#[test] +fn pencil_extend_closes_as_closed_says() { + let mut s = session(); + for (end, closed, ends_on_start) in [([0i32, 0], false, false), ([0, 50], true, false), ([0, 2], true, true)] { + let l = line(&mut s, 0.0, 0.0, 200.0, 0.0); + let points = json!([[200, 0], [200, 100], [100, 150], end]); + let p = json!({"points": points, "fidelity": 1.5, "closed": closed, "extend": {"id": l.0}, "keepSelected": false}); + s.execute("path.freehand", &p).unwrap(); + let pd = path(&s, l); + let sp = &pd.subpaths[0]; + assert_eq!(sp.closed, closed, "ending at {end:?}"); + if closed { + let stroke_end = Point::new(f64::from(end[0]), f64::from(end[1])); + let kept = sp.anchors.iter().any(|a| a.p.distance(stroke_end) < 1e-6); + assert_eq!(kept, !ends_on_start, "ending at {end:?}: {:?}", sp.anchors); + let on_start = sp.anchors.iter().filter(|a| a.p.distance(Point::new(0.0, 0.0)) < 1e-6).count(); + assert_eq!((sp.anchors[0].p, on_start), (Point::new(0.0, 0.0), 1), "ending at {end:?}"); + } + } +} + /// At Fidelity 0.01 short, one-point and repeated-point strokes still draw or are refused, never /// crash, and what they draw has finite anchors. #[test] diff --git a/crates/tools/src/cursor.rs b/crates/tools/src/cursor.rs index 584149da8f..85b9cf258e 100644 --- a/crates/tools/src/cursor.rs +++ b/crates/tools/src/cursor.rs @@ -110,14 +110,16 @@ pub enum Cursor { /// The Pencil tool with Alt pressed after the drag starts and Alt key toggles to Smooth Tool off: /// releasing the mouse button with Alt still held closes the path. A drag started with Shift or /// Alt held draws an open straight line instead. Also while a freehand stroke is drawn back near - /// its start, where releasing the mouse button closes the path. A crosshair with a ring. + /// its start, or a stroke continuing a selected path near that path's other end, where + /// releasing the mouse button closes the path. A crosshair with a ring. PencilClose, /// The Pencil tool before a press that draws a straight line (Shift or Alt held). A crosshair with a - /// short straight line. + /// short level line. PencilStraight, - /// The Pencil tool before a press that draws a straight line (Shift or Alt held), over an end of - /// a selected open path: the line continues the path from that end. A crosshair with a short - /// straight line leaving a square end point. + /// The Pencil tool before a press over an end of a selected open path: the stroke, freehand or a + /// straight line, continues the path from that end. Also while a continuing stroke ends near an + /// open end of another path selected at the press, where releasing the mouse button connects the + /// two. A crosshair with a short line rising to the right. PencilContinue, /// The Paintbrush tool. Paintbrush, diff --git a/crates/tools/src/cursors.toml b/crates/tools/src/cursors.toml index 6f5b32e56a..1c15b37d71 100644 --- a/crates/tools/src/cursors.toml +++ b/crates/tools/src/cursors.toml @@ -638,6 +638,7 @@ group = "Drawing and editing tools" shown_when = [ "The Pencil tool with Alt pressed after the drag starts, when Alt key toggles to Smooth Tool is off: releasing the mouse button with Alt still held closes the path. A drag started with Shift or Alt held draws an open straight line and does not show this cursor.", "The Pencil tool while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", + "The Pencil tool while a stroke continuing a selected open path, freehand or a straight line, is drawn to within Close Paths When Ends Are Within of that path's other end: releasing the mouse button there closes the path.", ] hotspot = "The lead tip." look = "The Pencil cursor with a ring badge (closing the loop), like PenClose." @@ -647,14 +648,17 @@ id = "PencilStraight" group = "Drawing and editing tools" shown_when = ["The Pencil tool with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line, at 45° steps from the Constrain Angle when Shift is held and at any angle with Alt alone."] hotspot = "The lead tip." -look = "The Pencil cursor with a short straight-line badge, where PencilClose has its ring." +look = "The Pencil cursor with a short level-line badge, where PencilClose has its ring." [[cursor]] id = "PencilContinue" group = "Drawing and editing tools" -shown_when = ["The Pencil tool over an end of a selected open path, within Edit Selected Paths, with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line that continues the path from that end."] +shown_when = [ + "The Pencil tool over an end of a selected open path, within Edit Selected Paths, before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press continues the path from that end, freehand, or with a straight line when Shift or Alt is held.", + "The Pencil tool while a stroke continuing a selected path ends within Edit Selected Paths of an open end of another path selected at the press: releasing the mouse button connects the two.", +] hotspot = "The lead tip." -look = "PencilStraight's badge with a small filled square at the line's lower end (the end point the line continues from)." +look = "The Pencil cursor with a short badge line rising to the right, where PencilClose has its ring (the stroke carries on from the path)." [[cursor]] id = "Paintbrush" diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index 2de44f97ba..67da5e3499 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -97,6 +97,8 @@ pub struct GestureTool { near_start: bool, /// Pencil/Paintbrush continuing a selected open path: (path, continue from its start). extend: Option<(NodeId, bool)>, + /// The other end of the path `extend` continues: a release near it closes the path. + close_to: Option, /// A Pencil stroke continuing a selected open path: the other paths selected at the press, /// whose open ends it connects to. connect_to: Vec, @@ -796,6 +798,7 @@ impl GestureTool { began: false, near_start: false, extend: None, + close_to: None, connect_to: vec![], redraw: None, fidelity, @@ -1066,6 +1069,21 @@ impl GestureTool { self.find_end(cx, p, &cx.selection.objects) } + /// The other end of the open end `find_end` found: the opposite end of `id`'s last open + /// subpath. + fn other_end(cx: &ToolContext, id: NodeId, at_start: bool) -> Option { + let pd = cx.doc.node(id)?.path_data()?; + let sp = pd.subpaths.iter().rev().find(|s| !s.closed && !s.anchors.is_empty())?; + let a = if at_start { sp.anchors.last() } else { sp.anchors.first() }; + a.map(|a| a.p) + } + + /// Continues the path at the open end `find_extend` found, if any, noting its other end. + fn set_extend(&mut self, cx: &ToolContext, found: Option<(NodeId, bool, Point)>) { + self.extend = found.map(|(id, at_start, _)| (id, at_start)); + self.close_to = found.and_then(|(id, at_start, _)| Self::other_end(cx, id, at_start)); + } + /// [`Self::find_extend`] among `ids`. fn find_end(&self, cx: &ToolContext, p: Point, ids: &[NodeId]) -> Option<(NodeId, bool, Point)> { if self.edit_within <= 0.0 { @@ -1110,16 +1128,32 @@ impl GestureTool { } /// Does a stroke that ends at `end` end near its start (Close Paths When Ends Are Within, off - /// at 0 pixels)? + /// at 0 pixels)? A stroke continuing a selected path ends near that path's other end, a + /// straight line too; a new straight line never closes, as it would run back over itself. fn ends_near_start(&self, cx: &ToolContext, end: Point) -> bool { - self.close_within > 0.0 && self.points.len() > 2 && self.points[0].distance(end) <= cx.tol(self.close_within) + if self.close_within <= 0.0 { + return false; + } + let tol = cx.tol(self.close_within); + match (self.extend, self.close_to) { + (Some(_), Some(to)) => self.points.len() >= 2 && to.distance(end) <= tol, + (Some(_), None) => false, + (None, _) => !self.straight && self.points.len() > 2 && self.points.first().is_some_and(|s| s.distance(end) <= tol), + } } - /// Does a release now, with the stroke ending at `end`, close a freehand stroke because it ends - /// near its start (`finish`)? A stroke that redraws a selected path closes only when the engine - /// finds it does not end on that path, so it counts as not closing. + /// The open end of another path selected at the press that a continuing Pencil stroke ending + /// at `end` connects to (`finish`). + fn connects_at(&self, cx: &ToolContext, end: Point) -> Option<(NodeId, bool, Point)> { + self.extend.and_then(|_| self.find_end(cx, end, &self.connect_to)) + } + + /// Does a release now, with the stroke ending at `end`, close the stroke because it ends near + /// its start, or near the other end of the path it continues (`finish`)? A stroke that redraws + /// a selected path closes only when the engine finds it does not end on that path, so it counts + /// as not closing, and a stroke that connects to another path doesn't close. fn release_closes_near_start(&self, cx: &ToolContext, end: Point) -> bool { - self.draws_path() && !self.smoothing && !self.straight && self.extend.is_none() && self.redraw.is_none() && self.ends_near_start(cx, end) + self.draws_path() && !self.smoothing && self.redraw.is_none() && self.ends_near_start(cx, end) && self.connects_at(cx, end).is_none() } /// Parameters of `path.freehand` for the current stroke, fitted at Fidelity on screen. Only the @@ -1160,14 +1194,16 @@ impl GestureTool { if !self.began { return vec![]; } - // Close Paths When Ends Are Within is off at 0 pixels. + // Close Paths When Ends Are Within is off at 0 pixels. A stroke continuing a + // selected path closes it near its other end, a straight line too. let near = pts.last().is_some_and(|&end| self.ends_near_start(cx, end)); - // A straight line has two points: closing it would draw it back over itself. - let closed = !self.straight && ((m.alt && (self.id == "paintbrush" || !self.alt_smooth)) || (self.extend.is_none() && near)); - let mut params = self.freehand_params(cx, closed); + // Alt at the release closes a freehand stroke; a new straight line has two points: + // closing it would draw it back over itself. + let closed = (!self.straight && m.alt && (self.id == "paintbrush" || !self.alt_smooth)) || near; // A Pencil stroke that continues a selected path and ends on an open end of another - // path selected at the press connects the two. - let connect = pts.last().and_then(|&end| self.find_end(cx, end, &self.connect_to)); + // path selected at the press connects the two, and doesn't close. + let connect = pts.last().and_then(|&end| self.connects_at(cx, end)); + let mut params = self.freehand_params(cx, closed && connect.is_none()); if let Some((other, at_start, _)) = connect { params["connect"] = json!({"id": other.0, "end": if at_start { "start" } else { "end" }}); } @@ -1241,7 +1277,7 @@ impl Tool for GestureTool { self.straight = (self.id == "pencil" && !self.smoothing && (ev.mods.shift || ev.mods.alt)) || (self.id == "knife" && ev.mods.alt); self.unconstrained = self.straight && !ev.mods.shift; let extend = if self.draws_path() && !self.smoothing { self.find_extend(cx, p) } else { None }; - self.extend = extend.map(|(id, at_start, _)| (id, at_start)); + self.set_extend(cx, extend); // The stroke's previews select the path it continues, so the others are kept now. self.connect_to = match self.extend { Some((id, _)) if self.id == "pencil" => cx.selection.objects.iter().copied().filter(|o| *o != id).collect(), @@ -1299,6 +1335,8 @@ impl Tool for GestureTool { return if std::mem::take(&mut self.began) { vec![Action::Cancel] } else { vec![] }; } self.points.truncate(1); + // The line ends at `p`, where its end snaps. + end = p; } else if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) || self.points.len() >= crate::MAX_STROKE_SAMPLES { // A release here keeps the last point unless it snaps (`Up`), so the ring // follows the point `finish` will test. @@ -1374,6 +1412,7 @@ impl Tool for GestureTool { self.unconstrained = false; self.clear(); self.extend = None; + self.close_to = None; self.connect_to.clear(); self.redraw = None; out @@ -1517,6 +1556,11 @@ impl Tool for GestureTool { if !self.active && self.straight_at_press(m) { return if self.find_extend(cx, p).is_some() { Cursor::PencilContinue } else { Cursor::PencilStraight }; } + // A continuing Pencil stroke that a release would connect to another selected path's open + // end (`finish`) shows that it continues on. + if self.id == "pencil" && self.active && self.points.last().is_some_and(|&end| self.connects_at(cx, end).is_some()) { + return Cursor::PencilContinue; + } // Alt held at the release closes the path (`finish`). // A straight line stays open, so its release has nothing to close, and before a press Alt // gives the Pencil a straight line. @@ -1536,9 +1580,12 @@ impl Tool for GestureTool { "smooth" => Cursor::Smooth, "pathEraser" => Cursor::PathEraser, "join" => Cursor::Join, + // Before a press over an open end of a selected path the Pencil shows that the stroke + // continues it (`PointerKind::Down`). + _ if !self.active && self.find_extend(cx, p).is_some() => Cursor::PencilContinue, // Before a press that draws a new path the Pencil shows its star; over a selected path - // that the press continues from an end or redraws (`PointerKind::Down`), it does not. - _ if !self.active && self.find_extend(cx, p).is_none() && self.find_redraw(cx, p).is_none() => Cursor::PencilNew, + // that the press redraws (`PointerKind::Down`), it does not. + _ if !self.active && self.find_redraw(cx, p).is_none() => Cursor::PencilNew, _ => Cursor::Pencil, } } diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 624adc2e98..65ad5f00e7 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -785,7 +785,7 @@ fn pencil_shows_the_star_before_a_press_that_draws_a_new_path() { } } let mut t = GestureTool::new("pencil"); - assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::Pencil, "over an end the stroke continues"); + assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::PencilContinue, "over an end the stroke continues"); assert_eq!(t.cursor(&cx, middle, Mods::default()), Cursor::Pencil, "over the path the stroke redraws"); assert_eq!(t.cursor(&cx, away, Mods::default()), Cursor::PencilNew, "away from the path"); // While a stroke is drawn, the Pencil's own cursor. @@ -830,7 +830,7 @@ fn pencil_shift_line_continues_a_selected_path_from_its_end() { } /// Before a press over an end of a selected open path, the Pencil shows its continuation cursor -/// exactly when the press would draw a straight line that continues the path; away from the +/// exactly when the press continues the path, freehand or with a straight line; away from the /// ends, with nothing selected, with Edit Selected Paths off, once the press is down and with /// the Paintbrush, it does not. #[test] @@ -851,12 +851,9 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { let shift = Mods { shift: true, ..Default::default() }; let alt = Mods { alt: true, ..Default::default() }; let shift_alt = Mods { shift: true, alt: true, ..Default::default() }; - // The release's stroke: a straight line that continues the path. - let continues = |acts: &[Vec]| { - acts.last() - .and_then(|a| a.first()) - .is_some_and(|a| matches!(a, Action::Preview(_, v) if v.get("extend").is_some() && v["points"].as_array().is_some_and(|q| q.len() == 2))) - }; + // The release's stroke continues the path. + let continues = + |acts: &[Vec]| acts.last().and_then(|a| a.first()).is_some_and(|a| matches!(a, Action::Preview(_, v) if v.get("extend").is_some())); for alt_smooth in [false, true] { for m in [Mods::default(), shift, alt, shift_alt] { for (at, other_end) in [(Point::new(405.0, 303.0), (500.0, 296.0)), (Point::new(296.0, 302.0), (200.0, 310.0))] { @@ -872,7 +869,7 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { let end = Point::new(405.0, 303.0); assert_eq!(t.cursor(&cx, end, shift), Cursor::PencilContinue); assert_eq!(t.cursor(&cx, end, alt), Cursor::PencilContinue, "Alt alone continues at any angle"); - assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::Pencil, "without Shift or Alt the stroke is freehand"); + assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::PencilContinue, "a freehand stroke continues it too"); assert_eq!(t.cursor(&cx, Point::new(350.0, 360.0), shift), Cursor::PencilStraight, "away from the ends"); // While the line is drawn, the Pencil's own cursor. t.pointer(&cx, &PointerEvent::new(PointerKind::Down, end.x, end.y).with_mods(shift)); @@ -889,6 +886,71 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { assert_eq!(Cursor::PencilContinue.precise(), Cursor::Crosshair, "Use Precise Cursors"); } +/// A Pencil stroke continuing a selected path shows the continuation cursor while a release would +/// connect it to an open end of another path selected at the press, and the closing ring while a +/// release would close the path because the stroke ends within Close Paths When Ends Are Within +/// of the path's other end, a straight line too; the release does what the cursor showed. +#[test] +fn a_continued_stroke_shows_where_it_connects_or_closes() { + let (mut d, _) = doc_with_rect(); + let l = d.layers[0].id; + let add = |d: &mut vectorcraft_doc::Document, pts: &[(f64, f64)]| { + let id = d.alloc_id(); + let mut els = vec![vectorcraft_geom::PathEl::MoveTo(Point::new(pts[0].0, pts[0].1))]; + els.extend(pts[1..].iter().map(|&(x, y)| vectorcraft_geom::PathEl::LineTo(Point::new(x, y)))); + let pd = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(els)); + d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, pd, vectorcraft_doc::Appearance::default_art())).unwrap(); + id + }; + let a = add(&mut d, &[(300.0, 300.0), (400.0, 300.0), (400.0, 400.0)]); + let b = add(&mut d, &[(500.0, 500.0), (600.0, 500.0)]); + let p = paint(); + let mut both = Selection::default(); + both.set([a, b]); + let mut cx = cx(&d, &both, &p); + cx.smart_guides = false; + cx.snap_to_point = false; + let none = Mods::default(); + let shift = Mods { shift: true, ..Default::default() }; + // Runs a stroke from `pts[0]`, giving the cursor at its last point before the release and the + // release's `path.freehand` parameters. + let run = |t: &mut GestureTool, pts: &[(f64, f64)], m: Mods| { + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, pts[0].0, pts[0].1).with_mods(m)); + for &(x, y) in &pts[1..] { + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x, y).with_mods(m)); + } + let (x, y) = pts[pts.len() - 1]; + let shown = t.cursor(&cx, Point::new(x, y), m); + let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, x, y).with_mods(m)); + let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") }; + (shown, v.clone()) + }; + let mut t = GestureTool::new("pencil"); + // To the other selected path's start: it connects, and doesn't close. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (450.0, 450.0), (498.0, 502.0)], none); + assert_eq!(shown, Cursor::PencilContinue, "near the other path's end"); + assert_eq!( + (v["extend"].clone(), v["connect"].clone(), v["closed"].clone()), + (json!({"id": a.0, "end": "end"}), json!({"id": b.0, "end": "start"}), json!(false)) + ); + // Back near the path's own start (10 px from it): the ring, and the release closes it. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 420.0), (290.0, 380.0), (292.0, 306.0)], none); + assert_eq!(shown, Cursor::PencilClose, "near the path's other end"); + assert_eq!((v["extend"].clone(), v["closed"].clone()), (json!({"id": a.0, "end": "end"}), json!(true))); + assert!(v.get("connect").is_none(), "{v}"); + // Farther than Close Paths When Ends Are Within (15 px): no ring, and it stays open. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 420.0), (290.0, 380.0), (290.0, 330.0)], none); + assert_eq!((shown, v["closed"].clone()), (Cursor::Pencil, json!(false)), "away from the other end"); + // A straight line from the end to near the start: the ring, and it closes the path. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 350.0), (306.0, 306.0)], shift); + assert_eq!(shown, Cursor::PencilClose, "a straight line near the other end"); + assert_eq!((v["points"].as_array().map(Vec::len), v["closed"].clone()), (Some(2), json!(true))); + // Close Paths When Ends Are Within off: neither ring nor close. + t.set_option("closeWithin", &json!(0.0)); + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 420.0), (290.0, 380.0), (292.0, 306.0)], none); + assert_eq!((shown, v["closed"].clone()), (Cursor::Pencil, json!(false)), "at 0 px"); +} + /// #3846: Alt during a Shift line doesn't promise a close the release can't do, and with Alt /// Toggles to Smooth, Alt pressed after the press doesn't turn the line into a smoothing drag. #[test] diff --git a/crates/ui-egui/src/cursors.rs b/crates/ui-egui/src/cursors.rs index a438a71881..5495d3753c 100644 --- a/crates/ui-egui/src/cursors.rs +++ b/crates/ui-egui/src/cursors.rs @@ -677,18 +677,16 @@ fn glyph(c: Cursor, p: Pos2) -> Option> { ink.circle_stroke(p + vec2(8.0, 8.0), 3.0, Stroke::new(3.0, HALO)); ink.circle_stroke(p + vec2(8.0, 8.0), 3.0, Stroke::new(1.2, INK)); } - // The Pencil's crosshair with a short straight line where the closing ring goes. + // The Pencil's crosshair with a short level line where the closing ring goes. Cursor::PencilStraight => { crosshair(ink, p); - line(ink, p + vec2(5.0, 12.0), p + vec2(12.0, 5.0)); + line(ink, p + vec2(3.55, 8.5), p + vec2(13.45, 8.5)); } - // The straight-line cursor's line leaving a filled square: the end point a line continues from. + // The Pencil's crosshair with a short line rising to the right where the closing ring goes: + // the stroke carries on from the path. Cursor::PencilContinue => { crosshair(ink, p); - // The square first, so its halo does not cover the line. - let end = p + vec2(5.0, 12.0); - poly(ink, [(-2.0, -2.0), (2.0, -2.0), (2.0, 2.0), (-2.0, 2.0)].map(|(x, y)| end + vec2(x, y)).to_vec(), INK, INK); - line(ink, end, p + vec2(12.0, 5.0)); + line(ink, p + vec2(5.0, 12.0), p + vec2(12.0, 5.0)); } Cursor::Eyedropper => eyedropper(ink, p), Cursor::ResizeH => double_arrow(ink, p, vec2(1.0, 0.0)), diff --git a/crates/ui-egui/src/cursors/tests.rs b/crates/ui-egui/src/cursors/tests.rs index ae1947616f..49c932175d 100644 --- a/crates/ui-egui/src/cursors/tests.rs +++ b/crates/ui-egui/src/cursors/tests.rs @@ -244,7 +244,7 @@ fn the_pencil_new_path_cursor_is_the_crosshair_with_a_star() { } } -/// The Pencil's straight-line cursor is the crosshair with a short straight line below right, +/// The Pencil's straight-line cursor is the crosshair with a short level line below right, /// through the point where the closing cursor's ring is hollow. #[test] fn the_pencil_straight_line_cursor_is_the_crosshair_with_a_line() { @@ -264,15 +264,18 @@ fn the_pencil_straight_line_cursor_is_the_crosshair_with_a_line() { assert_eq!(at(&cross, 8.5, 8.5)[3], 0, "the crosshair has no badge at {ppp}"); // Along the line, not across it: no ink off its ends. assert!(!inked(at(&img, 5.0, 5.0)) && !inked(at(&img, 12.0, 12.0)), "a line, not a cross, at {ppp}"); + // Level: inked at both ends of it, and not where the continuation cursor's line rises. + assert!(inked(at(&img, 4.5, 8.5)) && inked(at(&img, 12.5, 8.5)), "a level line at {ppp}"); + assert!(!inked(at(&img, 6.0, 11.0)) && !inked(at(&img, 11.0, 6.0)), "not a rising one at {ppp}"); assert_ne!(img, ring, "at {ppp}"); assert_eq!(at(&img, -6.0, 0.0), at(&cross, -6.0, 0.0), "the arms are the crosshair's at {ppp}"); } } -/// The Pencil's continuation cursor is the straight-line cursor with a filled square on the -/// line's lower end, beside the line where the straight-line cursor has no ink. +/// The Pencil's continuation cursor is the crosshair with a short line rising to the right below +/// right, where the straight-line cursor's line is level. #[test] -fn the_pencil_continuation_cursor_is_the_straight_line_cursor_with_an_end_point() { +fn the_pencil_continuation_cursor_is_the_crosshair_with_a_rising_line() { for ppp in [1.0, 2.0] { let cross = rasterize(Cursor::Crosshair, ppp).unwrap(); let img = rasterize(Cursor::PencilContinue, ppp).unwrap(); @@ -285,8 +288,10 @@ fn the_pencil_continuation_cursor_is_the_straight_line_cursor_with_an_end_point( }; let inked = |p: [u8; 4]| p[3] > 200 && p[0] < 64; assert!(inked(at(&img, 8.5, 8.5)), "the line crosses the badge's middle at {ppp}"); - assert!(inked(at(&img, 3.5, 13.5)), "the end point's square at {ppp}"); - assert!(!inked(at(&straight, 3.5, 13.5)), "the straight-line cursor has no end point at {ppp}"); + assert!(inked(at(&img, 5.0, 11.0)) && inked(at(&img, 10.0, 6.0)), "the line rises to the right at {ppp}"); + assert!(!inked(at(&straight, 6.0, 11.0)), "the straight-line cursor's line is level at {ppp}"); + assert!(!inked(at(&img, 4.5, 8.5)) && !inked(at(&img, 12.5, 8.5)), "not a level one at {ppp}"); + assert!(!inked(at(&img, 3.5, 13.5)), "no end point square at {ppp}"); assert!(!inked(at(&img, 12.0, 12.0)), "a line, not a cross, at {ppp}"); assert_eq!(at(&img, -6.0, 0.0), at(&cross, -6.0, 0.0), "the arms are the crosshair's at {ppp}"); } diff --git a/docs/mcp.md b/docs/mcp.md index cb5c40501c..e23a46b0b9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,6 +1,6 @@ # VectorCraft MCP server -> **Last updated:** 2026-11-13 · **Change:** Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097); existing response shapes retained. Previously: Save location-refusal error (#6146); existing response shapes retained. +> **Last updated:** 2026-11-13 · **Change:** Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; no response-shape change. Previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097); existing response shapes retained. `vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio (newline-delimited JSON-RPC 2.0; see [Protocol](#protocol) for version negotiation). Agents use it to @@ -3058,8 +3058,12 @@ already walks Image Trace. Short agent entry points: is rejected with an error. The Pencil and the Paintbrush give Fidelity in screen pixels, 0.01 to 100 px, and send it divided by the zoom (0.0313 to 640) and kept within that range, so a stroke is fitted at the same tolerance on screen at every zoom. A stroke - that continues a path (`extend`) closes it when it ends within four times the Fidelity of the path's other end, at - least 6 pt and at most 400 pt. The Blob Brush's `path.blob` Fidelity stays in points, 0.01 to 100 pt. + that continues a path (`extend`) closes it when `closed` is true: the Pencil and the Paintbrush send it when the + stroke ends within Close Paths When Ends Are Within of the path's other end (the cursor shows the closing ring there), + a straight Pencil line too, or with Alt held at the release. The stroke's end then joins the other end when it is + within four times the Fidelity of it (at least 6 pt and at most 400 pt), and a straight segment closes the path from + farther away. Without `closed`, the stroke closes the path when it ends within that four-times-Fidelity distance. The + Blob Brush's `path.blob` Fidelity stays in points, 0.01 to 100 pt. - Variables: `variable.list`, `variable.define {name, kind: text|visibility|image|graph}`, `variable.bind {variable, ids?}`, `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.makeGraphDynamic` (`variable.bind` and `variable.makeTextDynamic` refuse type that already has another text variable and name it), @@ -11327,5 +11331,6 @@ the error text. The interactive fallback applies only to ordinary File > Save, n | Date | Change | |---|---| +| 2026-11-13 | `path.freehand`: `closed`, when given, decides whether a stroke that continues a path (`extend`) closes it; the Pencil and the Paintbrush send it by Close Paths When Ends Are Within. | | 2026-11-13 | Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097). | | 2026-11-11 | Document READ_ONLY_LOCATION for saves (#6146); no response-shape change. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e309f72409..f195cab4b2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (`.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Pencil continuation, straight-line and closing cursors; scores and estimates unchanged). > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). > @@ -53,6 +53,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release (`a_continued_stroke_shows_where_it_connects_or_closes` in `crates/tools/src/draw2/gesture/tests.rs`, `pencil_extend_closes_as_closed_says` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Graphic Styles: in a panel made taller the list fills the space above the footer instead of stopping at 220 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/graphic_styles.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke instead of being left out, adding no text (#6971; `empty_actual_text_keeps_the_glyphs_art` in `crates/pdf/src/tests/actual_text.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Library panels (swatch, Color Book, brush, symbol and graphic style libraries): in a panel made taller the list fills the space above the footer instead of scrolling inside 220 or 280 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/library_panel/tests.rs`). Scores and estimates unchanged. | @@ -1044,7 +1045,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur With the Blob Brush, Alt held before the press, or pressed before the drag has moved, switches to the Smooth tool, as 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: the cursor shows the Smooth tool, the drag shows a thin line and, with a path selected that it can smooth, the dashed band it smooths, without the length label, Shift does not constrain it, and the release smooths the selected paths under the drag (`path.smoothRegion`) instead of painting; with nothing selected the release changes no path. A stroke that has moved keeps painting when Alt goes down. Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The curve through the points (`curvature_spline` in `crates/tools/src/draw2.rs`) makes each stretch of three points follow the circle through them: a smooth point's handles lie along that circle's tangent, each `chord / (3 cos²(α/2))` long (α between handle and chord, capped at 90°), and the ends of an open path mirror their neighbour's tangent in their chord, so a J or a U curves evenly, a turn tighter than 30° comes to a point, and uneven spacing doesn't loop; the rubber band, a dragged or inserted point and `path.convertAnchor` to smooth use the same rule, while the Pencil's fitting keeps its own; `tests/draw2.rs` checks that the rubber band is the path the click makes (#1236, in part). The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512). A path the Smooth tool changes stays selected whole, and the anchor points and segments selected on it with Direct Selection are deselected. Selected paths the drag does not change keep their selected anchors, and undo restores the earlier selection (`tests/draw2.rs`, `smoothing_a_path_drops_its_direct_selected_anchors`). While the Smooth tool drags with a path selected that it can smooth, a dashed outline shows the band it smooths: everything within its radius of the drag (12 screen pixels, VectorCraft's own value), round at both ends; Alt smoothing with the Pencil, Paintbrush and Blob Brush shows the same band. While Alt is held during a Paintbrush stroke or before a press with no path selected that the Smooth tool can act on, the Paintbrush shows a closing cursor (a crosshair with a ring), and so does the Pencil with Alt key toggles to Smooth Tool off when Alt is pressed after the drag starts; releasing the mouse button with Alt still held closes the path. Before a Paintbrush press with such a path selected, Alt shows the Smooth cursor, and holding Alt at the press smooths that path with the Smooth tool's radius and Smooth Tool Options' Fidelity. A Pencil drag started with Shift or Alt held draws a straight line instead, which stays open. - With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short line), and over an end of a selected open path, where the line continues the path, a path-continuation cursor (the same line leaving a small square), as the page describes. + With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short level line), as the page describes. Over an end of a selected open path, where the stroke continues the path, freehand or as a straight line, it shows a path-continuation cursor (a crosshair with a short line rising to the right), as [Extend paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/extend-paths-with-the-pencil-tool.html) describes, and again while a continuing stroke ends near an open end of another selected path, where the release connects the two ([Connect two paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/connect-two-paths-with-the-pencil-tool.html)). A continuing stroke, a straight line too, that ends within Close Paths When Ends Are Within of the path's other end shows the closing cursor, and the release closes the path; Alt held at the release closes it too. - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. A shape just drawn stays adjustable with its tool: its Live Shape widgets drag, a press on its centre point (Show Center) moves the selection with the Selection tool's move arrow, snapping and Alt copy, and a press in the rotate zone just outside a bounding-box corner turns it about the centre with the Selection tool's rotate cursor (#5477; tests `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` and, through the engine, `shapetools.rs`). A drag on one of its eight bounding-box handles scales it as the Selection tool's handles do, with their cursors and snapping, instead of drawing a new shape from the handle. A shape tool takes a press as a handle's only on the drawn handle square (VectorCraft's own rule: 3 screen points from its centre along each axis at the default Anchor Size, a half point more or less per step; the Selection tool takes 5), so a press just beside a handle still draws (tests `a_shape_tool_scales_the_selection_by_its_bounding_box_handles`, `a_shape_tool_draws_from_a_press_just_beside_a_handle` and `a_shape_tool_scales_the_shape_just_drawn_by_its_bounding_box_handle`). The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays by 0.1 radian at each drag update in which the pointer's document y grows, and back by as much at each one in which it falls, Shift held keeps their turn, and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays with the pointer, Shift keeps them in their original orientation (unturned, or turned by a nonzero General › Constrain Angle), and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 974fbb440e..a7adedb22d 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Remove Background in the Contextual Task Bar, Properties › Quick Actions and the Control bar; previously minor: Ruler origin: the corner drag snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Outline view: images and gradient meshes are picked on what Outline draws, so a click inside them reaches the path behind, #6437; previously minor: a middle-click on a document tab closes it, #6506; previously minor: Blob Brush: a round tip's preview is the outline of the area it adds, round at both ends, as the stroke it commits (#4343); scores and estimates unchanged; previously:; ruler double-click adds a guide at the pointer, #5239; G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Remove Background in the Contextual Task Bar, Properties › Quick Actions and the Control bar; previously minor: Ruler origin: the corner drag snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Outline view: images and gradient meshes are picked on what Outline draws, so a click inside them reaches the path behind, #6437; previously minor: a middle-click on a document tab closes it, #6506; previously minor: Blob Brush: a round tip's preview is the outline of the area it adds, round at both ends, as the stroke it commits (#4343); scores and estimates unchanged; previously:; ruler double-click adds a guide at the pointer, #5239; G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -51,6 +51,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release. Scores and estimates unchanged. | | 2026-11-13 | minor | Graphic Styles: in a panel made taller the list fills the space above the footer instead of stopping at 220 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/graphic_styles.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Library panels (swatch, Color Book, brush, symbol and graphic style libraries): in a panel made taller the list fills the space above the footer instead of scrolling inside 220 or 280 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/library_panel/tests.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Swatches: in a panel made taller the swatch list fills the space above the footer instead of scrolling inside 150 or 200 pt (#1638). Scores unchanged. | @@ -177,7 +178,7 @@ tools, never checked side by side) isn't captured row by row. | Blend spine Anchor Point conversion and handle editing (#2544) | implemented | symmetric handles, independent drag/retraction, visible-handle policy and selection/stacking covered by tool and engine regressions; Windows native canvas checks and screenshots reviewed in PR #2643 | | Several blend spine points picked and dragged together (#5285) | done | with Direct Selection, Shift-click picks more points of a selected blend's spine or takes one away, as on a path; dragging any picked point moves them all, and the key objects on them, in one undo step (`object.blend.spine.moveAnchor {with}`); a marquee doesn't pick spine points yet | | Add/Delete/Anchor Point tools; Anchor Point tool drags segments to reshape | done | #2267: a click on a handle end retracts that handle and leaves the anchor a corner with the other one; Shift held at the press on an anchor moves it, handles and all (Shift kept down: at 45° steps) instead of pulling out handles | -| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Fidelity is in screen pixels, the same at any zoom, and goes down to 0.01 px in the dialog and the tool, which sends `path.freehand` the value in points at the current zoom (#5729; the Smooth tool keeps 0.05 pt); its slider runs across the Tool Options row on a logarithmic rail from 0.01 to 20 px, the field shows hundredths, and the slider's and the field's arrow keys step hundredths up to 0.1 px, tenths up to 1 px and whole pixels above (Shift ×10) (#3328); Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor while Shift or Alt is held before the press, and a path-continuation cursor over an end of a selected open path, where the line continues the path; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) on, a stroke selects the path it draws or continues and replaces the earlier selection, and off a new stroke leaves nothing selected, the earlier selection included, while a continued or redrawn path is deselected and the rest of the selection stays; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path; a Pencil stroke from an end of one selected open path to an end of another connects the two into one path | +| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Fidelity is in screen pixels, the same at any zoom, and goes down to 0.01 px in the dialog and the tool, which sends `path.freehand` the value in points at the current zoom (#5729; the Smooth tool keeps 0.05 pt); its slider runs across the Tool Options row on a logarithmic rail from 0.01 to 20 px, the field shows hundredths, and the slider's and the field's arrow keys step hundredths up to 0.1 px, tenths up to 1 px and whole pixels above (Shift ×10) (#3328); Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor (a short level line) while Shift or Alt is held before the press, and a path-continuation cursor (a short rising line) over an end of a selected open path, where the stroke continues the path, freehand or straight, and while a continuing stroke ends near an open end of another selected path it connects to; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) on, a stroke selects the path it draws or continues and replaces the earlier selection, and off a new stroke leaves nothing selected, the earlier selection included, while a continued or redrawn path is deselected and the rest of the selection stays; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, or a stroke continuing a selected path (a straight line too) to within it of that path's other end, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path; a Pencil stroke from an end of one selected open path to an end of another connects the two into one path | | Drop Shadow, Outer Glow and Inner Glow: Opacity, the offsets, Blur and Darkness have spinner arrows that step by one (a unit for distances), repeat when held and stop at the fields' ends (#5367, #5368) | done | dialogs::effect test drop_shadow_and_outer_glow_fields_have_spinner_arrows; other effect dialogs' number fields still have none | | Effect dialogs' colour fields: a swatch beside the hex follows typed values, and a double-click opens the Color Picker over the dialog, whose OK fills it and Cancel comes back unchanged (#5365) | done | dialogs::effect tests outer_glow_colour_swatch_opens_the_color_picker and drop_shadow_colour_swatch_opens_the_color_picker (Drop Shadow's Color, #5364); the picker previews nothing itself, the effect dialog previews the returned colour | | Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533); the Join tool trims two crossing open paths whose overhanging ends it scrubs over and joins them at the crossing, instead of drawing a segment between their ends (#5844) | From 6b487226aaee8f328907355365eae59cd9cc7781 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 14:28:53 -0800 Subject: [PATCH 02/44] i18n: Japanese has a row for every message the untranslated report lists The Japanese catalog lacked a row for 1,842 of the status and error messages that the untranslated report finds in the sources, so the status bar showed those messages in English. - ja.tsv gains 1,842 `@msg` rows: the errors of commands and their parameter checks, scripts, file import and export, printing and plug-ins. The translations are VectorCraft's own, written from the English meaning with the catalog's conventions and its existing feature names. Placeholders, parameter names, command ids and literal values stay as the English writes them. - The untranslated report now lists no Japanese messages. Menu strings, `tl!` literals and undo labels are unchanged. - Two of the messages have two placeholders in a row. The status bar cannot match such a row, so it does not use those two rows. - The row for `{k} has control characters{_1}` does not match the message for a multi-line File Info field, where the last placeholder is empty. - The Japanese row of docs/localization-parity.md gives the catalog's current message counts. Docs: docs/target-app-parity.md, docs/localization-parity.md. --- crates/ui-egui/src/i18n/ja.tsv | 1842 ++++++++++++++++++++++++++++++++ docs/localization-parity.md | 2 +- docs/target-app-parity.md | 1 + 3 files changed, 1844 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 16d873ee32..9b945bd23d 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6299,6 +6299,1848 @@ axis Y Axis Y 軸 @msg no menu item that can be inserted runs this command このコマンドを実行する、挿入できるメニュー項目はありません @msg the action already has 10,000 steps アクションにはすでに 10,000 ステップがあります @msg the action set would be larger than 8 MiB アクションセットが 8 MiB を超えます +@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions strict モードの関数では 'caller'、'callee'、'arguments' を使用できません +@msg Alignment must be one of the choices 揃えには選択肢のいずれかを指定してください +@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean.prototype.{name} が Boolean ではない値に対して呼び出されました +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB サンプルを CMYK で読み取れなかった CMYK 画像は RGB で読み込みました +@msg Cannot write to the location of “{name}”; choose where to save it 「{name}」の場所に書き込めません。保存先を選択してください +@msg DCS files can only be placed linked: use File › Place with Link on DCS ファイルはリンクとしてのみ配置できます: 「リンク」をオンにして「ファイル › 配置」を使用してください +@msg Date.prototype method called on a value that is not a Date object Date.prototype のメソッドが Date オブジェクトではない値に対して呼び出されました +@msg Error.prototype.toString was called on a value that is not an object Error.prototype.toString がオブジェクトではない値に対して呼び出されました +@msg Function.prototype.toString was called on a value that is not a function Function.prototype.toString が関数ではない値に対して呼び出されました +@msg Import SVG Filter reads SVG files, not SVGZ 「SVG フィルターを読み込み」が読み込むのは SVG ファイルで、SVGZ ファイルは読み込みません +@msg JSON.parse: {why} (at offset {at}) JSON.parse: {why} (オフセット {at}) +@msg JSON.stringify cannot write a structure that contains itself JSON.stringify は自身を含む構造を書き出せません +@msg Line Gap must be a number 行間には数値を指定してください +@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 QName.prototype.toString needs a QName QName.prototype.toString には QName が必要です +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp.prototype.{name} が RegExp オブジェクトではない値に対して呼び出されました +@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 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` must be a string `align` には文字列を指定してください +@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` names an artboard the document does not have `artboards` にドキュメントにないアートボードが指定されています +@msg `artboards` names an artboard twice `artboards` に同じアートボードが 2 回指定されています +@msg `at` must be [x, y] (got {at}) `at` には [x, y] を指定してください (指定値: {at}) +@msg `at` must be a point [x, y] `at` には点 [x, y] を指定してください +@msg `beside` needs `side` `beside` には `side` が必要です +@msg `code` must be a whole number `code` には整数を指定してください +@msg `colorMode` must be one of rgb, cmyk `colorMode` には rgb、cmyk のいずれかを指定してください +@msg `colors` does not apply to the {name} reduction `colors` は {name} 減色には適用されません +@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 均等減色では `colors` に 9 以上を指定してください +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors` には 1 から {MAX_THEME_COLORS} までの値を指定してください (指定値: {n}) +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count` には 0 から {MAX_COUNT} までの整数を指定してください +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount` には 0 から 100 までの数値を指定してください +@msg `fidelity` must be from {lo} to {hi} pt `fidelity` には {lo}~{hi} pt の値を指定してください +@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 `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 `id` is an object id, not {v} `id` にはオブジェクト ID を指定してください ({v} は指定できません) +@msg `item` needs `index` `item` には `index` が必要です +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore` は文字スタイルではなく行の先頭文字の設定です (text.setRangeStyle で設定します) +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType` には "pushInFirst"、"pushOutFirst"、"pushOutOnly" のいずれかを指定してください +@msg `leader` must be a string `leader` には文字列を指定してください +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max` には 1~{MAX_SUGGESTIONS} を指定してください +@msg `name` must be a name that is not empty `name` には空でない名前を指定してください +@msg `name` must not be empty `name` を空にすることはできません +@msg `origin` must be [x, y] in points `origin` にはポイント単位の [x, y] を指定してください +@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 `points` must hold from 2 to 100000 points `points` には 2~100000 個の点が必要です +@msg `radius` must be 0 or more `radius` には 0 以上を指定してください +@msg `rect` must be [x, y, width, height] `rect` には [x, y, width, height] を指定してください +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect` には [x0, y0, x1, y1] を指定してください (指定値: {r}) +@msg `replace` must be a string `replace` には文字列を指定してください +@msg `rows` must be lists of numbers or nulls, not {r} `rows` には数値または null のリストを指定してください ({r} は指定できません) +@msg `scale` must be a number from 0.001 to 1000 `scale` には 0.001 から 1000 までの数値を指定してください +@msg `scope.{key}` must be a non-empty array `scope.{key}` には空でない配列を指定してください +@msg `scope` must be "document", {artboards} or {layers} `scope` には "document"、{artboards}、{layers} のいずれかを指定してください +@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 `source` is the only path in series group `target` `source` は系列グループ `target` の唯一のパスです +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` ではボックス全体のラインが {MAX_COUNT} 本を超えます +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing` には 0 より大きく {MAX_DISTANCE} pt 以下の値を指定してください +@msg `target` must be an object of the same Objects on Path group as `member` `target` には `member` と同じパス上のオブジェクトグループのオブジェクトを指定してください +@msg `tolerance` must be from 0 to 100 pt `tolerance` には 0~100 pt の値を指定してください +@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 つずつ、{_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 name a composite font `{_1}` は合成フォントの名前に使用できません +@msg `{_1}` is the name of an installed font `{_1}` はインストール済みのフォントの名前です +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} は合成フォント `{font}` で設定されています +@msg `{base}` is larger than {_1} MB `{base}` は {_1} MB を超えています +@msg `{base}` is not in the DCS file's folder `{base}` は DCS ファイルのフォルダーにありません +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` の単語が {MAX_EDIT_WORDS} 個を超えています +@msg `{key}` must be 0 or more `{key}` には 0 以上を指定してください +@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 number or null, not {v} `{key}` には数値または null を指定してください ({v} は指定できません) +@msg `{key}` must be a whole number of 0 or more `{key}` には 0 以上の整数を指定してください +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}` には 0~{MAX_DISTANCE} pt の値を指定してください +@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} 文字以内の 1 語を指定してください +@msg `{key}` must hold single words of at most {_1} characters `{key}` には {_1} 文字以内の単語を 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 `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` は独立したファイルで、DCS ファイルのパスと同じ場所でのみ見つけられます +@msg `{name}`: give the kanji part `{name}`: 漢字の部分を指定してください +@msg `{name}`: parts must be an object `{name}`: parts にはオブジェクトを指定してください +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: `{k}` は不明なクラスです。クラスは kanji、kana、punctuation、symbols、roman、numerals です +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} の `{k}` は不明なキーです +@msg `{name}`: {_1} must be an object `{name}`: {_1} にはオブジェクトを指定してください +@msg `{name}`: {_1}: {e} `{name}`: {_1}: {e} +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` は合成フォントの名前に使用できません (1~{COMPOSITE_NAME_MAX} 文字、制御文字なし) +@msg `{u}` is no Unicode character `{u}` は Unicode 文字ではありません +@msg `{word}` isn't in the custom dictionary `{word}` はカスタム辞書にありません +@msg a CMap has too many mappings CMap のマッピングが多すぎます +@msg a \\u escape needs four hexadecimal digits \\u エスケープには 4 桁の 16 進数が必要です +@msg a comma in an XPath expression must separate function arguments XPath 式のカンマは関数の引数を区切るものでなければなりません +@msg a document holds at most {_1} SVG filters ドキュメントに含められる SVG フィルターは最大 {_1} 個です +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ドキュメントに保持できる合成フォントは最大 {COMPOSITE_FONTS_MAX} 個です +@msg a document with unsaved changes has no recovery copy 未保存の変更があるドキュメントに復帰用のコピーがありません +@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 host value has too many entries ホストの値のエントリが多すぎます +@msg a host value is nested too deep ホストの値の入れ子が深すぎます +@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 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 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 an SVG filter is named by its id: give `xml` with `name` SVG フィルターの名前はその ID です: `xml` と `name` を指定してください +@msg an XML element has two attributes with the same name XML 要素に同じ名前の属性が 2 つあります +@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 に変換できるのは、項目が 1 つの場合だけです +@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 image couldn't be decoded and is missing from the picture 画像をデコードできなかったため、図に含まれていません +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) {w} × {h} ピクセルの画像は大きすぎて変換できません (最大 {_1} ピクセル) +@msg apply was called on a value that is not a function apply が関数ではない値に対して呼び出されました +@msg area type in a thread cannot use Dynamic Text: {frames} スレッド内のエリア内文字ではダイナミックテキストを使用できません: {frames} +@msg array literal too long 配列リテラルが長すぎます +@msg assignment to a constant binding 定数バインディングへの代入です +@msg at least one row that isn't excluded needs a weight above 0 除外されていない行の少なくとも 1 つに 0 より大きいウェイトが必要です +@msg bad point {v}: expected [x, y] 点 {v} が正しくありません: [x, y] を指定してください +@msg blend must be a mode name blend には描画モードの名前を指定してください +@msg both sides of an XPath | must select nodes XPath の | の両辺でノードを選択する必要があります +@msg canceled キャンセルしました +@msg cannot convert the object to a primitive value オブジェクトをプリミティブ値に変換できません +@msg cannot convert undefined or null to an object undefined または null をオブジェクトに変換できません +@msg character dimensions cannot be represented with the source text transform 文字の寸法をソースのテキスト変形で表すことができません +@msg character dimensions cannot be represented with the target text transform 文字の寸法をターゲットのテキスト変形で表すことができません +@msg continuation must be an array of path chunks continuation にはパスチャンクの配列を指定してください +@msg continuation requires points continuation には点が必要です +@msg dataBase64 is not base64 dataBase64 が base64 ではありません +@msg each font must be {name, parts} 各フォントは {name, parts} にしてください +@msg each font needs a name 各フォントに名前が必要です +@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 weight must be a number of 0 or more (got {v}) 各ウェイトには 0 以上の数値を指定してください (指定値: {v}) +@msg finally table {t} out of range finally テーブル {t} が範囲外です +@msg flags cannot be given with a RegExp object RegExp オブジェクトと一緒にフラグを指定することはできません +@msg fonts must be a list of composite fonts fonts には合成フォントのリストを指定してください +@msg function declaration index out of range 関数宣言のインデックスが範囲外です +@msg give `char`, `text`, `code` or `unicode` `char`、`text`、`code`、`unicode` のいずれかを指定してください +@msg give `count` or `spacing`, not both `count` または `spacing` のどちらか一方を指定してください +@msg give `name`, an SVG filter's name SVG フィルターの名前を `name` に指定してください +@msg give `path`, an SVG file, or `svg`, SVG text SVG ファイルを `path` に、または SVG テキストを `svg` に指定してください +@msg give `xml`, one SVG element SVG の 要素を 1 つ `xml` に指定してください +@msg give exactly one of `at`, `rect` and `points` `at`、`rect`、`points` のうち 1 つだけを指定してください +@msg give exactly one of `path`, `data` and `dataBase64` `path`、`data`、`dataBase64` のうち 1 つだけを指定してください +@msg give fonts: [{name, parts}…] fonts: [{name, parts}…] を指定してください +@msg give index or indexes, not both index または indexes のどちらか一方を指定してください +@msg give one element 要素を 1 つ指定してください +@msg give one of path, data or dataBase64 path、data、dataBase64 のいずれかを指定してください +@msg give opacity or blend opacity または blend を指定してください +@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 hvcC hvcC +@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 1 つのファイルに含められる最大ピクセル数を超えた画像は除外しました +@msg images too large to read were left out 大きすぎて読み込めない画像は除外しました +@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 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 array length 配列の長さが無効です +@msg invalid group グループが無効です +@msg invalid object id {v}: expected a non-negative integer オブジェクト ID {v} が無効です: 0 以上の整数を指定してください +@msg invalid regular expression flag '{_1}' 正規表現のフラグ '{_1}' が無効です +@msg invalid regular expression: {m} 正規表現が無効です: {m} +@msg invalid time value 時刻の値が無効です +@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 image is too large to read 画像が大きすぎて読み込めません +@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 シェイプを 1 つに結合できませんでした +@msg jump to {pc}, past the end 末尾を超えた {pc} へのジャンプです +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding には {_1} のいずれかを指定してください +@msg localize: the first argument must be a localization object or a string localize: 最初の引数にはローカライズオブジェクトまたは文字列を指定してください +@msg missing `picks` `picks` がありません +@msg missing `word` `word` がありません +@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 names must be a list of names names には名前のリストを指定してください +@msg names must be strings names には文字列を指定してください +@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 nesting too deep 入れ子が深すぎます +@msg no HP-GL format HP-GL 形式はありません +@msg no New Document profile called `{shown}` ({_1}) `{shown}` という新規ドキュメントプロファイルはありません ({_1}) +@msg no SVG filter at index {k} インデックス {k} に SVG フィルターはありません +@msg no arrowheads file here ここに矢印ファイルはありません +@msg no character has code {code} コード {code} の文字はありません +@msg no composite font 合成フォントがありません +@msg no composite font named `{n}` `{n}` という名前の合成フォントはありません +@msg no composite fonts to export 書き出す合成フォントがありません +@msg no custom arrowhead {name} (see stroke.arrowheads.list) カスタム矢印 {name} はありません (stroke.arrowheads.list を参照) +@msg no file to open 開くファイルがありません +@msg no instructions 命令がありません +@msg no layer to rename 名前を変更するレイヤーがありません +@msg no random numbers ({e}) 乱数を取得できません ({e}) +@msg no selected or given object has a visible 3D effect 選択または指定したオブジェクトに、表示されている 3D 効果がありません +@msg no such artboard {v} アートボード {v} はありません +@msg no symbol named `{_1}` `{_1}` という名前のシンボルはありません +@msg node {_1} already has the text variable `{_2}` ノード {_1} にはすでにテキスト変数 `{_2}` があります +@msg not a DCS file DCS ファイルではありません +@msg not a RegExp object RegExp オブジェクトではありません +@msg not a composite fonts file: no compositeFonts list 合成フォントファイルではありません: compositeFonts リストがありません +@msg not a composite fonts file: {e} 合成フォントファイルではありません: {e} +@msg not a regular expression 正規表現ではありません +@msg not an action set: the text doesn't start with /version and a number アクションセットではありません: テキストが /version と数値で始まっていません +@msg nothing to repeat 繰り返す対象がありません +@msg numbers out of order in {_1} quantifier {_1} 量指定子の数値の順序が正しくありません +@msg object {_1} needs a 3D effect オブジェクト {_1} には 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 のオブジェクトは面が 1 つです: グラフィックは前面にマッピングしてください +@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 opacity must be a number opacity には数値を指定してください +@msg operand too large オペランドが大きすぎます +@msg order is a whole number from 0 to {others}, not `{v}` order には 0 から {others} までの整数を指定してください (`{v}` は指定できません) +@msg path chunks must share their boundary point パスの分割部分は境界点を共有している必要があります +@msg path is empty パスが空です +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false の場合、useArtboards が false のときは、編集可能なアートワークを保持するためにアートボードが必要です +@msg point x must be a number 点の x には数値を指定してください +@msg point y must be a number 点の y には数値を指定してください +@msg points must be [x,y] pairs points には [x,y] の組を指定してください +@msg points must be finite canvas points points にはキャンバス上の有限の点を指定してください +@msg range out of order in character class 文字クラス内の範囲の順序が正しくありません +@msg reduce of an empty array with no initial value 初期値のない空の配列に対する reduce です +@msg regular expression flag '{_1}' given twice 正規表現のフラグ '{_1}' が 2 回指定されています +@msg sampled text would exceed the canvas (coordinates out of range) サンプリングしたテキストがキャンバスを超えます (座標が範囲外) +@msg select an SVG filter SVG フィルターを選択してください +@msg select an object with a 3D effect 3D 効果のあるオブジェクトを選択してください +@msg select an object with a 3D effect, or give its id in ids 3D 効果のあるオブジェクトを選択するか、その ID を ids に指定してください +@msg select one layer, group or object to rename 名前を変更するレイヤー、グループ、オブジェクトのいずれかを 1 つ選択してください +@msg select the art to weed カス取りするアートを選択してください +@msg select type on a path or area type パス上文字またはエリア内文字を選択してください +@msg side is left or right, not `{v}` side には left または right を指定してください (`{v}` は指定できません) +@msg source text transform is collapsed or unrepresentable 元のテキストの変形がつぶれているか、表現できません +@msg table must be true or false table には true または false を指定してください +@msg takes one name 名前を 1 つ指定してください +@msg target text transform is collapsed or unrepresentable 対象のテキストの変形がつぶれているか、表現できません +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer 分割数 (最大 100000) またはダッシュの段数が多くなりすぎます: 破線パターンを単純にするか、ダッシュを長くしてください +@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 SVG file is larger than {MAX_IMPORT_BYTES} bytes SVG ファイルが {MAX_IMPORT_BYTES} バイトを超えています +@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 argument list is not an object 引数リストがオブジェクトではありません +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out アートワークが EPS には大きすぎます: PostScript の 256 MB より後の部分は除外しました +@msg the bitmaps hold more pixels than one file may ビットマップのピクセル数が 1 つのファイルに含められる数を超えています +@msg the code runs past its last instruction コードが最後の命令を超えて実行されます +@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 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} 色ありますが、ドキュメント減色で使用できるのは最大 255 色です +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ファイルに {COMPOSITE_FONTS_MAX} 個を超える合成フォントがあります +@msg the file holds no composite fonts ファイルに合成フォントがありません +@msg the file holds two composite fonts named `{_1}` ファイルに `{_1}` という名前の合成フォントが 2 つあります +@msg the file is larger than 4 MB ファイルが 4 MB を超えています +@msg the file isn't valid SVG: {e} ファイルが有効な SVG ではありません: {e} +@msg the file names no plate ファイルに版が指定されていません +@msg the function's prototype is not an object 関数のプロトタイプがオブジェクトではありません +@msg the guide is hidden or locked ガイドが非表示またはロックされています +@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 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 パラメーターまたは本体が 1 つの関数になっていません +@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 paths are too many or too tangled to find where they cross パスが多すぎるか複雑に絡み合っているため、交差する位置を見つけられません +@msg the pen table holds more than 255 pens ペンテーブルに 255 本を超えるペンがあります +@msg the plates can't be kept 版を保持できません +@msg the position table does not match the instructions 位置テーブルが命令と一致しません +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes prompt のテキストが {MAX_PROMPT_BYTES} バイトを超えています +@msg the radix must be between 2 and 36 基数には 2 から 36 までの値を指定してください +@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 is nested more than {MAX_DEPTH} levels deep 選択したアートの入れ子が {MAX_DEPTH} 階層を超えています +@msg the selected art is too large to measure 選択したアートが大きすぎて計測できません +@msg the selection has no outlines to weed around 選択範囲に、カス取りの対象となるアウトラインがありません +@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 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 {_1} plate lies outside the file {_1} 版がファイルの外にあります +@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 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: ゲッターはソースとして書き出せません +@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 many arguments 引数が多すぎます +@msg too many quantifiers 量指定子が多すぎます +@msg two composite fonts are named `{_1}` `{_1}` という名前の合成フォントが 2 つあります +@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`) `{t}` は不明な OpenType 機能です (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 には、`salt=2` のように `tag=N` の形式で代替字形の番号を指定できます。N は 0 から {_1} まで) +@msg unknown dither `{d}` (diffusion, pattern, noise or none) `{d}` は不明なディザの指定です (diffusion、pattern、noise、none のいずれか) +@msg unknown key `{k}`; a font takes name and parts `{k}` は不明なキーです。フォントに指定できるのは name と parts です +@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 reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) `{reduction}` は不明な減色の指定です (selective、perceptual、adaptive、web、gray、blackWhite、uniform、document のいずれか) +@msg unmatched ')' 対応していない ')' があります +@msg unterminated character class 文字クラスが閉じられていません +@msg unterminated group グループが閉じられていません +@msg wordGap and lineGap need area type in the selection wordGap と lineGap を使用するには、選択範囲にエリア内文字が必要です +@msg xpath needs its variables as an object xpath の変数にはオブジェクトを指定してください +@msg xpath needs the expression as a string xpath の式には文字列を指定してください +@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 {_1} is not a folder: {e} {_1} はフォルダーではありません: {e} +@msg {_1}: {_2} and {_3} are in the way {_1}: {_2} と {_3} が妨げになっています +@msg {code}: {0} {code}: {0} +@msg {dir}: {e} {dir}: {e} +@msg {file} names more than one linked JavaScript file {file} にリンクされた JavaScript ファイルが複数指定されています +@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}: baseline must be {_1} to {_2}% {k}: baseline には {_1}~{_2}% を指定してください +@msg {k}: custom must be a name {k}: custom には名前を指定してください +@msg {k}: give a font {k}: フォントを指定してください +@msg {k}: only kana can be scaled at the center {k}: 中央を基準に拡大・縮小できるのはかなだけです +@msg {k}: only kana, roman and numerals can be scaled {k}: 拡大・縮小できるのはかな、欧文、数字だけです +@msg {k}: scale must be {_1} to {_2}% {k}: scale には {_1}~{_2}% を指定してください +@msg {k}: size must be {_1} to {_2}% {k}: size には {_1}~{_2}% を指定してください +@msg {name} must be a number {name} には数値を指定してください +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: copy で DCS ファイルを使用できるのは、版がファイル内にある場合だけです。版ファイルのある場所でそのファイルに再リンクしてください +@msg {path}: larger than {_1} MB {path}: {_1} MB を超えています +@msg {shown} is not an XPath 1.0 function {shown} は XPath 1.0 の関数ではありません +@msg {v} names no layer or sublayer {v} に該当するレイヤーまたはサブレイヤーはありません +@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} では 1 枚の用紙に {MAX_COPIES_PER_SHEET} 部を超えてプリントされます: 拡大してください +@msg {what} {i} out of range {what} {i} が範囲外です +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. 1 つのプロファイルに適用できる回転体効果は 1 つです。既存の効果のオプションを編集してください。 +@msg Add Points at Crossings: a path went missing 交差点にポイントを追加: パスが見つからなくなりました +@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 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 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 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 「内側描画」を使用するには、パス、複合パス、テキストのいずれかを 1 つだけ選択してください +@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 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 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 Invalid dimension offset 寸法のオフセットが無効です +@msg Invalid point 点が無効です +@msg Join: a subpath went missing 連結: サブパスが見つからなくなりました +@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 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 Recolor Artwork is no longer open 「アートワークを再配色」はすでに閉じられています +@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 保存で書き出せるアートボードは 1 つです: 1 つ指定するか、複数の場合は 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 2 つ以上のオブジェクト、またはグループを選択してください +@msg Shaper needs 1–64 source paths Shaper には 1~64 個の元パスが必要です +@msg Switch x/y is for scatter graphs 「X/Y を入れ替え」は散布図用です +@msg Text options: {e} テキストオプション: {e} +@msg `align` is start, center or end `align` には start、center、end のいずれかを指定してください +@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 `artboards` must be a list of artboard indices `artboards` にはアートボードのインデックスのリストを指定してください +@msg `asset` is the asset id (see assets.list) `asset` にはアセット ID を指定してください (assets.list を参照) +@msg `at` must be a byte offset `at` にはバイトオフセットを指定してください +@msg `autoLeading` must be a number `autoLeading` には数値を指定してください +@msg `background` must be "", "matte" or #rrggbb `background` には ""、"matte"、#rrggbb のいずれかを指定してください +@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 `color` needs a `rule` to make the theme from `color` には、テーマの作成に使用する `rule` が必要です +@msg `colors` is empty `colors` が空です +@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 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 `each` is the value one design stands for, more than 0 `each` にはデザイン 1 つが表す値を 0 より大きい数で指定してください +@msg `end` must be "start" or "end" `end` には "start" または "end" を指定してください +@msg `ends` applies to joining exactly two paths `ends` を使用できるのは、ちょうど 2 つのパスを連結する場合だけです +@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 `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` には、直前の text.findFontFiles の検索で見つかったフォントファイルのパスを 1 ~ 1000 個指定してください +@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` には 0 以上の整数を指定してください +@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` は 1 つの文字に設定するもので、文字スタイルには設定できません (text.setGlyphAlternate で設定します) +@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` 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` には項目のインデックスを 1 つ以上含む配列を指定してください +@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 `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 `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 `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 `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 `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 `path` must be a folder `path` にはフォルダーを指定してください +@msg `path` must be a path object `path` にはパスオブジェクトを指定してください +@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 `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 `ratio` must be from 0.1 to 0.5 `ratio` には 0.1 から 0.5 までの値を指定してください +@msg `rect` must be [x, y, width, height] with a positive size `rect` には、サイズが正の [x, y, width, height] を指定してください +@msg `relation` must be overlap, inside or outside `relation` には overlap、inside、outside のいずれかを指定してください +@msg `reversed` must be true or false `reversed` には true または false を指定してください +@msg `rotate` must be a finite number `rotate` には有限の数値を指定してください +@msg `rule` must be nonZero or evenOdd `rule` には nonZero または evenOdd を指定してください +@msg `scale` must be a number in (0, 100] `scale` には (0, 100] の範囲の数値を指定してください +@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` には系列のインデックスを 1 つ以上含むリストを指定してください +@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 `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` must be `folder` or `datasets` `source` には `folder` または `datasets` を指定してください +@msg `spacing` must be a number `spacing` には数値を指定してください +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing` には 0 以上のポイント単位の数値、または "auto" を指定してください +@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` には整数のインデックスを 2 つ指定してください +@msg `symbol` must be a symbol name `symbol` にはシンボル名を指定してください +@msg `target` must be "object" or "contents", not {v} `target` には "object" または "contents" を指定してください ({v} は指定できません) +@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` には、整列した行ではなく、1 つのオブジェクトのスタック内の行を指定してください +@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` に同じ種類が 2 回指定されています +@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 `{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}` 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 [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` には [min, desired, max] または {{min?, desired?, max?}} をパーセントで指定してください +@msg `{key}` must be a non-negative integer `{key}` には 0 以上の整数を指定してください +@msg `{key}` must be a number {_1}..{_2} `{key}` には {_1}..{_2} の数値を指定してください +@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 {_1} to {shift_max} `{key}` には {_1} から {shift_max} までの値を指定してください +@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 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}`: Minimum must not exceed Desired, nor Desired Maximum `{name}`: 最小は最適を、最適は最大を超えないようにしてください +@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" のような 4 文字です +@msg `{to}` is empty or already used `{to}` が空か、すでに使用されています +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` が {MAX_TEXT} バイトを超えています +@msg `{what}` must be text `{what}` にはテキストを指定してください +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard DXF 図面の原点は 1 つです: アートボードごとに 1 つの図面にするには useArtboards: true を指定してください +@msg a PDF presets file with over {_1} presets PDF プリセットファイルに {_1} 個を超えるプリセットがあります +@msg a batch preview cannot switch documents バッチのプレビューではドキュメントを切り替えられません +@msg a brush named `{nn}` exists `{nn}` という名前のブラシがすでにあります +@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 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 feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes feConvolveMatrix フィルターの order が {x}×{y} で、usvg が行うカーネルサイズの乗算がオーバーフローします +@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 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 名前で指定できるアセットは 1 つです: multiple: false を指定するか、オブジェクトを 1 つにしてください +@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 辺が 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 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 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 artboard is listed twice 同じアートボードが 2 回指定されています +@msg an empty artboard has no width or height to scale to 空のアートボードには、拡大・縮小の基準となる幅も高さもありません +@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 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 angle と roundness には有限の値を指定してください +@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 area must be a closed polygon of 3 to 100000 [x, y] points area には 3 から 100000 個の [x, y] の点からなる閉じた多角形を指定してください +@msg area type needs a path that encloses an area エリア内文字には、領域を囲むパスが必要です +@msg area type {_1} is named twice エリア内文字 {_1} が 2 回指定されています +@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 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] 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 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 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 choose at least one artboard アートボードを 1 つ以上選択してください +@msg choose at least one status 状態を 1 つ以上選択してください +@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} にオープンパスがちょうど 1 つ含まれている必要があります +@msg connect end must be "start" or "end" connect の end には "start" または "end" を指定してください +@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 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 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}`: アート、仕上がり、裁ち落とし、メディアの各ボックスがあるのは 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-…) データキー `{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}` の値が 2 つあります +@msg data set `{name}` is declared twice データセット `{name}` が 2 回宣言されています +@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 row needs `from` 各行には `from` が必要です +@msg each stop needs a position 各分岐点には位置が必要です +@msg each subpath needs an anchors array 各サブパスには anchors 配列が必要です +@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" から 2 つを指定してください +@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 fit must be none|autoHeight|shrinkText|block, got {id} fit には none|autoHeight|shrinkText|block のいずれかを指定してください (指定値: {id}) +@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 give `before` or `after`, not both `before` または `after` のどちらか一方を指定してください +@msg give `colors`, or `color` and `rule` `colors`、または `color` と `rule` を指定してください +@msg give `event`, such as onclick onclick などの `event` を指定してください +@msg give `file`, the JavaScript file's path or URL `file` に JavaScript ファイルのパスまたは URL を指定してください +@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 `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 `script`, the JavaScript the event runs ("" removes the event) `script` に、イベントで実行する JavaScript を指定してください ("" を指定するとイベントを削除します) +@msg give `text` (the reading) or `remove` `text` (読み) または `remove` を指定してください +@msg give `x` and `y` (and a `tolerance` of 0 or more) `x` と `y` (および 0 以上の `tolerance`) を指定してください +@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 のうち 1 つ以上を指定してください +@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 formats or a preset, not both formats またはプリセットのどちらか一方を指定してください +@msg give newName or delete newName または delete を指定してください +@msg give offset or by offset または by を指定してください +@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、列の位置のうち 1 つだけを指定してください +@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` を 2 つ以上指定してください +@msg give two or more swatches スウォッチを 2 つ以上指定してください +@msg give x, y, or a handle's length or angle x、y、またはハンドルの length か angle を指定してください +@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 にはプリセット名または 2 つのカラーを指定してください +@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 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 1 つのレイヤーまたはグループ内の行を 2 つ以上ハイライトしてください +@msg highlight two or more rows to merge 結合する行を 2 つ以上ハイライトしてください +@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 index out of range インデックスが範囲外です +@msg instances must be 1..1000 instances には 1..1000 の値を指定してください +@msg invalid `color` `color` が無効です +@msg invalid anchor index {v}: expected a non-negative integer アンカーのインデックス {v} が無効です: 0 以上の整数を指定してください +@msg invalid anchor reference {v}: expected [subpath, anchor] アンカーの参照 {v} が無効です: [subpath, anchor] を指定してください +@msg invalid angle 角度が無効です +@msg invalid brush: {e} ブラシが無効です: {e} +@msg invalid color カラーが無効です +@msg invalid colour {c} カラー {c} が無効です +@msg invalid delta 差分が無効です +@msg invalid point 点が無効です +@msg invalid size サイズが無効です +@msg invalid t t が無効です +@msg invalid tolerance 許容値が無効です +@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 metadata has no description メタデータに説明がありません +@msg its metadata has no direct length メタデータに直接指定された長さがありません +@msg join two different paths 異なる 2 つのパスを連結してください +@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 には 0 以上の幅を指定してください +@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 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` がありません: 指定するか、文字オブジェクトを 1 つ選択してください +@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 `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` がありません: 0 以上の整数を指定してください +@msg missing `type` `type` がありません +@msg missing `value` `value` がありません +@msg missing `{key}` `{key}` がありません +@msg missing `{k}` `{k}` がありません +@msg missing anchor 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 point がありません +@msg missing point [x,y] point [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 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 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 アンカーが 1 つ以上必要です +@msg need at least one point 点が 1 つ以上必要です +@msg need at least two points 点が 2 つ以上必要です +@msg needs points 点が必要です +@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 PDF preset named `{n}` `{n}` という名前の PDF プリセットはありません +@msg no action set is current 現在のアクションセットがありません +@msg no active data set アクティブなデータセットがありません +@msg no appearance item {index} アピアランス項目 {index} はありません +@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 colour カラーがありません +@msg no colour group `{group}` カラーグループ `{group}` はありません +@msg no colour group `{name}` カラーグループ `{name}` はありません +@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 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 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 {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 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 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 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 `{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} is not a graph ノード {_1} はグラフではありません +@msg node {_1} is not an image ノード {_1} は画像ではありません +@msg node {_1} is not type ノード {_1} は文字ではありません +@msg not a PDF presets file (format `{_1}`) PDF プリセットファイルではありません (形式 `{_1}`) +@msg not a PDF presets file: {e} PDF プリセットファイルではありません: {e} +@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 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 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 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 at index {index} オブジェクト {_1} にはインデックス {index} に 3D 効果が必要です +@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 open a document to look for its linked files リンクファイルを検索するには、ドキュメントを開いてください +@msg options must be an object (see file.formatOptions) options にはオブジェクトを指定してください (file.formatOptions を参照) +@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 重なり合う Shaper パスは、同じレイヤーまたはグループ内にある必要があります +@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}` には {n} {artboards} あります +@msg page {page}: `{name}` has {n} {pages} {page} ページ: `{name}` には {n} {pages} あります +@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 parameters にはオブジェクトを指定してください +@msg params must be an object params にはオブジェクトを指定してください +@msg passwords can have up to {MAX_PASSWORD} bytes パスワードは {MAX_PASSWORD} バイト以内です +@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 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 points must be [t, left, right] with non-negative widths points には、幅が 0 以上の [t, left, right] を指定してください +@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 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 rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi には 1–2400 を指定してください (72 がスクリーン、150 が標準、300 が高解像度) +@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 registration must be 0..8 registration には 0..8 の値を指定してください +@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: 置き換えるオブジェクトを 1 つ選択してください +@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 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 Shaper グループを選択してください +@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 path パスを選択してください +@msg select a text object テキストオブジェクトを選択してください +@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 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) エリア内文字とクローズパスを選択してください (エリア内文字オブジェクトが 1 つ以上必要です) +@msg select area type whose first line can fit its frame 1 行目がフレームに収まるエリア内文字を選択してください +@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 連結するパスの端点をちょうど 2 つ選択してください +@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 織り交ぜグループを 1 つ選択してください +@msg select one image or pass id 画像を 1 つ選択するか、id を指定してください +@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 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 織り交ぜるオブジェクトを 2 つ以上選択してください +@msg select two or more slices to combine 結合するスライスを 2 つ以上選択してください +@msg select type on a path パス上文字を選択してください +@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 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 元パスが Shaper には大きすぎます +@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 までの数値を 4 つ指定してください (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 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 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 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 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 action set ids are not a list アクションセットの ID がリストではありません +@msg the active paint is not a gradient アクティブなペイントがグラデーションではありません +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size アートボードがキャンバスに収まりません: アートボードの数を減らすか、間隔またはサイズを小さくしてください +@msg the character's size can't be measured 文字のサイズを計測できません +@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 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 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 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 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 header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them `{name}` のヘッダー `{cell}` は、読み戻すと変数 `{other}` として扱われます: どちらかの名前を変更してください +@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 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 parent must be a layer or sublayer 親にはレイヤーまたはサブレイヤーを指定してください +@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 picture has no objects 画像にオブジェクトがありません +@msg the program runs too long プログラムの実行時間が長すぎます +@msg the program uses too much memory プログラムのメモリ使用量が多すぎます +@msg the rectangle must have a width and a height 長方形には幅と高さが必要です +@msg the rectangle needs a size and must lie on the canvas 長方形にはサイズが必要で、キャンバス上にある必要があります +@msg the selected art has no width 選択したアートに幅がありません +@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 slice would leave the canvas スライスがキャンバスからはみ出します +@msg the spot colour couldn't be added 特色を追加できませんでした +@msg the subpath has no ends to extend (it is closed or a single point) サブパスに延長できる端点がありません (閉じているか、点が 1 つだけです) +@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 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 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 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 library: create one first ライブラリがありません: 先に作成してください +@msg these paths are too degenerate for Shaper これらのパスは縮退しているため、Shaper では使用できません +@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 too large for a zip archive: package to a folder zip アーカイブには大きすぎます: フォルダーにパッケージしてください +@msg too many Shaper edits Shaper の編集が多すぎます +@msg too many `points` (at most 100000) `points` が多すぎます (最大 100000) +@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 1 つのファイルにかかるフィルターが多すぎます +@msg too many overlapping paths for Shaper 重なり合うパスが Shaper には多すぎます +@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 two saved selections would be named `{_1}` 保存した選択範囲のうち 2 つが `{_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}` `{name}` は不明な CMYK プロファイルです +@msg unknown File Info field `{k}` `{k}` は不明なファイル情報フィールドです +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) featureChanges 内の `{t}` は不明な OpenType 機能です (`features` で使用できるタグを指定してください) +@msg unknown PDF preset `{name}` (presets: {_1}) `{name}` は不明な PDF プリセットです (プリセット: {_1}) +@msg unknown Warp style 不明なワープスタイルです +@msg unknown alignToPath `{a}` `{a}` は不明な alignToPath の指定です +@msg unknown arrowhead {v} {v} は不明な矢印です +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) `{k}` は不明な artboardLayout のキーです (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 effect `{e}` `{e}` は不明な効果です +@msg unknown end {e} {e} は不明な end の指定です +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) `{name}` は不明な flattenerPreset です (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} {f} は不明な from の指定です +@msg unknown graph type `{t}` `{t}` は不明なグラフの種類です +@msg unknown grid colours `{name}` (one of {_1}) `{name}` は不明なグリッドカラーです ({_1} のいずれか) +@msg unknown gridType `{t}` `{t}` は不明な gridType です +@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}` は不明な model です +@msg unknown model `{other}` (keep, new or document) `{other}` は不明な model です (keep、new、document のいずれか) +@msg unknown op {o} {o} は不明な op です +@msg unknown order `{o}`: toggle, front, forward, backward or back `{o}` は不明な order です: toggle、front、forward、backward、back のいずれかを指定してください +@msg unknown overprints `{v}` (preserve, simulate or discard) `{v}` は不明な overprints の指定です (preserve、simulate、discard のいずれか) +@msg unknown panel 不明なパネルです +@msg unknown pieLegend `{v}` (none, standard or wedges) `{v}` は不明な pieLegend の指定です (none、standard、wedges のいずれか) +@msg unknown piePosition `{v}` (even, ratio or stacked) `{v}` は不明な piePosition の指定です (even、ratio、stacked のいずれか) +@msg unknown pieSort `{v}` (none, all or first) `{v}` は不明な pieSort の指定です (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}` は不明な quotes のキーです (double、single のいずれか) +@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}) `{t}` は不明な tileType です ({_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) `{v}` は不明な {key} の指定です (none、short、full のいずれか) +@msg unknown {key} profile `{n}` `{n}` は不明な {key} プロファイルです +@msg unknown {k} key `{key}` (size, position) `{key}` は不明な {k} のキーです (size、position のいずれか) +@msg unreadable object 読み込めないオブジェクトです +@msg unsafe destination file name 出力先のファイル名が安全ではありません +@msg use paths or compound paths パスまたは複合パスを使用してください +@msg useArtboards must be true or false, not {v} useArtboards には true または false を指定してください ({v} は指定できません) +@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}` が 2 回宣言されています +@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} width と height には 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 width/height には 0 より大きい値を指定してください +@msg x and y must be finite numbers x と y には有限の数値を指定してください +@msg x and y must be given together x と y は一緒に指定してください +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone には anchor が必要です: 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 {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} 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} はアートボードごとに 1 つのファイルを書き出します: document.export を使用するか、アートボードを 1 つ指定してください +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} ピクセルは {_3} には大きすぎます (1 辺あたり最大 {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 {dest}: {e} {dest}: {e} +@msg {doc} has no folder {doc} にフォルダーがありません +@msg {file} isn't linked {file} はリンクされていません +@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}: can't read `{s}` as a length {k}: `{s}` を長さとして読み取れません +@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 {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 {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 {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} 枚を超えるタイルにプリントされます: 縮小してください Playback Options 再生オプション Accelerated 高速 Step by Step ステップ実行 diff --git a/docs/localization-parity.md b/docs/localization-parity.md index cd8dc33f30..12b68a0d8b 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -34,7 +34,7 @@ Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker | French | `fr` | 4,020 / 4,020 (100%) | all | Latin | no | full | — (review) | | Portuguese | `pt-br` | 6,106 catalog entries (counted 2026-10-31 on `main` at `d26dd933a`): menus, `tl!` literals and messages complete when counted; 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) | +| Japanese | `ja` | 4,004 / 4,020 (99.6%) | all (3,459 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists no missing messages for it) | 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 (602 of 604 message rows; landed 2026-10-10) | 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) | | 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) | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e7c1b28726..1d02d3c039 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -875,6 +875,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`). - The status bar translates the limits a plug-in run can exceed (its instruction, time and memory budgets and its output size) and the error of a plug-in removal that leaves the plug-in's grant in the plug-in store, in every language that translates every message. The untranslated-strings report also lists messages that an error or a status takes from a `&str` constant (`plugin_limit_errors_are_translated`, `message_scan_reads_messages_held_in_constants`). - The status bar translates the plug-in host's other errors (manifest, module and run checks, the output an importer, exporter or effect returns, and the approval a plug-in's permissions need) in every language that translates every message. A JSON object or array inside a message stays as written, and a message that wraps another one, such as `plug-in manifest: {0}`, is matched by its own row before a row that starts with a placeholder. The reasons inside these errors (what is wrong with a parameter's spec or dialog keys, the value a parameter needs, what is wrong with an object, stroke, paint or effect in a plug-in's output) are translated with them, and the untranslated-strings report lists the reasons the plug-in host's error helpers take (`plugin_messages_are_translated_in_every_complete_catalog`, `plugin_errors_translate_with_their_values`, `plugin_error_reasons_are_translated_with_the_error`, `message_scan_reads_plugin_helpers`, `templates_that_start_with_text_match_first`). + - The Japanese catalog (`crates/ui-egui/src/i18n/ja.tsv`) has an `@msg` row for every status and error message the untranslated report (`untranslated_report`) finds in the sources, 3,459 `@msg` rows in all. The report's Japanese section lists no messages. The status bar does not use the rows for ``unknown parameter `{within}{key}`; {why}`` and `{name} must be {lo}–{hi}{unit} (got {v})`, because a message with two placeholders in a row cannot be matched (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`). The row for `{k} has control characters{_1}` does not match the File Info error for a multi-line field, where the last placeholder is empty. A native speaker has not reviewed the wording. - The status bar translates the errors a script's `File` and `Folder` objects, `vc.run`, `$.evalFile`, the `$` object's properties and functions, `alert`, `confirm` and `prompt`, `vc.document` and `vc.log` raise, the limits a run can pass, and the errors of Drop Script Engine, and the line for a run that fails (a syntax error, a thrown error, a limit, a stop or an internal error, with the script's name, the line and column, the included file it is in and the files left written), in every language that translates every message. The script's name, paths, numbers, command ids and the text a script throws itself are shown as they are. A failure with no line and column, such as an error in code `eval` compiled, is shown with one colon after the script's name (`an_error_without_a_place_names_the_script_and_kind`). A message template that names a value twice matches only when both places hold the same text, so the line for a failed run is not taken for the message inside it (`a_repeated_placeholder_matches_the_same_text`). The untranslated-strings report also lists the messages the script host's `fail`, `value` and `over` helpers take and the errors the script interpreter throws, and a constant's text comes from the file or crate that declares it, so the Scripts menu's headers are not listed as messages (`script_errors_are_translated`, `script_host_errors_are_translated`, `message_scan_reads_script_interpreter_errors`, `message_scan_reads_constants_of_the_file_and_its_crate`, `message_scan_skips_test_modules_with_any_test_cfg`). - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian, Georgian and Catalan ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). 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`. From e06d0577b37e39d6f0e6ed7968aa0639db4f6228 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 16:00:22 -0800 Subject: [PATCH 03/44] i18n: Japanese leaves out the rows #7211, #7214 and #7116 add Open #7211, #7214 and #7116 add Japanese rows for 38 of the messages this branch translates. A second row for the same key fails bundled_catalogs_are_consistent once both sides merge, so this branch drops its 38 rows and leaves those messages to the other pull requests. The Japanese lines in docs/localization-parity.md and docs/target-app-parity.md give the new counts. --- crates/ui-egui/src/i18n/ja.tsv | 38 ---------------------------------- docs/localization-parity.md | 2 +- docs/target-app-parity.md | 2 +- 3 files changed, 2 insertions(+), 40 deletions(-) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 9b945bd23d..83145f57e2 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6401,9 +6401,6 @@ axis Y Axis Y 軸 @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 name a composite font `{_1}` は合成フォントの名前に使用できません -@msg `{_1}` is the name of an installed font `{_1}` はインストール済みのフォントの名前です -@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} は合成フォント `{font}` で設定されています @msg `{base}` is larger than {_1} MB `{base}` は {_1} MB を超えています @msg `{base}` is not in the DCS file's folder `{base}` は DCS ファイルのフォルダーにありません @msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` の単語が {MAX_EDIT_WORDS} 個を超えています @@ -6422,20 +6419,13 @@ axis Y Axis Y 軸 @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 `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` は独立したファイルで、DCS ファイルのパスと同じ場所でのみ見つけられます -@msg `{name}`: give the kanji part `{name}`: 漢字の部分を指定してください -@msg `{name}`: parts must be an object `{name}`: parts にはオブジェクトを指定してください -@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: `{k}` は不明なクラスです。クラスは kanji、kana、punctuation、symbols、roman、numerals です -@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} の `{k}` は不明なキーです -@msg `{name}`: {_1} must be an object `{name}`: {_1} にはオブジェクトを指定してください @msg `{name}`: {_1}: {e} `{name}`: {_1}: {e} -@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` は合成フォントの名前に使用できません (1~{COMPOSITE_NAME_MAX} 文字、制御文字なし) @msg `{u}` is no Unicode character `{u}` は Unicode 文字ではありません @msg `{word}` isn't in the custom dictionary `{word}` はカスタム辞書にありません @msg a CMap has too many mappings CMap のマッピングが多すぎます @msg a \\u escape needs four hexadecimal digits \\u エスケープには 4 桁の 16 進数が必要です @msg a comma in an XPath expression must separate function arguments XPath 式のカンマは関数の引数を区切るものでなければなりません @msg a document holds at most {_1} SVG filters ドキュメントに含められる SVG フィルターは最大 {_1} 個です -@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ドキュメントに保持できる合成フォントは最大 {COMPOSITE_FONTS_MAX} 個です @msg a document with unsaved changes has no recovery copy 未保存の変更があるドキュメントに復帰用のコピーがありません @msg a font's data is too large フォントのデータが大きすぎます @msg a font's subroutines are too large フォントのサブルーチンが大きすぎます @@ -6484,15 +6474,11 @@ axis Y Axis Y 軸 @msg character dimensions cannot be represented with the target text transform 文字の寸法をターゲットのテキスト変形で表すことができません @msg continuation must be an array of path chunks continuation にはパスチャンクの配列を指定してください @msg continuation requires points continuation には点が必要です -@msg dataBase64 is not base64 dataBase64 が base64 ではありません -@msg each font must be {name, parts} 各フォントは {name, parts} にしてください -@msg each font needs a name 各フォントに名前が必要です @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 weight must be a number of 0 or more (got {v}) 各ウェイトには 0 以上の数値を指定してください (指定値: {v}) @msg finally table {t} out of range finally テーブル {t} が範囲外です @msg flags cannot be given with a RegExp object RegExp オブジェクトと一緒にフラグを指定することはできません -@msg fonts must be a list of composite fonts fonts には合成フォントのリストを指定してください @msg function declaration index out of range 関数宣言のインデックスが範囲外です @msg give `char`, `text`, `code` or `unicode` `char`、`text`、`code`、`unicode` のいずれかを指定してください @msg give `count` or `spacing`, not both `count` または `spacing` のどちらか一方を指定してください @@ -6501,10 +6487,8 @@ axis Y Axis Y 軸 @msg give `xml`, one SVG element SVG の 要素を 1 つ `xml` に指定してください @msg give exactly one of `at`, `rect` and `points` `at`、`rect`、`points` のうち 1 つだけを指定してください @msg give exactly one of `path`, `data` and `dataBase64` `path`、`data`、`dataBase64` のうち 1 つだけを指定してください -@msg give fonts: [{name, parts}…] fonts: [{name, parts}…] を指定してください @msg give index or indexes, not both index または indexes のどちらか一方を指定してください @msg give one element 要素を 1 つ指定してください -@msg give one of path, data or dataBase64 path、data、dataBase64 のいずれかを指定してください @msg give opacity or blend opacity または blend を指定してください @msg handler at {_1} expects {_2} values but its range starts with {d} {_1} のハンドラーは {_2} 個の値を想定していますが、範囲の開始時は {d} 個です @msg handler depth too large ハンドラーの深さが大きすぎます @@ -6549,8 +6533,6 @@ axis Y Axis 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 names must be a list of names names には名前のリストを指定してください -@msg names must be strings names には文字列を指定してください @msg needs a closing quote after the name 名前の後に閉じ引用符が必要です @msg needs a name 名前が必要です @msg needs a name of at most 64 bytes 64 バイト以内の名前が必要です @@ -6562,9 +6544,6 @@ axis Y Axis Y 軸 @msg no SVG filter at index {k} インデックス {k} に SVG フィルターはありません @msg no arrowheads file here ここに矢印ファイルはありません @msg no character has code {code} コード {code} の文字はありません -@msg no composite font 合成フォントがありません -@msg no composite font named `{n}` `{n}` という名前の合成フォントはありません -@msg no composite fonts to export 書き出す合成フォントがありません @msg no custom arrowhead {name} (see stroke.arrowheads.list) カスタム矢印 {name} はありません (stroke.arrowheads.list を参照) @msg no file to open 開くファイルがありません @msg no instructions 命令がありません @@ -6576,8 +6555,6 @@ axis Y Axis Y 軸 @msg node {_1} already has the text variable `{_2}` ノード {_1} にはすでにテキスト変数 `{_2}` があります @msg not a DCS file DCS ファイルではありません @msg not a RegExp object RegExp オブジェクトではありません -@msg not a composite fonts file: no compositeFonts list 合成フォントファイルではありません: compositeFonts リストがありません -@msg not a composite fonts file: {e} 合成フォントファイルではありません: {e} @msg not a regular expression 正規表現ではありません @msg not an action set: the text doesn't start with /version and a number アクションセットではありません: テキストが /version と数値で始まっていません @msg nothing to repeat 繰り返す対象がありません @@ -6592,7 +6569,6 @@ axis Y Axis Y 軸 @msg operand too large オペランドが大きすぎます @msg order is a whole number from 0 to {others}, not `{v}` order には 0 から {others} までの整数を指定してください (`{v}` は指定できません) @msg path chunks must share their boundary point パスの分割部分は境界点を共有している必要があります -@msg path is empty パスが空です @msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false の場合、useArtboards が false のときは、編集可能なアートワークを保持するためにアートボードが必要です @msg point x must be a number 点の x には数値を指定してください @msg point y must be a number 点の y には数値を指定してください @@ -6644,10 +6620,6 @@ axis Y Axis Y 軸 @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} 色ありますが、ドキュメント減色で使用できるのは最大 255 色です -@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ファイルに {COMPOSITE_FONTS_MAX} 個を超える合成フォントがあります -@msg the file holds no composite fonts ファイルに合成フォントがありません -@msg the file holds two composite fonts named `{_1}` ファイルに `{_1}` という名前の合成フォントが 2 つあります -@msg the file is larger than 4 MB ファイルが 4 MB を超えています @msg the file isn't valid SVG: {e} ファイルが有効な SVG ではありません: {e} @msg the file names no plate ファイルに版が指定されていません @msg the function's prototype is not an object 関数のプロトタイプがオブジェクトではありません @@ -6696,10 +6668,8 @@ axis Y Axis Y 軸 @msg toString is not a function toString は関数ではありません @msg too many arguments 引数が多すぎます @msg too many quantifiers 量指定子が多すぎます -@msg two composite fonts are named `{_1}` `{_1}` という名前の合成フォントが 2 つあります @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`) `{t}` は不明な OpenType 機能です (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 には、`salt=2` のように `tag=N` の形式で代替字形の番号を指定できます。N は 0 から {_1} まで) @msg unknown dither `{d}` (diffusion, pattern, noise or none) `{d}` は不明なディザの指定です (diffusion、pattern、noise、none のいずれか) -@msg unknown key `{k}`; a font takes name and parts `{k}` は不明なキーです。フォントに指定できるのは name と parts です @msg unknown panel `{panel}` `{panel}` は不明なパネルです @msg unknown parameter `{key}` `{key}` は不明なパラメーターです @msg unknown parameter `{within}settings.{format}`; {why} `{within}settings.{format}` は不明なパラメーターです。{why} @@ -6720,13 +6690,7 @@ axis Y Axis Y 軸 @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}: baseline must be {_1} to {_2}% {k}: baseline には {_1}~{_2}% を指定してください @msg {k}: custom must be a name {k}: custom には名前を指定してください -@msg {k}: give a font {k}: フォントを指定してください -@msg {k}: only kana can be scaled at the center {k}: 中央を基準に拡大・縮小できるのはかなだけです -@msg {k}: only kana, roman and numerals can be scaled {k}: 拡大・縮小できるのはかな、欧文、数字だけです -@msg {k}: scale must be {_1} to {_2}% {k}: scale には {_1}~{_2}% を指定してください -@msg {k}: size must be {_1} to {_2}% {k}: size には {_1}~{_2}% を指定してください @msg {name} must be a number {name} には数値を指定してください @msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: copy で DCS ファイルを使用できるのは、版がファイル内にある場合だけです。版ファイルのある場所でそのファイルに再リンクしてください @msg {path}: larger than {_1} MB {path}: {_1} MB を超えています @@ -6996,7 +6960,6 @@ axis Y Axis Y 軸 @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}`: Minimum must not exceed Desired, nor Desired Maximum `{name}`: 最小は最適を、最適は最大を超えないようにしてください @msg `{n}` can't be deleted `{n}` は削除できません @msg `{n}` can't be merged `{n}` は結合できません @msg `{n}` can't be moved `{n}` は移動できません @@ -7747,7 +7710,6 @@ axis Y Axis Y 軸 @msg select a graph グラフを選択してください @msg select a group or layer with visible, unlocked members 表示されていてロックされていないメンバーを含むグループまたはレイヤーを選択してください @msg select a layer レイヤーを選択してください -@msg select a path パスを選択してください @msg select a text object テキストオブジェクトを選択してください @msg select an image (or an Image Trace group) 画像 (または画像トレースグループ) を選択してください @msg select an image, or pass id 画像を選択するか、id を指定してください diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 12b68a0d8b..1df39336d0 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -34,7 +34,7 @@ Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker | French | `fr` | 4,020 / 4,020 (100%) | all | Latin | no | full | — (review) | | Portuguese | `pt-br` | 6,106 catalog entries (counted 2026-10-31 on `main` at `d26dd933a`): menus, `tl!` literals and messages complete when counted; 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 (3,459 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists no missing messages for it) | CJK UI font as above; Japanese IME; vertical type partial; Japanese crop marks and composition settings | no | full | — (review) | +| Japanese | `ja` | 4,004 / 4,020 (99.6%) | all (3,421 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 38 missing messages for it) | 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 (602 of 604 message rows; landed 2026-10-10) | 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) | | 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) | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 1d02d3c039..72ee4a28f4 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -875,7 +875,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`). - The status bar translates the limits a plug-in run can exceed (its instruction, time and memory budgets and its output size) and the error of a plug-in removal that leaves the plug-in's grant in the plug-in store, in every language that translates every message. The untranslated-strings report also lists messages that an error or a status takes from a `&str` constant (`plugin_limit_errors_are_translated`, `message_scan_reads_messages_held_in_constants`). - The status bar translates the plug-in host's other errors (manifest, module and run checks, the output an importer, exporter or effect returns, and the approval a plug-in's permissions need) in every language that translates every message. A JSON object or array inside a message stays as written, and a message that wraps another one, such as `plug-in manifest: {0}`, is matched by its own row before a row that starts with a placeholder. The reasons inside these errors (what is wrong with a parameter's spec or dialog keys, the value a parameter needs, what is wrong with an object, stroke, paint or effect in a plug-in's output) are translated with them, and the untranslated-strings report lists the reasons the plug-in host's error helpers take (`plugin_messages_are_translated_in_every_complete_catalog`, `plugin_errors_translate_with_their_values`, `plugin_error_reasons_are_translated_with_the_error`, `message_scan_reads_plugin_helpers`, `templates_that_start_with_text_match_first`). - - The Japanese catalog (`crates/ui-egui/src/i18n/ja.tsv`) has an `@msg` row for every status and error message the untranslated report (`untranslated_report`) finds in the sources, 3,459 `@msg` rows in all. The report's Japanese section lists no messages. The status bar does not use the rows for ``unknown parameter `{within}{key}`; {why}`` and `{name} must be {lo}–{hi}{unit} (got {v})`, because a message with two placeholders in a row cannot be matched (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`). The row for `{k} has control characters{_1}` does not match the File Info error for a multi-line field, where the last placeholder is empty. A native speaker has not reviewed the wording. + - The Japanese catalog (`crates/ui-egui/src/i18n/ja.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,421 `@msg` rows in all. The report lists 38 Japanese messages: the composite font, select-a-path and justification order errors that open #7211, #7214 and #7116 translate. The status bar does not use the rows for ``unknown parameter `{within}{key}`; {why}`` and `{name} must be {lo}–{hi}{unit} (got {v})`, because a message with two placeholders in a row cannot be matched (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`). The row for `{k} has control characters{_1}` does not match the File Info error for a multi-line field, where the last placeholder is empty. A native speaker has not reviewed the wording. - The status bar translates the errors a script's `File` and `Folder` objects, `vc.run`, `$.evalFile`, the `$` object's properties and functions, `alert`, `confirm` and `prompt`, `vc.document` and `vc.log` raise, the limits a run can pass, and the errors of Drop Script Engine, and the line for a run that fails (a syntax error, a thrown error, a limit, a stop or an internal error, with the script's name, the line and column, the included file it is in and the files left written), in every language that translates every message. The script's name, paths, numbers, command ids and the text a script throws itself are shown as they are. A failure with no line and column, such as an error in code `eval` compiled, is shown with one colon after the script's name (`an_error_without_a_place_names_the_script_and_kind`). A message template that names a value twice matches only when both places hold the same text, so the line for a failed run is not taken for the message inside it (`a_repeated_placeholder_matches_the_same_text`). The untranslated-strings report also lists the messages the script host's `fail`, `value` and `over` helpers take and the errors the script interpreter throws, and a constant's text comes from the file or crate that declares it, so the Scripts menu's headers are not listed as messages (`script_errors_are_translated`, `script_host_errors_are_translated`, `message_scan_reads_script_interpreter_errors`, `message_scan_reads_constants_of_the_file_and_its_crate`, `message_scan_skips_test_modules_with_any_test_cfg`). - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian, Georgian and Catalan ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). 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`. From 5b4e613d0b243c3fa10edb925eccd3378c27f61c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 16:03:37 -0800 Subject: [PATCH 04/44] 3D: colored-light facets close their shared edges over an underlay Under a colored light, Inflate's facets were antialiased one by one. Where two facets met, their edge pixels added up to less than full coverage. The solid-color composite then showed its black channel backdrop there, so internal edges drew darker at 1:1, and the per-facet gradient art let the background through. - The solid-color composite draws, in each channel, an underlay of the facets with 2 pt round seams between the black backdrop and the facets themselves. - The per-facet routes put each run of consecutive faces of one surface over an underlay of its own faces, clipped to the union of the faces and their seams. Runs that carry a gradient's opacity mask are sealed with that mask. A face clip's footprint keeps only the outer contours of its seam stroke. - The paint-copy allowance for cut faces counts 12 copies per facet. At 1:1, inside the surface each color channel of an opaque fill is within 3 levels of the same art drawn at 3x and averaged down, and every pixel is opaque. Before, 2,267 channels differed by up to 10 levels on the solid Inflate, and 1,088 pixels of the gradient Inflate were below full alpha. Under white light, the rendered pixels, the SVG text, the PDF read back and the expanded art of Inflate, Extrude, Rotate and Revolve are the same as before. Docs: docs/three-d.md (Seams), docs/target-app-parity.md. --- crates/effects/src/lighting.rs | 141 +++++++++++++---- crates/effects/src/solids.rs | 109 +++++++++++-- crates/effects/src/solids/tests.rs | 85 +++++++++- crates/engine/tests/it/three_d_light_color.rs | 145 ++++++++++++++++++ docs/target-app-parity.md | 2 +- docs/three-d.md | 7 +- 6 files changed, 448 insertions(+), 41 deletions(-) diff --git a/crates/effects/src/lighting.rs b/crates/effects/src/lighting.rs index 8592776a95..378bfa5621 100644 --- a/crates/effects/src/lighting.rs +++ b/crates/effects/src/lighting.rs @@ -136,8 +136,86 @@ pub(crate) fn facet_art( composed_art(id, path, paint, Ramp::Rgb(light), seam) } -/// Composite smooth solid-fill facets with one clip and three channel groups per surface. -/// Black channel backdrops keep shared edges opaque before the final coverage is applied. +/// The width of the seam stroke of a facet's underlay: a round stroke that reaches 1 pt past each +/// edge, over the pixels that the antialiased edges of two neighboring facets each cover only in +/// part at 1:1. A clip to the surface's outline removes what it paints past the surface. +pub(crate) const UNDERLAY_SEAM: f64 = 2.0; + +/// The area facet `path` and its round seam stroke `seam` wide paint together: the stroke's outer +/// contours and the facet, all wound one way with a positive area. The stroke's inner contours, +/// wound the other way, are left out: the facet covers them, and contours that cancel each other +/// leave partial coverage where a union of footprints meets at a pixel. +pub(crate) fn footprint(path: &vectorcraft_geom::BezPath, seam: f64) -> vectorcraft_geom::BezPath { + use kurbo::{PathEl, Shape}; + let stroke = kurbo::stroke(path.iter(), &kurbo::Stroke::new(seam).with_join(kurbo::Join::Round), &kurbo::StrokeOpts::default(), 0.05); + let sign = if stroke.area() < 0.0 { -1.0 } else { 1.0 }; + let mut footprint = vectorcraft_geom::BezPath::new(); + let mut contour = vectorcraft_geom::BezPath::new(); + for el in stroke.elements().iter().chain(std::iter::once(&PathEl::MoveTo(kurbo::Point::ZERO))) { + if matches!(el, PathEl::MoveTo(_)) && !contour.elements().is_empty() { + if contour.area() * sign >= 0.0 { + footprint.extend(contour.iter()); + } + contour = vectorcraft_geom::BezPath::new(); + } + contour.push(*el); + } + let mut boundary = path.clone(); + if boundary.area() * sign < 0.0 { + boundary = boundary.reverse_subpaths(); + } + footprint.extend(boundary.iter()); + if sign < 0.0 { footprint.reverse_subpaths() } else { footprint } +} + +/// Colored-light facets `faces` (in drawing order) drawn over an underlay of themselves, the same +/// facets grown to seams [`UNDERLAY_SEAM`] wide, all clipped once to the union of the facets' +/// footprints. Where two facets' antialiased edges meet, the underlay fills what they leave +/// uncovered with the neighbors' own colors, so the background does not show through. `None` unless +/// every face is a facet a surface draws: a path filled and stroked by its seam, or a facet composed +/// in a clip of its footprint, neither with an opacity mask. +pub(crate) fn sealed(id: vectorcraft_doc::NodeId, faces: &[std::sync::Arc]) -> Option { + use std::sync::Arc; + use vectorcraft_doc::{Appearance, Node, NodeKind}; + let mut outline = vectorcraft_geom::BezPath::new(); + let mut underlay = Vec::with_capacity(faces.len()); + for face in faces { + if face.extras().mask.is_some() { + return None; + } + match &face.kind { + NodeKind::Path { clipping: false, .. } => { + let path = face.path_data()?.to_bezpath(); + let mut seam = None; + let mut grown = (**face).clone(); + for item in &mut grown.appearance.items { + if let AppearanceItem::Stroke(s) = item { + seam = Some(seam.map_or(s.width, |w: f64| w.max(s.width))); + s.width = UNDERLAY_SEAM; + } + } + outline.extend(footprint(&path, seam?).iter()); + underlay.push(Arc::new(grown)); + } + NodeKind::Group { children, clip: true } if children.len() == 2 => { + let clip = children.first()?.path_data()?.to_bezpath(); + let wider = Node::path(id, vectorcraft_geom::PathData::from_bezpath(&footprint(&clip, UNDERLAY_SEAM)), Appearance::default()); + let colors = children.get(1)?.clone(); + outline.extend(clip.iter()); + underlay.push(Arc::new(Node::new(id, NodeKind::Group { children: vec![Arc::new(wider), colors], clip: true }))); + } + _ => return None, + } + } + let clip = Node::path(id, vectorcraft_geom::PathData::from_bezpath(&outline), Appearance::default()); + underlay.extend(faces.iter().cloned()); + Some(Node::new(id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(Node::group(id, underlay))], clip: true })) +} + +/// Composite smooth solid-fill facets with one clip and three channel groups per surface. In each +/// channel, an underlay of the facets with wide seams lies under the facets themselves, so a +/// shared edge is opaque in that channel's own light before the channels are screened together; +/// the black backdrop shows only outside the surface, which the clip removes. pub(crate) fn solid_surface(id: vectorcraft_doc::NodeId, facets: &[vectorcraft_three_d::Facet], paint: &Paint) -> vectorcraft_doc::Node { use kurbo::Shape; use std::sync::Arc; @@ -167,20 +245,36 @@ pub(crate) fn solid_surface(id: vectorcraft_doc::NodeId, facets: &[vectorcraft_t if let Some(value) = color.get_mut(channel) { *value = 1.0; } + let paints: Vec = facets + .iter() + .map(|face| { + if let Some(ramp) = face.rgb_gradient { + smooth_paint(vectorcraft_three_d::LightGradient { diffuse: ramp.channels[channel], ambient: 0.0, color }, Color::WHITE) + } else { + Paint::solid(shade(Color::WHITE, color.map(|c| c * face.illumination[channel]))) + } + }) + .collect(); + let draw = |seam: fn(&vectorcraft_three_d::Facet) -> f64| -> Vec> { + facets + .iter() + .zip(&paints) + .map(|(face, paint)| { + let edge = seam_stroke(paint.clone(), seam(face)); + Arc::new(Node::path( + id, + face.path(), + Appearance { + items: vec![AppearanceItem::Fill(FillLayer::new(paint.clone())), AppearanceItem::Stroke(edge)], + ..Default::default() + }, + )) + }) + .collect() + }; let mut children = vec![Arc::new(cover(Paint::solid(Color::BLACK)))]; - children.extend(facets.iter().map(|face| { - let paint = if let Some(ramp) = face.rgb_gradient { - smooth_paint(vectorcraft_three_d::LightGradient { diffuse: ramp.channels[channel], ambient: 0.0, color }, Color::WHITE) - } else { - Paint::solid(shade(Color::WHITE, color.map(|c| c * face.illumination[channel]))) - }; - let edge = seam_stroke(paint.clone(), (face.thickness() / 2.0).clamp(0.05, 0.75)); - Arc::new(Node::path( - id, - face.path(), - Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(paint)), AppearanceItem::Stroke(edge)], ..Default::default() }, - )) - })); + children.extend(draw(|_| UNDERLAY_SEAM)); + children.extend(draw(|face| (face.thickness() / 2.0).clamp(0.05, 0.75))); let mut group = Node::group(id, children); group.isolate = true; group.blend = BlendMode::Screen; @@ -228,24 +322,19 @@ pub(crate) fn opaque_paint(paint: &Paint) -> (Paint, Option) { } fn composed_art(id: vectorcraft_doc::NodeId, path: &vectorcraft_geom::PathData, paint: &Paint, light: Ramp, seam: f64) -> vectorcraft_doc::Node { - use kurbo::Shape; use std::sync::Arc; use vectorcraft_doc::color::BlendMode; use vectorcraft_doc::{Appearance, Node, NodeKind, OpacityMask}; use vectorcraft_geom::{Rect, shapes}; let (base, mask) = opaque_paint(paint); // Clip once to the union of this single-contour face and its round seam stroke. - // Align their winding so the nonzero fill adds coverage instead of cancelling it. - let mut boundary = path.to_bezpath(); - let mut footprint = kurbo::stroke(boundary.iter(), &kurbo::Stroke::new(seam).with_join(kurbo::Join::Round), &kurbo::StrokeOpts::default(), 0.05); - if boundary.area() * footprint.area() < 0.0 { - boundary = boundary.reverse_subpaths(); - } - footprint.extend(boundary.iter()); - let clip = Node::path(id, vectorcraft_geom::PathData::from_bezpath(&footprint), Appearance::default()); + let clip = Node::path(id, vectorcraft_geom::PathData::from_bezpath(&footprint(&path.to_bezpath(), seam)), Appearance::default()); // Opaque rectangles cover the complete seam stroke, without separately antialiasing - // either ramp. Opaque sources need only vector clipping, not full-frame opacity masks. - let bounds = path.bounds().unwrap_or(Rect::ZERO).inflate(1.0, 1.0); + // either ramp. Opaque sources need only vector clipping, not full-frame opacity masks. They + // reach at least half a point past the seam stroke grown for an underlay ([`sealed`]), so + // their own edges stay outside either clip. + let margin = seam.max(1.0) + UNDERLAY_SEAM / 2.0; + let bounds = path.bounds().unwrap_or(Rect::ZERO).inflate(margin, margin); let cover = |paint| Node::path(id, shapes::rectangle(bounds), Appearance::basic(paint, Paint::None, 0.0)); let mut illumination = match light { Ramp::Single(light) => cover(smooth_paint(light, Color::WHITE)), diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index a68ff9529f..022dadfce9 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -599,11 +599,26 @@ fn paint_ref(item: &AppearanceItem) -> &Paint { } } -/// Paint copies `surface` makes for `facets`: two per facet (fill and seam stroke), six when colored -/// lights give each facet derived paints (`lighting::solid_surface`). +/// Are `facets` drawn over an underlay of themselves ([`crate::lighting::sealed`], +/// [`crate::lighting::solid_surface`])? Under colored lights, when any facet takes a smooth light +/// ramp. +fn underlaid(facets: &[Facet], colored: bool) -> bool { + colored && facets.iter().any(|f| f.rgb_gradient.is_some()) +} + +/// The most paint copies `surface` makes for one facet ([`facet_copies`]). +const MAX_FACET_COPIES: usize = 12; + +/// Paint copies `surface` makes for `facets`: two per facet (fill and seam stroke), twice that when +/// the facets lie over an underlay of themselves, and [`MAX_FACET_COPIES`] when colored lights also +/// give each facet derived paints in three channels (`lighting::solid_surface`). fn facet_copies(facets: &[Facet], paint: &Paint, colored: bool) -> usize { - let derived = colored && paint.color().is_some() && facets.iter().any(|f| f.rgb_gradient.is_some()); - facets.len().saturating_mul(if derived { 6 } else { 2 }) + let per_facet = match (underlaid(facets, colored), paint.color().is_some()) { + (true, true) => MAX_FACET_COPIES, + (true, false) => 4, + (false, _) => 2, + }; + facets.len().saturating_mul(per_facet) } /// The area `item` paints on `path`, and the rule that fills it: the path itself for a fill, the @@ -789,14 +804,14 @@ fn rects_overlap(a: Rect, b: Rect) -> bool { /// How many faces [`cut_stroke_faces`] may add to solids painted by `items` that hold `faces` faces /// and whose paint copies take `paint_bytes` so far: as many as keep the faces within -/// [`MAX_OBJECT_FACES`] and the copies within the allowance, each added face counted as six copies -/// of the costliest stroke's paint. +/// [`MAX_OBJECT_FACES`] and the copies within the allowance, each added face counted as +/// [`MAX_FACET_COPIES`] copies of the costliest stroke's paint. fn cut_room<'a>(items: impl Iterator, faces: usize, paint_bytes: usize) -> usize { let per_face = items .filter(|item| matches!(item, AppearanceItem::Stroke(_))) .map(|item| { let mut copies = 0usize; - crate::revolve::paint_copies_add(&mut copies, 6, paint_ref(item)); + crate::revolve::paint_copies_add(&mut copies, MAX_FACET_COPIES, paint_ref(item)); copies }) .max() @@ -1606,6 +1621,10 @@ fn surfaces_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile placed_graphics(doc, n.id, (&fx.id, &p), bounds, &filled, masks) }; let members: Vec = solids.iter().map(|s| s.4).collect(); + // Which surfaces lie over an underlay of their facets, in one order or whole + // ([`in_one_order`], [`underlaid_surface`]). + let seals: Vec = solids.iter().map(|(.., f, _)| underlaid(f, colored)).collect(); + let seal = |range: std::ops::Range| seals.get(range).is_some_and(|s| s.iter().any(|s| *s)); let surfaces: Vec = solids .into_iter() .map(|(item, member, scopes, f, shape)| { @@ -1637,19 +1656,37 @@ fn surfaces_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile run.push(s); } let flat = run.iter().all(|s| plain(s) && s.1.opacity >= 1.0 && s.1.blend == BlendMode::Normal && s.1.extras().mask.is_none()); + let range = first..first + run.len(); if ordered(member) && flat { let scopes = run.first().map_or(&[][..], |s| s.2); - out.push((scopes, Arc::new(Node::group(n.id, in_one_order(n.id, run, first, &arts, sealed))))); + let seals = seals.get(range).unwrap_or_default(); + out.push((scopes, Arc::new(Node::group(n.id, in_one_order(n.id, run, first, &arts, sealed, seals))))); } else { - out.extend(with_graphics(run.into_iter().map(|(_, s, scopes, _)| (scopes, Arc::new(s))).collect(), first, &arts)); + let run = run.into_iter().zip(range).map(|((_, s, scopes, _), k)| (scopes, Arc::new(underlaid_surface(n.id, s, seal(k..k + 1))))); + out.extend(with_graphics(run.collect(), first, &arts)); } } return Some(masked(n.id, out)); } if !opaque || !surfaces.iter().all(plain) { + let surfaces = + surfaces.into_iter().enumerate().map(|(k, (d, s, scopes, e))| (d, underlaid_surface(n.id, s, seal(k..k + 1)), scopes, e)).collect(); return Some(whole_surfaces(surfaces, &arts).into_iter().map(|(_, s)| s).collect()); } - Some(vec![Arc::new(Node::group(n.id, in_one_order(n.id, surfaces, 0, &arts, sealed)))]) + Some(vec![Arc::new(Node::group(n.id, in_one_order(n.id, surfaces, 0, &arts, sealed, &seals)))]) +} + +/// A plain `surface` ([`surface`]: a group of its facets) over an underlay of its facets when +/// `seal`, keeping the surface's opacity, blend mode and opacity mask. +fn underlaid_surface(id: NodeId, surface: Node, seal: bool) -> Node { + let NodeKind::Group { children, clip: false } = &surface.kind else { return surface }; + let Some(mut out) = seal.then(|| crate::lighting::sealed(id, children)).flatten() else { return surface }; + out.opacity = surface.opacity; + out.blend = surface.blend; + if let Some(mask) = surface.extras().mask.clone() { + out.extras_mut().mask = Some(mask); + } + out } /// The faces of `surfaces` (plain groups of their faces, [`surfaces_for`]'s from index `first`) in @@ -1657,7 +1694,16 @@ fn surfaces_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile /// surfaces right after the last face it is drawn on, of the surfaces that share it. A surface in a gradient with transparent stops /// carries an opacity mask: its faces go in runs ([`in_runs`]), each carrying the mask, and each of /// its graphics carries the mask too ([`with_graphics`]), so a graphic is hidden where its cap is. -fn in_one_order(id: NodeId, surfaces: Vec>, first: usize, arts: &[(usize, PlanarFace, Arc)], sealed: bool) -> Vec> { +/// The faces of the surfaces that `seals` marks lie over underlays of themselves +/// ([`underlaid_runs`], [`underlaid_surface`]). +fn in_one_order( + id: NodeId, + surfaces: Vec>, + first: usize, + arts: &[(usize, PlanarFace, Arc)], + sealed: bool, + seals: &[bool], +) -> Vec> { let masks: Vec>> = surfaces.iter().map(|(_, s, _, _)| s.extras().mask.clone()).collect(); let arts: Vec<&(usize, PlanarFace, Arc)> = arts.iter().filter(|(at, ..)| (first..first + surfaces.len()).contains(at)).collect(); let (mut placed, mut faces, mut ends) = (vec![], vec![], vec![]); @@ -1696,7 +1742,46 @@ fn in_one_order(id: NodeId, surfaces: Vec>, first: usize, arts: (masks.len(), art) })); } - if masks.iter().any(Option::is_some) { in_runs(id, drawn, &masks) } else { drawn.into_iter().map(|(_, face)| face).collect() } + let takes_underlay = |surface: usize| seals.get(surface) == Some(&true); + if masks.iter().any(Option::is_some) { + // The faces of a surface without a mask are sealed run by run before the masked surfaces' + // faces go in runs of their own, each holding one surface's faces, sealed with its mask. + let unmasked = underlaid_runs(id, drawn, |s| takes_underlay(s) && masks.get(s).is_some_and(Option::is_none)); + return in_runs(id, unmasked, &masks) + .into_iter() + .map(|run| match &run.kind { + NodeKind::Group { clip: false, .. } if seals.iter().any(|s| *s) && run.extras().mask.is_some() => { + Arc::new(underlaid_surface(id, (*run).clone(), true)) + } + _ => run, + }) + .collect(); + } + underlaid_runs(id, drawn, takes_underlay).into_iter().map(|(_, face)| face).collect() +} + +/// `faces` in their drawing order, each with the index of its surface. Each run of consecutive +/// faces of one surface that `seal` takes lies over an underlay of its own faces, clipped to their +/// outline ([`crate::lighting::sealed`]), so no underlay reaches under another surface's faces or a +/// graphic drawn between them. A sealed run keeps its surface's index. +fn underlaid_runs(id: NodeId, faces: Vec<(usize, Arc)>, seal: impl Fn(usize) -> bool) -> Vec<(usize, Arc)> { + let mut out = Vec::with_capacity(faces.len()); + let mut run: Vec> = vec![]; + let mut faces = faces.into_iter().peekable(); + while let Some((surface, face)) = faces.next() { + run.push(face); + if faces.peek().is_some_and(|(next, _)| *next == surface) { + continue; + } + match seal(surface).then(|| crate::lighting::sealed(id, &run)).flatten() { + Some(sealed) => { + out.push((surface, Arc::new(sealed))); + run.clear(); + } + None => out.extend(run.drain(..).map(|face| (surface, face))), + } + } + out } /// `faces` in their back-to-front order, each with the index of its surface, where some surfaces diff --git a/crates/effects/src/solids/tests.rs b/crates/effects/src/solids/tests.rs index c825159dda..ccf87e37a0 100644 --- a/crates/effects/src/solids/tests.rs +++ b/crates/effects/src/solids/tests.rs @@ -535,6 +535,89 @@ fn a_colored_inflate_in_gradients_draws_its_faces_in_one_order() { assert_eq!(drawn(&solid, Some(colored)).0, 2); } +/// Under a colored light, the faces of an Inflate in gradients, in one order, lie over underlays +/// run by run: each run of consecutive faces of one surface (the fill's or the dashed stroke's) +/// over an underlay of its own faces, so no underlay reaches under the other surface's faces. +#[test] +fn a_colored_inflate_in_gradients_lies_over_underlays_of_each_surfaces_runs() { + use vectorcraft_doc::color::{Gradient, GradientKind, GradientPaint, GradientStop}; + let gradient = |c: Color| { + Paint::Gradient(Box::new(GradientPaint::new(Gradient::new( + GradientKind::Linear, + vec![GradientStop::new(0.0, c), GradientStop::new(1.0, Color::WHITE)], + )))) + }; + let mut n = stroked(&letter_c()); + for item in &mut n.appearance.items { + match item { + AppearanceItem::Fill(f) => f.paint = gradient(RED), + AppearanceItem::Stroke(s) => { + s.paint = gradient(BLUE); + s.dash = Some(vectorcraft_doc::Dash { pattern: vec![12.0, 12.0], offset: 0.0, align_corners: false }); + } + } + } + let p = json!({"rotationX": -20, "rotationY": -40, "rotationZ": 0, "depth": 60}); + let cut = drawn_parts(&n, INFLATE, &p).len(); + let mut lit = p.clone(); + lit["lights"] = json!([{"color": "#ff8080"}]); + n.appearance.effects.push(fx(INFLATE, lit)); + // Which surface a face belongs to, by the first stop of its paint (a composed face's own paint, + // or a path face's shaded one): red for the fill's, blue for the stroke's. + fn surface_of(face: &Node) -> Option { + let item = match &face.kind { + NodeKind::Path { .. } => face.appearance.items.first()?, + _ => face.children()?.get(1)?.children()?.first()?.appearance.items.first()?, + }; + let Paint::Gradient(g) = item.paint() else { return None }; + let [r, _, b] = g.gradient.stops.first()?.color.to_rgb(); + Some(if r > b { RED } else { BLUE }) + } + // The faces in `order`, and the surface of each run over an underlay, looking into the groups + // that carry a surface's opacity mask. + fn runs(order: &[Arc], faces: &mut usize, surfaces: &mut Vec) { + for child in order { + let NodeKind::Group { children, clip } = &child.kind else { + assert!(surface_of(child).is_some(), "a face"); + *faces += 1; + continue; + }; + if !clip { + assert!(child.extras().mask.is_some(), "a masked run"); + runs(children, faces, surfaces); + continue; + } + // A composed face isolates its colors; a run's faces are not isolated together. + if children[1].isolate { + assert!(surface_of(child).is_some(), "a composed face"); + *faces += 1; + continue; + } + let stack = children[1].children().unwrap(); + let (under, own) = stack.split_at(stack.len() / 2); + assert_eq!(under.len(), own.len(), "one underlay face for each face"); + let colors: Vec<_> = own.iter().map(|f| surface_of(f).unwrap()).collect(); + assert!(colors.iter().all(|c| *c == colors[0]), "a run holds one surface's faces"); + surfaces.push(colors[0]); + *faces += own.len(); + } + } + let (mut faces, mut surfaces) = (0, vec![]); + runs(solid_art(&n).unwrap().children().unwrap()[0].children().unwrap(), &mut faces, &mut surfaces); + assert!(faces >= cut, "{faces} faces, {cut} under a white light, whose cells are not triangulated"); + assert!(surfaces.contains(&RED) && surfaces.contains(&BLUE), "{surfaces:?}"); + // With transparent stops in the fill's gradient, the fill's faces go in masked runs; the + // stroke's faces, without a mask, still lie over underlays of their own runs. + if let Some(AppearanceItem::Fill(f)) = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))) + && let Paint::Gradient(g) = &mut f.paint + { + g.gradient.stops.iter_mut().for_each(|s| s.opacity = 0.5); + } + let (mut faces, mut surfaces) = (0, vec![]); + runs(solid_art(&n).unwrap().children().unwrap()[0].children().unwrap(), &mut faces, &mut surfaces); + assert!(faces >= cut && surfaces.contains(&RED) && surfaces.contains(&BLUE), "{faces} faces, {surfaces:?}"); +} + /// A face of a stroke that leaves gaps passing through the plane of its fill's wall where they /// overlap is cut along the line where their depths meet, into the piece in front of the wall and /// the piece behind it; a face on one side of the wall, another shape's stroke, a covering stroke @@ -622,7 +705,7 @@ fn cutting_stays_within_the_face_limit_and_the_paint_allowance() { let items = stroked(&letter_c()).appearance.items.clone(); let stroke = items.iter().find(|i| matches!(i, AppearanceItem::Stroke(_))).unwrap(); let mut one = 0usize; - crate::revolve::paint_copies_add(&mut one, 6, paint_ref(stroke)); + crate::revolve::paint_copies_add(&mut one, MAX_FACET_COPIES, paint_ref(stroke)); let max = crate::revolve::MAX_PAINT_COPIES; assert_eq!(cut_room(items.iter(), 10, 0), (MAX_OBJECT_FACES - 10).min(max / one)); assert_eq!(cut_room(items.iter(), MAX_OBJECT_FACES + 5, 0), 0); diff --git a/crates/engine/tests/it/three_d_light_color.rs b/crates/engine/tests/it/three_d_light_color.rs index 91ce875796..8723e8d0a3 100644 --- a/crates/engine/tests/it/three_d_light_color.rs +++ b/crates/engine/tests/it/three_d_light_color.rs @@ -481,6 +481,151 @@ fn solid_gradient_opacity_is_applied_once_even_without_shading() { } } +/// How `image`, `d` drawn at 1:1 on 400 × 400 pixels, differs from `d` drawn at 3x and averaged +/// down, on the pixels inside the art: those whose 3x average has some alpha, the same as every +/// pixel within 3 of them. Left out too are pixels where the 3x average bends sharply (a channel's +/// second difference across the pixel, along x or y, over 4 levels), such as the rim where a cap +/// meets a body: a gradient sampled at each pixel's center matches an area average only where the +/// light changes close to linearly across the pixel. Returns (pixels measured, premultiplied RGB +/// channels more than 3 levels off, the largest RGB difference, alphas off: below 255 where the 3x +/// average is opaque, otherwise more than 3 levels off, the first few pixels off). +fn seams_against_supersampled(d: &Document, image: &vectorcraft_render::Rendered) -> (usize, usize, u8, usize, Vec) { + let fine = vectorcraft_render::Renderer::new().render( + d, + 1200, + 1200, + kurbo::Affine::scale(3.0), + &vectorcraft_render::RenderOptions { background: None, artboards: false, ..Default::default() }, + ); + let average = fine.area_resized(400, 400).unwrap(); + let (mut measured, mut color, mut max, mut holes) = (0, 0, 0, 0); + let mut worst = vec![]; + for y in 3..397 { + for x in 3..397 { + let q = average.pixel(x, y); + if q[3] == 0 || !(y - 3..=y + 3).all(|y| (x - 3..=x + 3).all(|x| average.pixel(x, y)[3] == q[3])) { + continue; + } + let bend = |a: [u8; 4], b: [u8; 4]| (0..3).any(|c| (i32::from(a[c]) + i32::from(b[c]) - 2 * i32::from(q[c])).abs() > 4); + if bend(average.pixel(x - 1, y), average.pixel(x + 1, y)) || bend(average.pixel(x, y - 1), average.pixel(x, y + 1)) { + continue; + } + measured += 1; + let p = image.pixel(x, y); + for c in 0..3 { + let diff = p[c].abs_diff(q[c]); + max = max.max(diff); + color += usize::from(diff > 3); + } + let hole = if q[3] == 255 { p[3] < 255 } else { p[3].abs_diff(q[3]) > 3 }; + holes += usize::from(hole); + if (p.into_iter().zip(q).any(|(a, b)| a.abs_diff(b) > 3) || hole) && worst.len() < 4 { + worst.push(format!("({x},{y}) {p:?} vs {q:?}")); + } + } + } + (measured, color, max, holes, worst) +} + +/// Colored lights on smooth 3D surfaces, compared at 1:1 with a 3x render averaged down, on every +/// pixel inside the art ([`seams_against_supersampled`]). A solid fill draws as one batched surface; +/// a source gradient composes each facet on its own, and one with transparent stops applies their +/// opacity once over the surface. Inflate seen straight on, one layer of facets, has no alpha off, and +/// in opaque fills no RGB channel more than 3 levels off. Inflate turned so facets narrow to slivers +/// at the edges, and Revolve with a cap over its body, draw facets over other facets, and have no +/// alpha off. +#[test] +fn colored_light_surfaces_match_supersampling_at_internal_facet_edges() { + let lights = json!([ + {"color":"#ff0000","rotation":135,"height":35,"intensity":70}, + {"color":"#0000ff","rotation":-35,"height":25,"intensity":65} + ]); + let fills = |start: [u32; 2], end: [u32; 2]| { + [ + json!({"color":"#ffcc66"}), + json!({"gradient":{"kind":"linear","start":start,"end":end,"stops":[ + {"offset":0,"color":"#ffffff"},{"offset":1,"color":"#808080"} + ]}}), + json!({"gradient":{"kind":"linear","start":start,"end":end,"stops":[ + {"offset":0,"color":"#ffffff","opacity":0.6},{"offset":1,"color":"#808080","opacity":0.6} + ]}}), + ] + }; + let mut cases = vec![]; + for rotation in [(0, 0), (50, 35)] { + let (mut s, _) = setup("threeD.inflate"); + let change = json!({"index":0,"item":null,"params":{"ambient":10,"lights":lights,"rotationX":rotation.0,"rotationY":rotation.1}}); + s.execute("effect.setParams", &change).unwrap(); + cases.push((format!("inflate {rotation:?}"), s, fills([100, 150], [200, 150]), rotation == (0, 0))); + } + let (mut s, _) = capped_profile(); + s.execute("effect.setParams", &json!({"index":0,"item":null,"params":{"ambient":10,"lights":lights}})).unwrap(); + cases.push(("revolve".into(), s, fills([80, 200], [240, 200]), false)); + let mut failures = vec![]; + for (name, mut s, fills, one_layer) in cases { + for fill in fills { + s.execute("paint.setFill", &fill).unwrap(); + let image = render(doc(&s)); + let (measured, color, max, holes, worst) = seams_against_supersampled(doc(&s), &image); + assert!(measured > 2_000, "{name} {fill}: {measured} pixels inside"); + let opaque = !fill.to_string().contains("opacity"); + if holes > 0 || (one_layer && opaque && color > 0) { + failures.push(format!( + "{name} {fill}: of {measured} pixels, {color} channels off by more than 3 (at most {max}), {holes} alphas off: {worst:?}" + )); + } + } + } + assert!(failures.is_empty(), "{failures:#?}"); +} + +/// The closed facet edges of colored-light surfaces survive SVG and PDF, read back and drawn at +/// 1:1, and Expand Appearance: inside the art, every pixel stays opaque and within 6 levels of the +/// live art. +#[test] +fn colored_light_seams_stay_closed_in_svg_pdf_and_expansion() { + let lights = json!([ + {"color":"#ff0000","rotation":135,"height":35,"intensity":70}, + {"color":"#0000ff","rotation":-35,"height":25,"intensity":65} + ]); + for fill in [ + json!({"color":"#ffcc66"}), + json!({"gradient":{"kind":"linear","start":[100,150],"end":[200,150],"stops":[ + {"offset":0,"color":"#ffffff"},{"offset":1,"color":"#808080"} + ]}}), + ] { + let (mut s, _) = setup("threeD.inflate"); + s.execute("effect.setParams", &json!({"index":0,"item":null,"params":{"ambient":10,"lights":lights}})).unwrap(); + s.execute("paint.setFill", &fill).unwrap(); + let live = render(doc(&s)); + let svg = s.execute("document.serialize", &json!({"format":"svg"})).unwrap()["text"].as_str().unwrap().to_owned(); + assert!(svg.contains(" Date: Fri, 13 Nov 2026 16:20:17 -0800 Subject: [PATCH 05/44] Docs: Japanese message coverage is partial, with 38 rows left to open pull requests The Japanese row of docs/localization-parity.md marks its messages as partial and names the 38 messages the untranslated report lists without a row: the 36 composite font errors that open #7211 translates, "select a path" (open #7214) and the justification order error (open #7116). It also names the three rows that do not match their message yet and why. The Japanese line in docs/target-app-parity.md says the catalog has a row for each reported message except those 38. --- docs/localization-parity.md | 2 +- docs/target-app-parity.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 1df39336d0..ff5c7c7cb7 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -34,7 +34,7 @@ Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker | French | `fr` | 4,020 / 4,020 (100%) | all | Latin | no | full | — (review) | | Portuguese | `pt-br` | 6,106 catalog entries (counted 2026-10-31 on `main` at `d26dd933a`): menus, `tl!` literals and messages complete when counted; 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 (3,421 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 38 missing messages for it) | CJK UI font as above; Japanese IME; vertical type partial; Japanese crop marks and composition settings | no | full | — (review) | +| Japanese | `ja` | 4,004 / 4,020 (99.6%) | partial: 3,421 `@msg` and 48 `@msgplural` rows (counted 2026-11-13). The untranslated report lists 38 messages without a row: the 36 composite font errors that open #7211 translates, `select a path` (open #7214) and ``{name}`: Minimum must not exceed Desired, nor Desired Maximum`` (open #7116). Three rows do not match their message yet: `Template::parse` returns no template for a message with two placeholders in a row, so the rows for ``unknown parameter `{within}{key}`; {why}`` and `{name} must be {lo}–{hi}{unit} (got {v})` are not used, and the row for `{k} has control characters{_1}` does not match the multi-line File Info error, whose last placeholder is empty | 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 (602 of 604 message rows; landed 2026-10-10) | 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) | | 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) | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 72ee4a28f4..d850450b6e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -875,7 +875,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`). - The status bar translates the limits a plug-in run can exceed (its instruction, time and memory budgets and its output size) and the error of a plug-in removal that leaves the plug-in's grant in the plug-in store, in every language that translates every message. The untranslated-strings report also lists messages that an error or a status takes from a `&str` constant (`plugin_limit_errors_are_translated`, `message_scan_reads_messages_held_in_constants`). - The status bar translates the plug-in host's other errors (manifest, module and run checks, the output an importer, exporter or effect returns, and the approval a plug-in's permissions need) in every language that translates every message. A JSON object or array inside a message stays as written, and a message that wraps another one, such as `plug-in manifest: {0}`, is matched by its own row before a row that starts with a placeholder. The reasons inside these errors (what is wrong with a parameter's spec or dialog keys, the value a parameter needs, what is wrong with an object, stroke, paint or effect in a plug-in's output) are translated with them, and the untranslated-strings report lists the reasons the plug-in host's error helpers take (`plugin_messages_are_translated_in_every_complete_catalog`, `plugin_errors_translate_with_their_values`, `plugin_error_reasons_are_translated_with_the_error`, `message_scan_reads_plugin_helpers`, `templates_that_start_with_text_match_first`). - - The Japanese catalog (`crates/ui-egui/src/i18n/ja.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,421 `@msg` rows in all. The report lists 38 Japanese messages: the composite font, select-a-path and justification order errors that open #7211, #7214 and #7116 translate. The status bar does not use the rows for ``unknown parameter `{within}{key}`; {why}`` and `{name} must be {lo}–{hi}{unit} (got {v})`, because a message with two placeholders in a row cannot be matched (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`). The row for `{k} has control characters{_1}` does not match the File Info error for a multi-line field, where the last placeholder is empty. A native speaker has not reviewed the wording. + - The Japanese catalog (`crates/ui-egui/src/i18n/ja.tsv`) has 3,421 `@msg` rows: a row for each status and error message the untranslated report (`untranslated_report`) finds in the sources, except 38. The report lists those 38 for Japanese: the 36 composite font errors that open #7211 translates, `select a path` (open #7214) and the justification order error (open #7116). The status bar does not use the rows for ``unknown parameter `{within}{key}`; {why}`` and `{name} must be {lo}–{hi}{unit} (got {v})`, because a message with two placeholders in a row cannot be matched (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`). The row for `{k} has control characters{_1}` does not match the File Info error for a multi-line field, where the last placeholder is empty. A native speaker has not reviewed the wording. - The status bar translates the errors a script's `File` and `Folder` objects, `vc.run`, `$.evalFile`, the `$` object's properties and functions, `alert`, `confirm` and `prompt`, `vc.document` and `vc.log` raise, the limits a run can pass, and the errors of Drop Script Engine, and the line for a run that fails (a syntax error, a thrown error, a limit, a stop or an internal error, with the script's name, the line and column, the included file it is in and the files left written), in every language that translates every message. The script's name, paths, numbers, command ids and the text a script throws itself are shown as they are. A failure with no line and column, such as an error in code `eval` compiled, is shown with one colon after the script's name (`an_error_without_a_place_names_the_script_and_kind`). A message template that names a value twice matches only when both places hold the same text, so the line for a failed run is not taken for the message inside it (`a_repeated_placeholder_matches_the_same_text`). The untranslated-strings report also lists the messages the script host's `fail`, `value` and `over` helpers take and the errors the script interpreter throws, and a constant's text comes from the file or crate that declares it, so the Scripts menu's headers are not listed as messages (`script_errors_are_translated`, `script_host_errors_are_translated`, `message_scan_reads_script_interpreter_errors`, `message_scan_reads_constants_of_the_file_and_its_crate`, `message_scan_skips_test_modules_with_any_test_cfg`). - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian, Georgian and Catalan ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). 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`. From 0cb2b435a80e16a96f1460a4763d3b77c18e78c1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 16:23:32 -0800 Subject: [PATCH 06/44] Live Paint: a Live Paint Selection double-click selects the connected faces or edges A double-click with the Live Paint Selection tool on a face now selects that face and the faces of its group that no stroked edge parts from it: the selection floods across edges that are hidden or draw no stroke, and hidden faces are left out. On an edge (Alt-double-click, or a double-click with Select Fills off) it selects the edge and the edges of its group joined to it end to end that draw with the same stroke paint. With Shift held, the faces or edges reached leave the selection or join it. The tool options, locks and isolation mode apply as they do to a click. The reach is computed by two read-only functions in the builder lookup module, flood_faces and connected_edges, so the Live Paint commands can use the same rules. An edge's stroke paint is the paint of its topmost stroke that draws. Face outlines are matched to edges within the planar map's precision, and edges join only at the ends of their subpaths. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/tests/build.rs | 31 ++ crates/tools/src/builder.rs | 6 +- crates/tools/src/builder/live_paint.rs | 33 +- crates/tools/src/builder/lookup.rs | 343 +++++++++++++++ crates/tools/src/builder/tests.rs | 577 +++++++++++++++++++++++++ docs/mcp.md | 10 + docs/target-app-parity.md | 1 + 7 files changed, 995 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/tests/build.rs b/crates/engine/src/tests/build.rs index cb61ac1b9e..0791ce538d 100644 --- a/crates/engine/src/tests/build.rs +++ b/crates/engine/src/tests/build.rs @@ -3307,6 +3307,37 @@ fn live_paint_bucket_alt_shift_gradient_samples_active_stroke_only() { assert!(matches!(node(&s, src).appearance.fill_paint(), Paint::Gradient(_))); } +#[test] +fn live_paint_selection_double_click_selects_the_faces_no_stroked_edge_parts() { + let mut s = session(); + let g = live(&mut s); + // The edge between the middle and right faces draws no stroke. + s.execute("livePaint.strokeEdge", &json!({"group": g.0, "point": [100.0, 50.0], "none": true})).unwrap(); + let v = ViewInfo::default(); + s.select_tool("livePaintSelection", v).unwrap(); + let gn = node(&s, g); + let face = |x: f64| vectorcraft_tools::builder::face_at(&gn, Point::new(x, 50.0)).unwrap().id; + let before = s.doc().unwrap().doc.clone(); + let double_click = |s: &mut Session, x: f64, m: Mods| { + for kind in [PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick] { + s.pointer(&PointerEvent::new(kind, x, 50.0).with_mods(m), v).unwrap(); + } + let mut ids = s.doc().unwrap().selection.objects.clone(); + ids.sort(); + ids + }; + let sorted = |mut ids: Vec| { + ids.sort(); + ids + }; + assert_eq!(double_click(&mut s, 125.0, Mods::default()), sorted(vec![face(75.0), face(125.0)])); + // The stroked edge at x = 50 parts the left face from the others. + assert_eq!(double_click(&mut s, 25.0, Mods::default()), vec![face(25.0)]); + // Shift-double-click adds the faces reached to the selection. + assert_eq!(double_click(&mut s, 75.0, Mods { shift: true, ..Default::default() }), sorted(vec![face(25.0), face(75.0), face(125.0)])); + assert_eq!(s.doc().unwrap().doc, before, "selecting changes no paint"); +} + #[test] fn live_paint_selection_marquee_selects_what_it_touches() { let mut s = session(); diff --git a/crates/tools/src/builder.rs b/crates/tools/src/builder.rs index 2190d34180..0a6cf583cf 100644 --- a/crates/tools/src/builder.rs +++ b/crates/tools/src/builder.rs @@ -41,9 +41,9 @@ pub use live_paint::{ }; use live_paint::{check_live_paint_options, lp_color, lp_rgb_color}; pub use lookup::{ - EDGE_NAME, FACE_NAME, Face, LIVE_PAINT_NAME, LIVE_PAINT_PREFIX, SOURCES_NAME, builder_members, drawn_outlines, edge_near, edges, face_at, faces, - is_live_paint, leaf_shapes, live_paint_at, live_paint_takes, node_outline, outline_snap, path_contains, shape_builder_shapes, shapes_for, - sources, visible_edge_near, visible_face_at, + EDGE_NAME, FACE_NAME, Face, LIVE_PAINT_NAME, LIVE_PAINT_PREFIX, LP_JOIN_TOL, SOURCES_NAME, builder_members, connected_edges, drawn_outlines, + edge_near, edges, face_at, faces, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_takes, node_outline, outline_snap, + path_contains, shape_builder_shapes, shapes_for, sources, visible_edge_near, visible_face_at, }; pub use map::{ BuilderMap, HATCH, HIGHLIGHT_RED, Hit, MergeGeometry, MergeInput, PreparedMerge, Rest, Touched, hatch, maps_built, merge_geometry, merge_inputs, diff --git a/crates/tools/src/builder/live_paint.rs b/crates/tools/src/builder/live_paint.rs index 6b744e4435..15b5c55553 100644 --- a/crates/tools/src/builder/live_paint.rs +++ b/crates/tools/src/builder/live_paint.rs @@ -1,5 +1,7 @@ use super::cache::RegionCache; -use super::lookup::{is_live_paint, live_paint_at, live_paint_at_where, visible_edge_near, visible_face_at}; +use super::lookup::{ + connected_edges, flood_faces, is_live_paint, live_paint_at, live_paint_at_where, lp_stroke_paint, visible_edge_near, visible_face_at, +}; use super::map::{HIGHLIGHT_RED, sorted_roots}; use super::{ Action, Arc, BezPath, CLICK_TOL_PX, Cursor, Document, EDGE_NAME, FACE_NAME, FillRule, Mods, Node, NodeId, Overlay, Point, PointerEvent, @@ -471,7 +473,11 @@ pub const LP_SELECT_WIDTH_RANGE: (f64, f64) = (1.0, 20.0); /// Live Paint Selection: click selects a face (Shift adds); Alt-click selects an edge. A drag draws /// a marquee, and on release the faces and edges it touches ([`lp_parts_in`]) become the selection; /// with Shift held at the press, each of them leaves the selection if it is selected and joins it -/// otherwise. A click and a marquee change the selection on release, so Escape between the press +/// otherwise. A double-click on a face selects it and the faces of its group that are not parted +/// from it by a stroked edge; on an edge, it selects the edge and the edges joined to it end to end +/// that have its stroke paint ([`LivePaintSelectionTool::reach_at`]). With Shift held, each of them +/// leaves the selection if it is selected and joins it otherwise. +/// A click and a marquee change the selection on release, so Escape between the press /// and the release (which removes the marquee) leaves the selection as it was. The tool does not /// highlight or select the faces and edges of a Live Paint group that is locked, on a locked layer /// or on a locked artboard, or in isolation mode outside the isolated art. Its Tool Options @@ -558,6 +564,18 @@ impl LivePaintSelectionTool { }; (hit, cursor) } + + /// What a double-click at `p` selects, Alt held or not: on the face a click there picks + /// ([`LivePaintSelectionTool::picks_edge`]), that face and the faces of its group a flood from it + /// reaches across edges that are hidden or draw no stroke ([`flood_faces`]); on the edge a click + /// picks, that edge and the edges of its group joined to it end to end through edges that draw + /// with the stroke paint it draws with ([`connected_edges`], [`lp_stroke_paint`]). Empty where a click selects no face or edge. + fn reach_at(&self, cx: &ToolContext, p: Point, alt: bool) -> Vec { + let Some(edge) = self.picks_edge(alt) else { return vec![] }; + let Some((gid, Some(part))) = lp_part_at(cx.doc, p, cx.tol(4.0), edge, &|id| lp_selectable(cx, id)) else { return vec![] }; + let Some(group) = cx.doc.node(gid) else { return vec![] }; + if edge { connected_edges(group, part.id, &lp_stroke_paint(part)) } else { flood_faces(group, part.id) } + } } /// The faces (with `faces`) and edges (with `edges`) that marquee `r` touches, among the faces and @@ -679,7 +697,16 @@ impl Tool for LivePaintSelectionTool { (false, false) => vec![Action::Exec("select.set".into(), json!({ "ids": json_ids(&ids) }))], } } - _ => vec![], + PointerKind::DoubleClick => { + // The two clicks before it already selected the part under the pointer (or, with + // Shift, toggled it twice); the double-click selects the part with what it reaches. + let ids = self.reach_at(cx, p, ev.mods.alt); + match (ids.is_empty(), ev.mods.shift) { + (true, _) => vec![], + (false, true) => vec![Action::Exec("select.toggle".into(), json!({ "ids": json_ids(&ids) }))], + (false, false) => vec![Action::Exec("select.set".into(), json!({ "ids": json_ids(&ids) }))], + } + } } } diff --git a/crates/tools/src/builder/lookup.rs b/crates/tools/src/builder/lookup.rs index 5e85d9339d..f577732ade 100644 --- a/crates/tools/src/builder/lookup.rs +++ b/crates/tools/src/builder/lookup.rs @@ -104,6 +104,349 @@ fn nearest_edge<'a>(edges: impl IntoIterator>, p: Point, to best.map(|b| b.1) } +/// The greatest distance (pt) between the ends of two Live Paint edges that the Live Paint tools take +/// as one point. Every edge comes from one planar map, so their ends meet this closely. +pub const LP_JOIN_TOL: f64 = 1e-4; + +/// The greatest distance (pt) between the middle of a Live Paint face's outline segment and an edge +/// it runs along. The planar map refits outlines to [`vectorcraft_pathops::DEFAULT_PRECISION`], +/// keeping each refit segment's ends, and with Gap Detection on, the faces and the edges come from +/// two maps, so an outline can lie that far off its edges between its ends. +const LP_FACE_EDGE_TOL: f64 = vectorcraft_pathops::DEFAULT_PRECISION; + +/// The cell [`LP_JOIN_TOL`] wide that holds `p`, for finding the points near it; two points within +/// [`LP_JOIN_TOL`] of each other lie in the same or neighboring cells. The cast saturates, so a +/// coordinate too large or not finite does not overflow. +fn lp_cell(p: Point) -> (i64, i64) { + ((p.x / LP_JOIN_TOL).round() as i64, (p.y / LP_JOIN_TOL).round() as i64) +} + +/// The cell that holds `p` ([`lp_cell`]) and the eight cells around it. +fn lp_cells_around(p: Point) -> impl Iterator { + let (x, y) = lp_cell(p); + (-1..=1).flat_map(move |dx: i64| (-1..=1).map(move |dy: i64| (x.saturating_add(dx), y.saturating_add(dy)))) +} + +/// The stroke paint Live Paint edge `edge` draws with: the paint of its topmost stroke that is shown +/// with a paint, a weight above 0 and an opacity above 0, or [`Paint::None`] when the edge is fully +/// transparent or has no such stroke. +pub(super) fn lp_stroke_paint(edge: &Node) -> Paint { + if edge.opacity <= 0.0 { + return Paint::None; + } + edge.appearance + .items + .iter() + .rev() + .find_map(|i| match i { + vectorcraft_doc::appearance::AppearanceItem::Stroke(s) if s.visible && !s.paint.is_none() && s.width > 0.0 && s.opacity > 0.0 => { + Some(s.paint.clone()) + } + _ => None, + }) + .unwrap_or(Paint::None) +} + +/// Whether Live Paint edge `edge` draws no stroke ([`lp_stroke_paint`] is [`Paint::None`]). +fn unstroked(edge: &Node) -> bool { + lp_stroke_paint(edge).is_none() +} + +/// The segments of `pd`. +fn lp_segments(pd: &PathData) -> Vec { + pd.subpaths.iter().flat_map(|sp| (0..sp.segment_count()).map(|i| sp.segment(i))).collect() +} + +/// The faces of Live Paint group `group` that a flood from face `start` reaches, `start` first: +/// `start`, and each face that shares with a face already reached an edge that is hidden or draws +/// no stroke. Hidden faces are not reached and do not pass the flood on. A face runs along the parts +/// of the edge segments its outline segments lie on ([`lp_lies_on`]) or overlap ([`lp_overlap`]). +/// Two faces share an edge where those parts overlap along more than [`LP_FACE_EDGE_TOL`], or, where +/// both outlines lie within [`LP_JOIN_TOL`] of the edge, along more than half the shorter part. So +/// two faces that a Gap Detection gap parts on the same side of an edge, whose parts only touch, do +/// not share it, and two faces whose shared edge is shorter than [`LP_FACE_EDGE_TOL`] do. Empty when +/// `start` is not a face of `group` that is not hidden. +pub fn flood_faces(group: &Node, start: NodeId) -> Vec { + use kurbo::{ParamCurve, ParamCurveArclen}; + use std::collections::{HashMap, HashSet}; + let shown: Vec<&Arc> = faces(group).into_iter().filter(|f| f.visible).collect(); + if !shown.iter().any(|f| f.id == start) { + return vec![]; + } + // Each edge segment, and whether a flood crosses it. + let edge_segs: Vec<(kurbo::CubicBez, bool)> = edges(group) + .into_iter() + .flat_map(|e| { + let cross = !e.visible || unstroked(e); + e.path_data().map(lp_segments).unwrap_or_default().into_iter().map(move |s| (s, cross)) + }) + .collect(); + // The faces along each edge segment a flood crosses, each with the part of the segment it runs + // along (from and to, in the segment's own parameter). A face outline segment usually lies on + // one edge segment, stroked or not ([`lp_lies_on`]); one that does not (a piece of an edge + // segment where a Gap Detection line meets it, the line itself, or an outline segment that spans + // several edge segments) is measured against the edge segments a flood crosses that its bounds + // meet ([`lp_overlap`]). Each part also records whether its outline segment lies within + // [`LP_JOIN_TOL`] of it ([`lp_close`]). + let all: Vec<(kurbo::CubicBez, bool)> = edge_segs.iter().map(|&(s, _)| (s, true)).collect(); + let all_grid = LpEdgeGrid::new(&all); + let mut along: HashMap> = HashMap::new(); + let mut pieces: Vec<(kurbo::CubicBez, NodeId)> = vec![]; + for f in &shown { + for piece in f.path_data().map(lp_segments).unwrap_or_default() { + let m = piece.eval(0.5); + let bounds = kurbo::ParamCurveExtrema::bounding_box(&piece).inflate(LP_FACE_EDGE_TOL, LP_FACE_EDGE_TOL); + // The edge segment the outline segment lies on whose middle is nearest its own. + let on = all_grid + .near_box(bounds) + .into_iter() + .filter_map(|k| edge_segs.get(k).and_then(|(s, cross)| lp_lies_on(s, &piece).map(|part| (k, s, *cross, part)))) + .min_by(|x, y| (x.1.eval(0.5) - m).hypot().total_cmp(&(y.1.eval(0.5) - m).hypot())); + match on { + Some((k, s, cross, (from, to))) => { + if cross { + along.entry(k).or_default().push((f.id, from, to, lp_close(s, from, to, &piece))); + } + } + None => pieces.push((piece, f.id)), + } + } + } + if !pieces.is_empty() { + let grid = LpEdgeGrid::new(&edge_segs); + for (piece, id) in pieces { + let bounds = kurbo::ParamCurveExtrema::bounding_box(&piece).inflate(LP_FACE_EDGE_TOL, LP_FACE_EDGE_TOL); + for k in grid.near_box(bounds) { + if let Some((s, true)) = edge_segs.get(k) + && let Some((a, c)) = lp_overlap(s, &piece) + { + along.entry(k).or_default().push((id, a, c, lp_close(s, a, c, &piece))); + } + } + } + } + let mut next: HashMap> = HashMap::new(); + // Sorted by where each part starts, a part is compared only with the parts after it that start + // before it ends. Two parts that overlap along [`LP_FACE_EDGE_TOL`] or less join only when both + // lie within [`LP_JOIN_TOL`] of the edge and the overlap is more than half the shorter part: an + // edge that short is shared, while an outline that only crosses it lies off it. + for (k, on) in &mut along { + let Some((s, _)) = edge_segs.get(*k) else { continue }; + on.sort_by(|x, y| x.1.total_cmp(&y.1)); + for (i, &(a, a0, a1, a_close)) in on.iter().enumerate() { + for &(c, c0, c1, c_close) in on.iter().skip(i + 1).take_while(|x| x.1 < a1) { + let (from, to) = (a0.max(c0), a1.min(c1)); + let len = |x: f64, y: f64| s.subsegment(x..y).arclen(1e-6); + let shared = if a != c && from < to { len(from, to) } else { 0.0 }; + let short_shared = a_close && c_close && shared > 0.5 * len(a0, a1).min(len(c0, c1)); + if shared > 0.0 && (shared > LP_FACE_EDGE_TOL || short_shared) { + next.entry(a).or_default().push(c); + next.entry(c).or_default().push(a); + } + } + } + } + let mut reached = vec![start]; + let mut seen = HashSet::from([start]); + let mut i = 0; + while let Some(&f) = reached.get(i) { + i += 1; + for &n in next.get(&f).map(Vec::as_slice).unwrap_or_default() { + if seen.insert(n) { + reached.push(n); + } + } + } + reached +} + +/// The part of edge segment `s` that outline segment `piece` runs along, as (from, to) in `s`'s +/// own parameter, or None when they share no length. The part runs between the ends of `s` that lie +/// within [`LP_FACE_EDGE_TOL`] of `piece` and the ends of `piece` that lie within [`LP_JOIN_TOL`] of +/// `s` (the map keeps the ends of an outline segment it refits), and its middle lies within +/// [`LP_FACE_EDGE_TOL`] of `piece`, where the two run along each other ([`lp_runs_along`]). When no +/// end of `piece` lies on `s`, `piece` must lie within [`LP_JOIN_TOL`] of the part ([`lp_close`]). So +/// two segments that only cross or touch, or that only lie near each other, share nothing. +fn lp_overlap(s: &kurbo::CubicBez, piece: &kurbo::CubicBez) -> Option<(f64, f64)> { + use kurbo::{ParamCurve, ParamCurveNearest}; + let on_piece = |p: Point| piece.nearest(p, 1e-9).distance_sq.sqrt() <= LP_FACE_EDGE_TOL; + let mut ts: Vec = [(s.p0, 0.0), (s.p3, 1.0)].into_iter().filter(|(p, _)| on_piece(*p)).map(|(_, t)| t).collect(); + let mut piece_end_on = false; + for p in [piece.p0, piece.p3] { + let n = s.nearest(p, 1e-9); + if n.distance_sq.sqrt() <= LP_JOIN_TOL { + ts.push(n.t); + piece_end_on = true; + } + } + let from = ts.iter().copied().fold(f64::INFINITY, f64::min); + let to = ts.iter().copied().fold(f64::NEG_INFINITY, f64::max); + if from >= to { + return None; + } + let mid = (from + to) / 2.0; + let n = piece.nearest(s.eval(mid), 1e-9); + let along = if piece_end_on { lp_runs_along(s, from, to, piece, mid, n.t) } else { lp_close(s, from, to, piece) }; + (n.distance_sq.sqrt() <= LP_FACE_EDGE_TOL && along).then_some((from, to)) +} + +/// The part of edge segment `s` that outline segment `piece` lies on as a whole, as (from, to) in +/// `s`'s own parameter: both ends of `piece` lie within [`LP_JOIN_TOL`] of `s`, its middle lies +/// within [`LP_FACE_EDGE_TOL`] of `s`, and the two run along each other ([`lp_runs_along`]). +fn lp_lies_on(s: &kurbo::CubicBez, piece: &kurbo::CubicBez) -> Option<(f64, f64)> { + use kurbo::{ParamCurve, ParamCurveNearest}; + let (a, c, m) = (s.nearest(piece.p0, 1e-9), s.nearest(piece.p3, 1e-9), s.nearest(piece.eval(0.5), 1e-9)); + let (from, to) = (a.t.min(c.t), a.t.max(c.t)); + let near = a.distance_sq.sqrt() <= LP_JOIN_TOL && c.distance_sq.sqrt() <= LP_JOIN_TOL && m.distance_sq.sqrt() <= LP_FACE_EDGE_TOL; + (near && lp_runs_along(s, from, to, piece, m.t, 0.5)).then_some((from, to)) +} + +/// Whether outline segment `piece` runs along the part of edge segment `s` from `from` to `to` +/// rather than crossing it: it lies within [`LP_JOIN_TOL`] of that part ([`lp_close`]), or the two +/// run the same way or opposite ways at `ts` on `s` and `tp` on `piece` ([`lp_parallel`]). +fn lp_runs_along(s: &kurbo::CubicBez, from: f64, to: f64, piece: &kurbo::CubicBez, ts: f64, tp: f64) -> bool { + lp_close(s, from, to, piece) || lp_parallel(s, ts, piece, tp) +} + +/// Whether outline segment `piece` lies within [`LP_JOIN_TOL`] of edge segment `s` at the ends and +/// the middle of the part of `s` from `from` to `to`. +fn lp_close(s: &kurbo::CubicBez, from: f64, to: f64, piece: &kurbo::CubicBez) -> bool { + use kurbo::{ParamCurve, ParamCurveNearest}; + [from, (from + to) / 2.0, to].into_iter().all(|t| piece.nearest(s.eval(t), 1e-9).distance_sq.sqrt() <= LP_JOIN_TOL) +} + +/// Whether segment `s` at parameter `ts` and segment `piece` at parameter `tp` run the same way or +/// opposite ways: the sine of the angle between their directions there is at most 0.2. False where +/// either has no direction. +fn lp_parallel(s: &kurbo::CubicBez, ts: f64, piece: &kurbo::CubicBez, tp: f64) -> bool { + use kurbo::{ParamCurve, ParamCurveDeriv}; + let (u, v) = (s.deriv().eval(ts).to_vec2(), piece.deriv().eval(tp).to_vec2()); + let n = u.hypot() * v.hypot(); + n > 0.0 && u.cross(v).abs() <= 0.2 * n +} + +/// Most grid cells one edge segment is listed in by [`LpEdgeGrid`]; a longer one is tried for +/// every point. +const LP_GRID_SPAN: i64 = 1024; + +/// The farthest cell from the grid's origin, on each axis, that [`LpEdgeGrid`] numbers a point in. +const LP_GRID_LIMIT: f64 = 1_048_576.0; + +/// The edge segments a flood crosses, by the cells of a square grid (about one cell per segment) +/// that their bounds, widened by [`LP_FACE_EDGE_TOL`], cover. +struct LpEdgeGrid { + origin: Point, + cell: f64, + /// Segment indices by cell. + cells: std::collections::HashMap<(i64, i64), Vec>, + /// Segments whose bounds cover more than [`LP_GRID_SPAN`] cells, near every point. + wide: Vec, +} + +impl LpEdgeGrid { + /// The grid of the segments of `edges` that a flood crosses (those marked true). + fn new(edges: &[(kurbo::CubicBez, bool)]) -> Self { + let boxes: Vec<(usize, Rect)> = edges + .iter() + .enumerate() + .filter(|(_, e)| e.1) + .map(|(k, (s, _))| (k, kurbo::ParamCurveExtrema::bounding_box(s).inflate(LP_FACE_EDGE_TOL, LP_FACE_EDGE_TOL))) + .collect(); + let all = boxes.iter().map(|b| b.1).reduce(|a, c| a.union(c)).unwrap_or_default(); + let side = (boxes.len() as f64).sqrt().ceil().max(1.0); + let cell = all.width().max(all.height()) / side; + let cell = if cell.is_finite() && cell > LP_FACE_EDGE_TOL { cell } else { LP_FACE_EDGE_TOL }; + let mut grid = Self { origin: all.origin(), cell, cells: std::collections::HashMap::new(), wide: vec![] }; + for (k, b) in boxes { + let ((x0, y0), (x1, y1)) = (grid.cell_of(Point::new(b.x0, b.y0)), grid.cell_of(Point::new(b.x1, b.y1))); + if (x1 - x0 + 1).saturating_mul(y1 - y0 + 1) > LP_GRID_SPAN { + grid.wide.push(k); + continue; + } + for x in x0..=x1 { + for y in y0..=y1 { + grid.cells.entry((x, y)).or_default().push(k); + } + } + } + grid + } + + /// The cell `p` is in, counted from the origin (at most [`LP_GRID_LIMIT`] cells away on each + /// axis). + fn cell_of(&self, p: Point) -> (i64, i64) { + let at = |v: f64, o: f64| ((v - o) / self.cell).floor().clamp(-LP_GRID_LIMIT, LP_GRID_LIMIT) as i64; + (at(p.x, self.origin.x), at(p.y, self.origin.y)) + } + + /// The segments whose widened bounds may meet `r`, each once, in ascending order. When `r` + /// covers more than [`LP_GRID_SPAN`] cells, every listed segment is returned. + fn near_box(&self, r: Rect) -> Vec { + let ((x0, y0), (x1, y1)) = (self.cell_of(Point::new(r.x0, r.y0)), self.cell_of(Point::new(r.x1, r.y1))); + let mut out: Vec = self.wide.clone(); + if (x1 - x0 + 1).saturating_mul(y1 - y0 + 1) > LP_GRID_SPAN { + out.extend(self.cells.values().flatten()); + } else { + for x in x0..=x1 { + for y in y0..=y1 { + out.extend(self.cells.get(&(x, y)).into_iter().flatten()); + } + } + } + out.sort_unstable(); + out.dedup(); + out + } +} + +/// The edges of Live Paint group `group` that are not hidden and are joined to edge `start` through +/// edges that draw with stroke paint `paint` ([`lp_stroke_paint`]), `start` first: `start`, and each +/// edge that draws with that stroke paint and has an end (the first or last point of one of its subpaths) within [`LP_JOIN_TOL`] of an +/// end of an edge already reached. +/// Empty when `start` is not an edge of `group` that is not hidden. +pub fn connected_edges(group: &Node, start: NodeId, paint: &Paint) -> Vec { + use std::collections::{HashMap, HashSet}; + let es: Vec<&Arc> = edges(group).into_iter().filter(|e| e.visible && (e.id == start || lp_stroke_paint(e) == *paint)).collect(); + // The first and last points of each subpath of each edge. + let ends: Vec> = es + .iter() + .map(|e| { + let Some(pd) = e.path_data() else { return vec![] }; + pd.subpaths + .iter() + .filter_map(|sp| { + let n = sp.segment_count(); + (n > 0).then(|| [sp.segment(0).p0, sp.segment(n - 1).p3]) + }) + .flatten() + .collect() + }) + .collect(); + let mut at: HashMap<(i64, i64), Vec<(Point, usize)>> = HashMap::new(); + for (i, pts) in ends.iter().enumerate() { + for &p in pts { + at.entry(lp_cell(p)).or_default().push((p, i)); + } + } + let Some(first) = es.iter().position(|e| e.id == start) else { return vec![] }; + let mut reached = vec![first]; + let mut seen = HashSet::from([first]); + let mut k = 0; + while let Some(&i) = reached.get(k) { + k += 1; + for &p in ends.get(i).map(Vec::as_slice).unwrap_or_default() { + for c in lp_cells_around(p) { + for &(q, j) in at.get(&c).map(Vec::as_slice).unwrap_or_default() { + if (q - p).hypot() <= LP_JOIN_TOL && seen.insert(j) { + reached.push(j); + } + } + } + } + } + reached.into_iter().filter_map(|i| es.get(i).map(|e| e.id)).collect() +} + /// The visible Live Paint group with a face under `p` that is not hidden (front-most in paint order). pub fn live_paint_at(doc: &Document, p: Point) -> Option { live_paint_at_where(doc, p, |id| doc.is_visible(id)) diff --git a/crates/tools/src/builder/tests.rs b/crates/tools/src/builder/tests.rs index 298e6a1907..0d8d12f183 100644 --- a/crates/tools/src/builder/tests.rs +++ b/crates/tools/src/builder/tests.rs @@ -1636,6 +1636,583 @@ fn live_paint_selection_click_outside_the_faces_selects_the_edge_it_highlights() assert_eq!(lp_click(&mut t, &cx, ev(PointerKind::Down, 106.0, 50.0)), vec![Action::Exec("select.none".into(), json!({}))]); } +/// A Live Paint group of three faces in a row, A (0..50), B (50..100) and C (100..150), 50 pt tall, +/// with these edges: AB between A and B, with no stroke; BC between B and C, stroked red; the tops +/// of A and B stroked black; the top of C stroked red; and one bottom edge under all three, +/// stroked black. Returns the group, the faces [A, B, C] and the edges [AB, BC, top A, top B, +/// top C, bottom]. +fn live_strip(d: &mut Document) -> (NodeId, [NodeId; 3], [NodeId; 6]) { + let l = d.layers[0].id; + let black = Paint::solid(vectorcraft_color::Color::BLACK); + let red = Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0)); + let mut kids = vec![empty_sources(d)]; + let mut fs = [NodeId(0); 3]; + for (k, x) in [0.0, 50.0, 100.0].into_iter().enumerate() { + fs[k] = d.alloc_id(); + let mut face = Node::path(fs[k], shapes::rectangle(Rect::new(x, 0.0, x + 50.0, 50.0)), Appearance::default()); + face.name = Some(FACE_NAME.into()); + kids.push(Arc::new(face)); + } + let pt = Point::new; + let lines = [ + (pt(50.0, 0.0), pt(50.0, 50.0), None), + (pt(100.0, 0.0), pt(100.0, 50.0), Some(red.clone())), + (pt(0.0, 0.0), pt(50.0, 0.0), Some(black.clone())), + (pt(50.0, 0.0), pt(100.0, 0.0), Some(black.clone())), + (pt(100.0, 0.0), pt(150.0, 0.0), Some(red)), + (pt(0.0, 50.0), pt(150.0, 50.0), Some(black)), + ]; + let mut es = [NodeId(0); 6]; + for (k, (from, to, stroke)) in lines.into_iter().enumerate() { + es[k] = d.alloc_id(); + let mut a = Appearance::default(); + if let Some(s) = stroke { + a.set_stroke(s); + } + let mut edge = Node::path(es[k], shapes::line(from, to), a); + edge.name = Some(EDGE_NAME.into()); + kids.push(Arc::new(edge)); + } + let gid = d.alloc_id(); + let mut g = Node::group(gid, kids); + g.name = Some(LIVE_PAINT_NAME.into()); + let at = d.layers[0].children().map_or(0, |c| c.len()); + d.insert(Some(l), at, g).unwrap(); + (gid, fs, es) +} + +#[test] +fn live_paint_flood_crosses_unstroked_edges_and_stops_at_stroked_ones() { + let mut d = Document::new(500.0, 500.0); + let (g, [a, b, c], [ab, bc, ..]) = live_strip(&mut d); + let group = |d: &Document| d.node(g).unwrap().clone(); + assert_eq!(flood_faces(&group(&d), a), vec![a, b]); + assert_eq!(flood_faces(&group(&d), b), vec![b, a]); + assert_eq!(flood_faces(&group(&d), c), vec![c]); + // A hidden edge does not part the faces either; a hidden face is not reached and does not start + // a flood; an id that is not a face of the group reaches nothing. + let mut hidden = d.clone(); + hidden.node_mut(bc).unwrap().visible = false; + assert_eq!(flood_faces(&group(&hidden), a), vec![a, b, c]); + hidden.node_mut(b).unwrap().visible = false; + assert_eq!(flood_faces(&group(&hidden), a), vec![a]); + assert!(flood_faces(&group(&hidden), b).is_empty()); + assert!(flood_faces(&group(&d), ab).is_empty()); + // An edge whose stroke is hidden, 0 pt wide or fully transparent, or that is itself fully + // transparent, draws no stroke, so the flood crosses it. + use vectorcraft_doc::appearance::{AppearanceItem, StrokeLayer}; + let base = d.node(bc).unwrap().appearance.stroke().unwrap().clone(); + let changes: [fn(&mut StrokeLayer); 3] = [|s| s.visible = false, |s| s.width = 0.0, |s| s.opacity = 0.0]; + for change in changes { + let mut e = d.clone(); + let mut s = base.clone(); + change(&mut s); + e.node_mut(bc).unwrap().appearance.items.retain(|i| !matches!(i, AppearanceItem::Stroke(_))); + e.node_mut(bc).unwrap().appearance.items.push(AppearanceItem::Stroke(s)); + assert_eq!(flood_faces(&group(&e), a), vec![a, b, c]); + } + let mut e = d.clone(); + e.node_mut(bc).unwrap().opacity = 0.0; + assert_eq!(flood_faces(&group(&e), a), vec![a, b, c]); +} + +/// Faces P (0..50) and Q (50..100) above one edge from (0, 0) to (100, 0) and face R (0..100) below +/// it, with no edge between P and Q (as where a Gap Detection line parts them): P and Q each run +/// along part of the edge, and R along all of it. +#[test] +fn live_paint_flood_measures_faces_along_part_of_an_edge() { + let mut d = Document::new(500.0, 500.0); + let l = d.layers[0].id; + let mut kids = vec![empty_sources(&mut d)]; + let mut ids = [NodeId(0); 3]; + for (k, r) in [Rect::new(0.0, -50.0, 50.0, 0.0), Rect::new(50.0, -50.0, 100.0, 0.0), Rect::new(0.0, 0.0, 100.0, 50.0)].into_iter().enumerate() { + ids[k] = d.alloc_id(); + let mut face = Node::path(ids[k], shapes::rectangle(r), Appearance::default()); + face.name = Some(FACE_NAME.into()); + kids.push(Arc::new(face)); + } + let [p, q, r] = ids; + let mut edge = Node::path(d.alloc_id(), shapes::line(Point::new(0.0, 0.0), Point::new(100.0, 0.0)), Appearance::default()); + edge.name = Some(EDGE_NAME.into()); + kids.push(Arc::new(edge)); + let gid = d.alloc_id(); + let mut g = Node::group(gid, kids); + g.name = Some(LIVE_PAINT_NAME.into()); + d.insert(Some(l), 0, g).unwrap(); + let group = d.node(gid).unwrap().clone(); + // P reaches R across the edge, and Q through R; P and Q only touch at (50, 0). + assert_eq!(flood_faces(&group, p), vec![p, r, q]); + let mut without_r = d.clone(); + without_r.node_mut(r).unwrap().visible = false; + assert_eq!(flood_faces(without_r.node(gid).unwrap(), p), vec![p]); +} + +/// Face P (0..100) above unstroked edges along y = 0 split at `splits`, whose outline runs along +/// all of them with one segment, and below them one face between each two splits, the faces parted +/// from each other by stroked edges. A flood from any face below reaches P and, through it, the +/// others; this holds when the outline's middle is no edge's middle (30), is the end shared by two +/// edges (50), and is the middle of an edge shorter than the outline (25 and 75). +#[test] +fn live_paint_flood_crosses_an_outline_segment_that_spans_several_edges() { + for splits in [vec![30.0], vec![50.0], vec![25.0, 75.0]] { + let mut d = Document::new(500.0, 500.0); + let l = d.layers[0].id; + let mut kids = vec![empty_sources(&mut d)]; + let xs: Vec = std::iter::once(0.0).chain(splits.iter().copied()).chain(std::iter::once(100.0)).collect(); + let add_face = |d: &mut Document, kids: &mut Vec>, r: Rect| { + let id = d.alloc_id(); + let mut face = Node::path(id, shapes::rectangle(r), Appearance::default()); + face.name = Some(FACE_NAME.into()); + kids.push(Arc::new(face)); + id + }; + let p = add_face(&mut d, &mut kids, Rect::new(0.0, -50.0, 100.0, 0.0)); + let below: Vec = xs.windows(2).map(|w| add_face(&mut d, &mut kids, Rect::new(w[0], 0.0, w[1], 50.0))).collect(); + let pt = Point::new; + let lines = xs + .windows(2) + .map(|w| (pt(w[0], 0.0), pt(w[1], 0.0), false)) + .chain(splits.iter().map(|&x| (pt(x, 0.0), pt(x, 50.0), true))) + .collect::>(); + for (from, to, stroked) in lines { + let mut a = Appearance::default(); + if stroked { + a.set_stroke(Paint::solid(vectorcraft_color::Color::BLACK)); + } + let mut edge = Node::path(d.alloc_id(), shapes::line(from, to), a); + edge.name = Some(EDGE_NAME.into()); + kids.push(Arc::new(edge)); + } + let gid = d.alloc_id(); + let mut g = Node::group(gid, kids); + g.name = Some(LIVE_PAINT_NAME.into()); + d.insert(Some(l), 0, g).unwrap(); + let group = d.node(gid).unwrap().clone(); + let mut all = below.clone(); + all.push(p); + all.sort(); + for &f in &below { + let mut got = flood_faces(&group, f); + assert_eq!(got.first(), Some(&f), "{splits:?}"); + got.sort(); + assert_eq!(got, all, "{splits:?} from {f:?}"); + } + } +} + +/// A Live Paint group built from `paths` as `livePaint.make` builds one, with Gap Detection closing +/// gaps up to `gap` pt when given, and with no stroke on any edge. Returns the group and its faces. +fn live_from_paths(paths: Vec, gap: Option) -> (Node, Vec) { + let mut d = Document::new(500.0, 500.0); + let shapes: Vec = paths.into_iter().enumerate().map(|(k, p)| po::Shape::new(p, FillRule::NonZero, k as u64)).collect(); + let (regions, lines) = match gap { + Some(g) => { + let (r, e, _) = po::live_paint_with_gaps(&shapes, g); + (r, e) + } + None => po::live_paint(&shapes), + }; + let mut kids = vec![empty_sources(&mut d)]; + let mut ids = vec![]; + for r in regions { + let id = d.alloc_id(); + let mut face = Node::path(id, r.path, Appearance::default()); + face.name = Some(FACE_NAME.into()); + kids.push(Arc::new(face)); + ids.push(id); + } + for e in lines { + let mut edge = Node::path(d.alloc_id(), e.path, Appearance::default()); + edge.name = Some(EDGE_NAME.into()); + kids.push(Arc::new(edge)); + } + let mut g = Node::group(d.alloc_id(), kids); + g.name = Some(LIVE_PAINT_NAME.into()); + (g, ids) +} + +/// Live Paint groups built from curves, one with a T-junction where lines end inside an ellipse and +/// one with Gap Detection closing the gap between a curve's end and a circle: with no stroke on any +/// edge, a flood from any face reaches every face. +#[test] +fn live_paint_flood_crosses_unstroked_curves_in_built_groups() { + let pt = Point::new; + let frame = shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 200.0)); + let t_junction = vec![ + frame, + shapes::ellipse(Rect::new(50.0, 60.0, 150.0, 140.0)), + shapes::line(pt(113.0, 0.0), pt(113.0, 80.0)), + shapes::line(pt(113.0, 200.0), pt(113.0, 120.0)), + ]; + let a = |p: Point, h_in: Point, h_out: Point| vectorcraft_geom::Anchor { p, h_in, h_out, kind: vectorcraft_geom::AnchorKind::Smooth }; + let (start, end) = (pt(102.7, 20.0), pt(109.0, 100.0 + (2500.0f64 - 81.0).sqrt() - 2.0)); + let curve = PathData::single(SubPath::new( + vec![a(start, start, start + kurbo::Vec2::new(40.0, 40.0)), a(end, end + kurbo::Vec2::new(-30.0, -40.0), end)], + false, + )); + let gap = vec![shapes::ellipse(Rect::new(50.0, 50.0, 150.0, 150.0)), curve]; + for (name, paths, max_gap, count) in [("T-junction", t_junction, None, 3), ("Gap Detection", gap, Some(3.0), 2)] { + let (g, fs) = live_from_paths(paths, max_gap); + assert_eq!(fs.len(), count, "{name}"); + for &f in &fs { + let mut got = flood_faces(&g, f); + got.sort(); + assert_eq!(got, fs, "{name} from {f:?}"); + } + } +} + +/// A closed outline: `curve`, then straight lines through `rest` and back to the curve's start. +fn curve_then_lines(curve: kurbo::CubicBez, rest: &[Point]) -> PathData { + let a = |p: Point, h_in: Point, h_out: Point| vectorcraft_geom::Anchor { p, h_in, h_out, kind: vectorcraft_geom::AnchorKind::Corner }; + let mut anchors = vec![a(curve.p0, curve.p0, curve.p1), a(curve.p3, curve.p2, curve.p3)]; + anchors.extend(rest.iter().map(|&p| a(p, p, p))); + PathData::single(SubPath::new(anchors, true)) +} + +/// `c` with its two handles moved 0.006 pt along y: its ends stay put and its middle moves 0.0045 pt, +/// as a face outline refit within the planar map's precision can lie off its edge. +fn refit(c: kurbo::CubicBez) -> kurbo::CubicBez { + let up = kurbo::Vec2::new(0.0, 0.006); + kurbo::CubicBez::new(c.p0, c.p1 + up, c.p2 + up, c.p3) +} + +/// A Live Paint group of `faces` and of `lines`, each line with or without a stroke. Returns the +/// group and the ids of the faces. +fn live_of(faces: Vec, lines: Vec<(PathData, bool)>) -> (Node, Vec) { + let mut d = Document::new(500.0, 500.0); + let mut kids = vec![empty_sources(&mut d)]; + let mut ids = vec![]; + for pd in faces { + let id = d.alloc_id(); + let mut face = Node::path(id, pd, Appearance::default()); + face.name = Some(FACE_NAME.into()); + kids.push(Arc::new(face)); + ids.push(id); + } + for (pd, stroked) in lines { + let mut a = Appearance::default(); + if stroked { + a.set_stroke(Paint::solid(vectorcraft_color::Color::BLACK)); + } + let mut edge = Node::path(d.alloc_id(), pd, a); + edge.name = Some(EDGE_NAME.into()); + kids.push(Arc::new(edge)); + } + let mut g = Node::group(d.alloc_id(), kids); + g.name = Some(LIVE_PAINT_NAME.into()); + (g, ids) +} + +/// Faces whose outlines along an unstroked curved edge lie up to 0.0045 pt off it, as the planar map +/// can leave them when it refits an outline across a T-junction or, with Gap Detection on, builds the +/// faces and the edges from two maps: the flood still crosses the edge. Two faces that a gap parts +/// on the same side of the edge, which only touch, are still not joined. +#[test] +fn live_paint_flood_crosses_curved_edges_that_outlines_lie_slightly_off() { + use kurbo::ParamCurve; + let c = kurbo::CubicBez::new(Point::new(0.0, 0.0), Point::new(30.0, 40.0), Point::new(70.0, 40.0), Point::new(100.0, 0.0)); + let edge = |s: kurbo::CubicBez| { + let a = |p: Point, h_in: Point, h_out: Point| vectorcraft_geom::Anchor { p, h_in, h_out, kind: vectorcraft_geom::AnchorKind::Corner }; + PathData::single(SubPath::new(vec![a(s.p0, s.p0, s.p1), a(s.p3, s.p2, s.p3)], false)) + }; + // T-junction: a stroked line ends on the curve at t = 0.4 and parts the faces below it (L and R); + // the curve is two edges, and the face above (U) runs along both with one refit segment. + let j = c.eval(0.4); + let (left, right) = (c.subsegment(0.0..0.4), c.subsegment(0.4..1.0)); + let (g, ids) = live_of( + vec![ + curve_then_lines(left, &[Point::new(j.x, 80.0), Point::new(0.0, 80.0)]), + curve_then_lines(right, &[Point::new(100.0, 80.0), Point::new(j.x, 80.0)]), + curve_then_lines(refit(c), &[Point::new(100.0, -50.0), Point::new(0.0, -50.0)]), + ], + vec![(edge(left), false), (edge(right), false), (shapes::line(j, Point::new(j.x, 80.0)), true)], + ); + let [l, r, u] = [ids[0], ids[1], ids[2]]; + assert_eq!(flood_faces(&g, l), vec![l, u, r]); + let mut from_u = flood_faces(&g, u); + assert_eq!(from_u.first(), Some(&u)); + from_u.sort(); + assert_eq!(from_u, ids); + // Gap Detection: a gap line, which is no edge, parts P and Q above the curve at t = 0.5; the curve + // is one edge, and Q and the face below (B) run along it with refit segments. + let m = c.eval(0.5); + let (g, ids) = live_of( + vec![ + curve_then_lines(c.subsegment(0.0..0.5), &[Point::new(m.x, -50.0), Point::new(0.0, -50.0)]), + curve_then_lines(refit(c.subsegment(0.5..1.0)), &[Point::new(100.0, -50.0), Point::new(m.x, -50.0)]), + curve_then_lines(refit(c), &[Point::new(100.0, 80.0), Point::new(0.0, 80.0)]), + ], + vec![(edge(c), false)], + ); + let [p, q, b] = [ids[0], ids[1], ids[2]]; + assert_eq!(flood_faces(&g, p), vec![p, b, q]); + let mut without_b = g.clone(); + if let Some(kids) = without_b.children_mut() + && let Some(n) = kids.iter_mut().find(|n| n.id == b) + { + Arc::make_mut(n).visible = false; + } + assert_eq!(flood_faces(&without_b, p), vec![p]); +} + +/// Two large faces whose only shared edge, with no stroke, is 0.005 pt long: the flood crosses it. +#[test] +fn live_paint_flood_crosses_a_shared_edge_shorter_than_the_precision() { + let pt = Point::new; + let (a, b) = (pt(50.0, 0.0), pt(50.0, 0.005)); + let (g, ids) = live_of( + vec![ + PathData::single(SubPath::polyline(&[pt(0.0, -50.0), a, b, pt(0.0, 50.0)], true)), + PathData::single(SubPath::polyline(&[pt(100.0, -50.0), pt(100.0, 50.0), b, a], true)), + ], + vec![(shapes::line(a, b), false)], + ); + assert_eq!(flood_faces(&g, ids[0]), ids); +} + +/// A stroked edge along y = 0 and an edge with no stroke 0.008 pt below it, with a face above the +/// first whose outline ends on it and bows 0.005 pt off it in the middle, nearer the second, and a +/// face below the second: the stroked edge parts the two faces, so the flood does not join them. +#[test] +fn live_paint_flood_stops_where_a_stroked_edge_runs_along_an_unstroked_one() { + let pt = Point::new; + let bow = kurbo::CubicBez::new(pt(100.0, 0.0), pt(70.0, 0.02 / 3.0), pt(30.0, 0.02 / 3.0), pt(0.0, 0.0)); + let (g, ids) = live_of( + vec![ + curve_then_lines(bow, &[pt(0.0, -50.0), pt(100.0, -50.0)]), + PathData::single(SubPath::polyline(&[pt(0.0, 0.008), pt(0.0, 50.0), pt(100.0, 50.0), pt(100.0, 0.008)], true)), + ], + vec![(shapes::line(pt(0.0, 0.0), pt(100.0, 0.0)), true), (shapes::line(pt(0.0, 0.008), pt(100.0, 0.008)), false)], + ); + assert_eq!(flood_faces(&g, ids[0]), vec![ids[0]]); + assert_eq!(flood_faces(&g, ids[1]), vec![ids[1]]); +} + +/// An edge with no stroke from (10, 0) to (90, 0), and two faces that meet on a line 0.005 pt below +/// it from (0, 0.005) to (100, 0.005) (as a Gap Detection line): neither face runs along the edge, so +/// the flood does not join them. +#[test] +fn live_paint_flood_does_not_cross_an_edge_beside_a_line_two_faces_meet_on() { + let pt = Point::new; + let (a, b) = (pt(0.0, 0.005), pt(100.0, 0.005)); + let (g, ids) = live_of( + vec![ + PathData::single(SubPath::polyline(&[a, b, pt(100.0, -50.0), pt(0.0, -50.0)], true)), + PathData::single(SubPath::polyline(&[b, a, pt(0.0, 50.0), pt(100.0, 50.0)], true)), + ], + vec![(shapes::line(pt(10.0, 0.0), pt(90.0, 0.0)), false)], + ); + assert_eq!(flood_faces(&g, ids[0]), vec![ids[0]]); +} + +/// A stroked edge and an edge with no stroke that leave one point at a slope of 0.01, so they lie +/// within 0.01 pt of each other for their first point or so: the face between them and the face +/// beyond the edge with no stroke share that edge, so the flood joins them. +#[test] +fn live_paint_flood_crosses_an_unstroked_edge_beside_a_stroked_one() { + let pt = Point::new; + let (o, a, b) = (pt(0.0, 0.0), pt(100.0, 0.0), pt(100.0, 1.0)); + let (g, ids) = live_of( + vec![ + PathData::single(SubPath::polyline(&[o, a, b], true)), + PathData::single(SubPath::polyline(&[o, b, pt(100.0, 50.0), pt(0.0, 50.0)], true)), + ], + vec![(shapes::line(o, a), true), (shapes::line(o, b), false), (shapes::line(a, b), true)], + ); + assert_eq!(flood_faces(&g, ids[0]), ids); +} + +/// Two faces on either side of an edge with no stroke whose curve has a cusp at its middle (no +/// direction there): the flood crosses it. +#[test] +fn live_paint_flood_crosses_an_edge_with_a_cusp_at_its_middle() { + let c = kurbo::CubicBez::new(Point::new(0.0, 0.0), Point::new(100.0, 60.0), Point::new(0.0, 60.0), Point::new(100.0, 0.0)); + let a = |p: Point, h_in: Point, h_out: Point| vectorcraft_geom::Anchor { p, h_in, h_out, kind: vectorcraft_geom::AnchorKind::Corner }; + let edge = PathData::single(SubPath::new(vec![a(c.p0, c.p0, c.p1), a(c.p3, c.p2, c.p3)], false)); + let (g, ids) = live_of( + vec![ + curve_then_lines(c, &[Point::new(100.0, -50.0), Point::new(0.0, -50.0)]), + curve_then_lines(c, &[Point::new(100.0, 100.0), Point::new(0.0, 100.0)]), + ], + vec![(edge, false)], + ); + assert_eq!(flood_faces(&g, ids[0]), ids); +} + +/// Two faces parted by a line that is no edge (as a Gap Detection line), which crosses an edge with +/// no stroke: one 0.006 pt long, upright or at a slope of 0.1, and one 1 pt long at a slope of 0.09. +/// The faces only cross the edge and do not run along it, so the flood does not join them. +#[test] +fn live_paint_flood_does_not_cross_an_edge_an_outline_only_crosses() { + let pt = Point::new; + let short = pt(0.006, 0.0); + for (a, b, end) in + [(pt(0.003, -0.004), pt(0.003, 0.02), short), (pt(-0.037, -0.004), pt(0.043, 0.004), short), (pt(0.1, -0.009), pt(0.3, 0.009), pt(1.0, 0.0))] + { + let (g, ids) = live_of( + vec![ + PathData::single(SubPath::polyline(&[a, b, pt(-50.0, 50.0), pt(-50.0, -50.0)], true)), + PathData::single(SubPath::polyline(&[b, a, pt(50.0, -50.0), pt(50.0, 50.0)], true)), + ], + vec![(shapes::line(pt(0.0, 0.0), end), false)], + ); + assert_eq!(flood_faces(&g, ids[0]), vec![ids[0]], "{a:?} to {b:?}"); + } +} + +#[test] +fn live_paint_connected_edges_follow_the_same_stroke_paint() { + let mut d = Document::new(500.0, 500.0); + let (g, [a, ..], [ab, bc, top_a, top_b, top_c, bottom]) = live_strip(&mut d); + let group = d.node(g).unwrap().clone(); + let black = Paint::solid(vectorcraft_color::Color::BLACK); + let red = Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0)); + // Top A and top B join end to end; top C joins them but is red, and the black bottom does not + // join them. + assert_eq!(connected_edges(&group, top_a, &black), vec![top_a, top_b]); + // BC and top C, both red, join at (100, 0). + assert_eq!(connected_edges(&group, bc, &red), vec![bc, top_c]); + assert_eq!(connected_edges(&group, bottom, &black), vec![bottom]); + // The edge started from is taken whatever its paint. + assert_eq!(connected_edges(&group, ab, &Paint::None), vec![ab]); + assert!(connected_edges(&group, a, &black).is_empty()); +} + +/// An edge from (0, 0) through (50, 0) to (100, 0) and a second edge with the same stroke from +/// (50, 0) to (50, 50): the second one ends at an anchor in the middle of the first, not at one of +/// its ends, so the two are not joined end to end. A third edge from (100, 0) is. +#[test] +fn live_paint_connected_edges_join_only_at_path_ends() { + let mut d = Document::new(500.0, 500.0); + let l = d.layers[0].id; + let black = Paint::solid(vectorcraft_color::Color::BLACK); + let mut kids = vec![empty_sources(&mut d)]; + let pt = Point::new; + let paths = [ + PathData::single(SubPath::polyline(&[pt(0.0, 0.0), pt(50.0, 0.0), pt(100.0, 0.0)], false)), + shapes::line(pt(50.0, 0.0), pt(50.0, 50.0)), + shapes::line(pt(100.0, 0.0), pt(100.0, 50.0)), + ]; + let mut ids = vec![]; + for pd in paths { + let mut a = Appearance::default(); + a.set_stroke(black.clone()); + let id = d.alloc_id(); + let mut edge = Node::path(id, pd, a); + edge.name = Some(EDGE_NAME.into()); + kids.push(Arc::new(edge)); + ids.push(id); + } + let gid = d.alloc_id(); + let mut g = Node::group(gid, kids); + g.name = Some(LIVE_PAINT_NAME.into()); + d.insert(Some(l), 0, g).unwrap(); + let group = d.node(gid).unwrap().clone(); + assert_eq!(connected_edges(&group, ids[0], &black), vec![ids[0], ids[2]]); + assert_eq!(connected_edges(&group, ids[1], &black), vec![ids[1]]); +} + +/// Three edges end to end along (0, 0) to (150, 0) under one face: a red one, one drawn red with a +/// hidden blue stroke over it, and one drawn blue with a hidden red stroke over it. Edges join by the +/// paint they draw with, not by a hidden stroke's paint, for `connected_edges` and a double-click. +#[test] +fn live_paint_connected_edges_follow_the_stroke_paint_they_draw_with() { + use vectorcraft_doc::appearance::{AppearanceItem, StrokeLayer}; + let mut d = Document::new(500.0, 500.0); + let l = d.layers[0].id; + let red = Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0)); + let blue = Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0)); + let mut kids = vec![empty_sources(&mut d)]; + let fid = d.alloc_id(); + let mut face = Node::path(fid, shapes::rectangle(Rect::new(0.0, 0.0, 150.0, 50.0)), Appearance::default()); + face.name = Some(FACE_NAME.into()); + kids.push(Arc::new(face)); + let pt = Point::new; + let hidden = |p: &Paint| AppearanceItem::Stroke(StrokeLayer { visible: false, ..StrokeLayer::new(p.clone(), 1.0) }); + let shown = |p: &Paint| AppearanceItem::Stroke(StrokeLayer::new(p.clone(), 1.0)); + let edges = [(0.0, vec![shown(&red)]), (50.0, vec![shown(&red), hidden(&blue)]), (100.0, vec![shown(&blue), hidden(&red)])]; + let mut ids = vec![]; + for (x, items) in edges { + let id = d.alloc_id(); + let mut edge = Node::path(id, shapes::line(pt(x, 0.0), pt(x + 50.0, 0.0)), Appearance { items, ..Appearance::default() }); + edge.name = Some(EDGE_NAME.into()); + kids.push(Arc::new(edge)); + ids.push(id); + } + let gid = d.alloc_id(); + let mut g = Node::group(gid, kids); + g.name = Some(LIVE_PAINT_NAME.into()); + d.insert(Some(l), 0, g).unwrap(); + let group = d.node(gid).unwrap().clone(); + assert_eq!(connected_edges(&group, ids[0], &red), vec![ids[0], ids[1]]); + assert_eq!(connected_edges(&group, ids[2], &blue), vec![ids[2]]); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let set = |ids: &[NodeId]| vec![Action::Exec("select.set".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::>() }))]; + let alt = Mods { alt: true, ..Default::default() }; + let mut t = LivePaintSelectionTool::default(); + let mut dbl = |x: f64| t.pointer(&cx, &ev(PointerKind::DoubleClick, x, 1.0).with_mods(alt)); + assert_eq!(dbl(75.0), set(&[ids[1], ids[0]])); + assert_eq!(dbl(125.0), set(&[ids[2]])); +} + +#[test] +fn live_paint_selection_double_click_selects_connected_faces_or_edges() { + let mut d = Document::new(500.0, 500.0); + let (_, [a, b, c], [_, _, top_a, top_b, ..]) = live_strip(&mut d); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let set = |ids: &[NodeId]| vec![Action::Exec("select.set".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::>() }))]; + let toggle = |ids: &[NodeId]| vec![Action::Exec("select.toggle".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::>() }))]; + let alt = Mods { alt: true, ..Default::default() }; + let shift = Mods { shift: true, ..Default::default() }; + let dbl = |t: &mut LivePaintSelectionTool, x: f64, y: f64, m: Mods| t.pointer(&cx, &ev(PointerKind::DoubleClick, x, y).with_mods(m)); + let mut t = LivePaintSelectionTool::default(); + // A face: the faces not parted from it by a stroked edge. + assert_eq!(dbl(&mut t, 25.0, 25.0, Mods::default()), set(&[a, b])); + assert_eq!(dbl(&mut t, 125.0, 25.0, Mods::default()), set(&[c])); + // An edge (Alt): the edges joined to it end to end with its stroke paint. + assert_eq!(dbl(&mut t, 25.0, 1.0, alt), set(&[top_a, top_b])); + // With Shift held, the parts reached leave the selection or join it. + assert_eq!(dbl(&mut t, 75.0, 25.0, shift), toggle(&[b, a])); + assert_eq!(dbl(&mut t, 75.0, 1.0, Mods { alt: true, shift: true, ..Default::default() }), toggle(&[top_b, top_a])); + // Away from the group, a double-click adds nothing to what its clicks did. + assert!(dbl(&mut t, 300.0, 300.0, Mods::default()).is_empty()); + // Select Fills off: the double-click picks the edge nearest the pointer as a click does. + t.set_option("selectFills", &json!(false)); + assert_eq!(dbl(&mut t, 25.0, 1.0, Mods::default()), set(&[top_a, top_b])); + assert!(dbl(&mut t, 25.0, 25.0, Mods::default()).is_empty()); + // Select Strokes off: Alt picks the face. + t.set_option("selectFills", &json!(true)); + t.set_option("selectStrokes", &json!(false)); + assert_eq!(dbl(&mut t, 25.0, 1.0, alt), set(&[a, b])); + // Both off: nothing. + t.set_option("selectFills", &json!(false)); + assert!(dbl(&mut t, 25.0, 25.0, Mods::default()).is_empty()); + assert!(dbl(&mut t, 25.0, 1.0, alt).is_empty()); +} + +#[test] +fn live_paint_selection_double_click_skips_locked_groups_and_art_outside_isolation() { + let mut d = Document::new(500.0, 500.0); + let (g, _, _) = live_strip(&mut d); + let (other, _) = live_square(&mut d, Rect::new(200.0, 0.0, 300.0, 100.0)); + let s = Selection::default(); + let p = paint(); + let dbl = |d: &Document, iso: Option, x: f64| { + let cx = ToolContext { isolation: iso, ..cx(d, &s, &p) }; + LivePaintSelectionTool::default().pointer(&cx, &ev(PointerKind::DoubleClick, x, 25.0)) + }; + assert!(!dbl(&d, None, 25.0).is_empty()); + assert!(dbl(&d, Some(other), 25.0).is_empty(), "outside the isolated art"); + d.node_mut(g).unwrap().locked = true; + assert!(dbl(&d, None, 25.0).is_empty(), "a locked group"); + assert!(!dbl(&d, None, 250.0).is_empty()); +} + /// The Live Paint Selection pointer shows what a click at the point selects, from the point and /// the modifier keys alone (no move first): a face, an edge, or nothing. Groups the tool does not /// take (locked, hidden, outside isolation) show nothing to select. A press keeps its look until diff --git a/docs/mcp.md b/docs/mcp.md index 912ac32562..5826ace727 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3491,6 +3491,16 @@ already walks Image Trace. Short agent entry points: a locked artboard, or in isolation mode outside the isolated art. In the app, the tool's cursor shows what a click at the pointer selects under these rules: the arrow with a filled square for a face, with a short line for an edge, and with an x for nothing. It keeps the look it had at the press until the release. + A double-click (`pointer_gesture` kind `doubleclick`, after the two clicks' `down` and `up`) on the face a click there + picks runs `select.set {ids}` with that face and the faces of its group that a flood from it reaches across edges + that are hidden or draw no stroke (an edge that is fully transparent, or whose strokes are each hidden, without + paint, 0 pt wide or fully transparent); a stroked edge and a Gap Detection gap stop the flood, and hidden faces are + left out. On the edge a click + there picks, it runs `select.set {ids}` with that edge and the edges of its group, not hidden, joined to it end to + end through edges that draw with the stroke paint it draws with (the paint of an edge's topmost stroke that is + shown, with paint, above 0 pt and not fully transparent; none for an edge that draws no stroke). With Shift held it runs `select.toggle {ids}` with the same faces or edges, + and where a click selects no face or edge it runs nothing. The same options, locks and isolation rules apply as for + a click. The Live Paint Bucket's options (`tool.setOption {tool: "livePaintBucket", values}`; double-clicking the tool opens them, dialog `livePaintBucketOptions`) are `paintFills` (on), `paintStrokes` (off), `highlight` (on), `color` (the highlight color, `#rrggbb` or a layer color's name; `#ff2a2a`) and `width` (the face highlight's width, 1 to 20 screen diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 26681e5b7f..2280ab468d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1433,6 +1433,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur As VectorCraft's own rule, `tool.setOption` for the Shape Builder, chosen or not, checks every value the same way: a switch that is not `true` or `false`, a `pickColorFrom` or `selection` that is not one of its names, a `gapLength` that is not a number, a `highlightColor` that is not an RGB color or a layer color's name, or that has an alpha part, and an option the tool does not have give an error naming the option, and nothing is set or saved. A `gapLength` outside 0 to 72 pt is still set to the nearest end of that range. Evidence: `tests::build::{shape_builder_tool_options_refuse_invalid_values_and_change_nothing, shape_builder_tool_options_take_valid_values_and_clamp_the_gap}`. A drag of 3 screen pixels or more with the Live Paint Selection tool draws a marquee and selects the faces and edges it touches, partly enclosed ones included, and a Shift-drag adds them to the selection or removes them from it ([Modify Live Paint groups](https://web.archive.org/web/20261006115247/https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/modify-live-paint-groups.html)). A click and a marquee change the selection on release, and Escape before the release leaves the selection as it was. Locked Live Paint groups, those on a locked layer or artboard, and in isolation mode those outside the isolated art are not highlighted or selected. A drag with the Live Paint Selection tool draws a marquee and selects the faces and edges it touches, partly enclosed ones included, and a Shift-drag adds them to the selection or removes them from it ([Modify Live Paint groups](https://web.archive.org/web/20261006115247/https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/modify-live-paint-groups.html)). A click and a marquee change the selection on release, and Escape before the release leaves the selection as it was. Locked Live Paint groups, those on a locked layer or artboard, and in isolation mode those outside the isolated art are not highlighted or selected. + A double-click with the Live Paint Selection tool on a face selects it and the faces of its group that no stroked edge parts from it ([the help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/modify-live-paint-groups.html)), and on an edge selects it and the edges joined to it end to end that have its stroke paint ([the help page](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)); Shift-double-click adds them to the selection or removes them from it. Triple-click is not handled yet. When the object the Blend tool picked first is deleted or undone away before the second click, the next click on an object picks that object first instead of doing nothing. Regression tests: `blend_tool_picks_again_when_its_first_pick_is_gone` in `meshblend.rs` and `the_blend_tool_picks_again_when_its_first_pick_is_deleted` in `tests/blendopts.rs`. Window › Blend opens the Blend panel: Make, the selected blend's Spacing and Orientation (with no blend selected, the options new blends start with), Reverse Front to Back, Reverse Spine, Replace Spine, Expand and Release, each running its Object › Blend command. Make is dimmed until two or more objects are selected. Spacing and Orientation are dimmed until a blend or two or more objects are selected, and the other buttons are dimmed until a blend is selected. The Blend panel's Create Spine menu (Auto, Always, Never; `object.blend.createSpine`) sets whether new blends get a spine, from the Blend tool, Make and `object.blend.make` alike ([Blend panel overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/apply-transparency-and-blending/blend-panel-overview.html)). Auto and Always give every new blend a straight spine, as before; Never makes blends without one, which keep their steps on the straight lines between the keys, have Type and Ramp dimmed, refuse Reverse Spine and spine edits, release without a spine path, and get a spine from Replace Spine. The setting is kept with the preferences and leaves blends already made as they are; Auto as the starting value and keeping the setting across sessions are VectorCraft's own rules. Builds on #5177 by adambelis. Evidence: `tests::blendopts::{create_spine_sets_whether_new_blends_get_a_spine, a_blend_without_a_spine_has_none_to_edit_reverse_ease_or_release, whether_a_blend_has_a_spine_is_saved_and_old_blends_have_one}` and `panels::tests_blend::create_spine_sets_what_new_blends_get_whatever_is_selected`. From 8a449cb98edf71e59289a414f150e21435a07301 Mon Sep 17 00:00:00 2001 From: ncic <1848265+ncic@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:45:40 +0100 Subject: [PATCH 07/44] Pencil: badged cursors keep their badge with Use Precise Cursors The Pencil's and the Paintbrush's new-path, continue, close and straight-line cursors are drawn on the crosshair already; Precise Cursors no longer turns them into the plain crosshair, so the badge still says what the press or the release does. --- crates/tools/src/cursor.rs | 9 +++------ crates/tools/src/cursors.toml | 13 +++++++++++-- crates/tools/src/draw2/gesture/tests.rs | 4 ++-- crates/tools/src/lib.rs | 5 ++++- docs/ui-parity.md | 2 +- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/tools/src/cursor.rs b/crates/tools/src/cursor.rs index 85b9cf258e..56e2cd2ae7 100644 --- a/crates/tools/src/cursor.rs +++ b/crates/tools/src/cursor.rs @@ -259,7 +259,9 @@ impl Cursor { /// General › Use Precise Cursors: the drawing tools' pointers (the Pen's in every state, the /// Anchor Point and Curvature tools', the Eyedropper's, the Slice and Blend tools', the Live - /// Paint tools') become a plain crosshair at the hotspot. + /// Paint tools') become a plain crosshair at the hotspot. The Pencil's and the Paintbrush's + /// badged cursors are drawn on the crosshair already and keep their badge, which says what the + /// press or the release does (a new path, continuing, closing, a straight line). pub fn precise(self) -> Self { match self { Cursor::Pen @@ -282,12 +284,7 @@ impl Cursor { | Cursor::Lasso | Cursor::MagicWand | Cursor::Pencil - | Cursor::PencilNew - | Cursor::PencilClose - | Cursor::PencilStraight - | Cursor::PencilContinue | Cursor::Paintbrush - | Cursor::PaintbrushClose | Cursor::BlobBrush | Cursor::Eraser | Cursor::Smooth diff --git a/crates/tools/src/cursors.toml b/crates/tools/src/cursors.toml index 1c15b37d71..efa31a515d 100644 --- a/crates/tools/src/cursors.toml +++ b/crates/tools/src/cursors.toml @@ -628,7 +628,10 @@ hotspot = "The lead tip." [[cursor]] id = "PencilNew" group = "Drawing and editing tools" -shown_when = ["The Pencil tool before a press that draws a new freehand path: the pointer is not within Edit Selected Paths of a selected path that the press would continue from an end or redraw. Over such a path, and while a stroke is drawn, the Pencil cursor has no star."] +shown_when = [ + "The Pencil tool before a press that draws a new freehand path: the pointer is not within Edit Selected Paths of a selected path that the press would continue from an end or redraw. Over such a path, and while a stroke is drawn, the Pencil cursor has no star.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", +] hotspot = "The lead tip." look = "The Pencil cursor with a small star badge, where PencilClose has its ring." @@ -639,6 +642,7 @@ shown_when = [ "The Pencil tool with Alt pressed after the drag starts, when Alt key toggles to Smooth Tool is off: releasing the mouse button with Alt still held closes the path. A drag started with Shift or Alt held draws an open straight line and does not show this cursor.", "The Pencil tool while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", "The Pencil tool while a stroke continuing a selected open path, freehand or a straight line, is drawn to within Close Paths When Ends Are Within of that path's other end: releasing the mouse button there closes the path.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a ring badge (closing the loop), like PenClose." @@ -646,7 +650,10 @@ look = "The Pencil cursor with a ring badge (closing the loop), like PenClose." [[cursor]] id = "PencilStraight" group = "Drawing and editing tools" -shown_when = ["The Pencil tool with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line, at 45° steps from the Constrain Angle when Shift is held and at any angle with Alt alone."] +shown_when = [ + "The Pencil tool with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line, at 45° steps from the Constrain Angle when Shift is held and at any angle with Alt alone.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", +] hotspot = "The lead tip." look = "The Pencil cursor with a short level-line badge, where PencilClose has its ring." @@ -656,6 +663,7 @@ group = "Drawing and editing tools" shown_when = [ "The Pencil tool over an end of a selected open path, within Edit Selected Paths, before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press continues the path from that end, freehand, or with a straight line when Shift or Alt is held.", "The Pencil tool while a stroke continuing a selected path ends within Edit Selected Paths of an open end of another path selected at the press: releasing the mouse button connects the two.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a short badge line rising to the right, where PencilClose has its ring (the stroke carries on from the path)." @@ -672,6 +680,7 @@ group = "Drawing and editing tools" shown_when = [ "The Paintbrush tool with Alt held while drawing, or before a press when no path is selected that the Smooth tool can act on (with one selected, Smooth shows): releasing the mouse button with Alt still held closes the path.", "The Paintbrush tool while a stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The bristle tip." look = "The Paintbrush cursor with a ring badge (closing the loop), like PenClose." diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 65ad5f00e7..8ff1ac3d67 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -748,7 +748,7 @@ fn pencil_shows_the_straight_line_cursor_when_a_press_draws_a_line() { t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 40.0, -10.0).with_mods(shift)); assert_eq!(t.cursor(&cx, at, shift), Cursor::Pencil, "a freehand stroke"); assert_eq!(GestureTool::new("paintbrush").cursor(&cx, at, shift), Cursor::Paintbrush); - assert_eq!(Cursor::PencilStraight.precise(), Cursor::Crosshair, "Use Precise Cursors"); + assert_eq!(Cursor::PencilStraight.precise(), Cursor::PencilStraight, "Use Precise Cursors keeps the badge"); } /// Before a press, the Pencil shows its new-path cursor (the star) exactly when the press draws @@ -883,7 +883,7 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { let cx_none = crate::testutil::cx(&d, &none, &p); assert_eq!(GestureTool::new("pencil").cursor(&cx_none, end, shift), Cursor::PencilStraight, "nothing selected"); assert_eq!(GestureTool::new("paintbrush").cursor(&cx, end, shift), Cursor::Paintbrush); - assert_eq!(Cursor::PencilContinue.precise(), Cursor::Crosshair, "Use Precise Cursors"); + assert_eq!(Cursor::PencilContinue.precise(), Cursor::PencilContinue, "Use Precise Cursors keeps the badge"); } /// A Pencil stroke continuing a selected path shows the continuation cursor while a release would diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index 17e6f038e9..1a629d7ab8 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -660,7 +660,10 @@ mod cursor_tests { // Away from a selected path the Pencil shows its new-path cursor, drawn with a star; its // own cursor, for drawing and for a selected path under the pointer, borrows the crosshair. assert_eq!(cursor_at("pencil", &d, away), Cursor::PencilNew); - assert_eq!(Cursor::PencilNew.precise(), Cursor::Crosshair); + // Its badged cursors are drawn on the crosshair, and Use Precise Cursors keeps the badge. + for c in [Cursor::PencilNew, Cursor::PencilClose, Cursor::PencilStraight, Cursor::PencilContinue, Cursor::PaintbrushClose] { + assert_eq!(c.precise(), c, "{c:?}"); + } assert_eq!(Cursor::PencilNew.stand_in(), None); assert_eq!(Cursor::Pencil.precise(), Cursor::Crosshair); assert_eq!(Cursor::Pencil.stand_in(), Some(Cursor::Crosshair)); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index a7adedb22d..8b97b1a88b 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -341,7 +341,7 @@ tools, never checked side by side) isn't captured row by row. | Polygon/Star exact-size placement remembers the chosen sides/points for later drags and dialog openings; Cancel preserves the previous count | done | Session tool options; existing dialog-opening gestures retained; regression in `toolbar.rs` | | Tool double-click opens its options | done | | | Per-platform mapping (Cmd ↔ Ctrl, Option ↔ Alt) | done | | -| Eyedropper cursor; Use Precise Cursors | partial | original pipette at the sampling tip, distinct from the precise crosshair at five DPI scales (#1269); OS cursor display unverified; while Caps Lock is on, the tools that Use Precise Cursors changes show the crosshair, and the saved preference does not change (the desktop app reads the lock state on macOS, Windows and Linux X11, not on native Wayland; `view.capsLock` reads or sets it); a Use Precise Cursors command toggles the preference and can take a shortcut (#1663); cursors are painted into the window on Wayland (#1652, not run on Linux) | +| Eyedropper cursor; Use Precise Cursors | partial | original pipette at the sampling tip, distinct from the precise crosshair at five DPI scales (#1269); OS cursor display unverified; while Caps Lock is on, the tools that Use Precise Cursors changes show the crosshair, and the saved preference does not change; the Pencil's and the Paintbrush's cursors with a badge (new path, continue, close, straight line) are drawn on the crosshair and keep their badge (the desktop app reads the lock state on macOS, Windows and Linux X11, not on native Wayland; `view.capsLock` reads or sets it); a Use Precise Cursors command toggles the preference and can take a shortcut (#1663); cursors are painted into the window on Wayland (#1652, not run on Linux) | | macOS canvas cursor delivery (#2292) | partial | The UI prepares the existing glyphs as aligned 1×/2× RGBA requests; the desktop host builds AppKit cursors and owns cursor rectangles for the root document window. While it owns the cursor, the host sets it again on every pass, so the tool's cursor stays when the pointer moves (#6362). Detached document viewports and requests without a usable native bitmap retain the visible system fallback; precise-cursor and UI-scale preferences retained. Headless regressions cover routing, pixels/hotspots and lifecycle; native focus/menu/handback timing, mixed displays, Spotlight and full-screen Spaces remain unverified. Wayland still paints its glyphs. | | Every tool's modifiers checked against the shortcut reference | missing | this is the fidelity pass | From e7240b5a373753267b07eca20b5c2bc12dfdb3bd Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 17:06:43 -0800 Subject: [PATCH 08/44] i18n: Hungarian has a row for 1,813 of the messages the untranslated report lists The Hungarian catalog lacked a row for 1,853 of the status and error messages that the untranslated report finds in the sources, so the status bar showed those messages in English. - hu.tsv gains 1,813 `@msg` rows: the errors of commands and their parameter checks, scripts, file import and export, printing and plug-ins. The translations are VectorCraft's own, written from the English meaning with the catalog's conventions and its existing feature names. Placeholders, parameter names, command ids and literal values stay as the English writes them. - The 38 rows that open #7211, #7116 and #7330 add to hu.tsv are left out, since a second row for the same key fails bundled_catalogs_are_consistent. The report lists those 38 and two messages that no catalog has a row for. Menu strings, `tl!` literals, undo labels and swatch names are unchanged. - Two of the messages have two placeholders in a row. The catalog does not load such a row, so those two are still shown in English. The row for `{k} has control characters{_1}` does not match the File Info error for a multi-line field, where the last placeholder is empty. - The Hungarian row of docs/localization-parity.md gives the catalog's current message counts. Docs: docs/target-app-parity.md, docs/localization-parity.md. --- crates/ui-egui/src/i18n/hu.tsv | 1814 ++++++++++++++++++++++++++++++++ docs/localization-parity.md | 2 +- docs/target-app-parity.md | 1 + 3 files changed, 1816 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index ab06863782..e7c16017d1 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -7475,3 +7475,1817 @@ breakText Glyphs Glifák Grommets Ringlik Grommet Inset Ringlik távolsága a széltől Diameter Átmérő +# 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 a 'caller', a 'callee' és az 'arguments' nem használható szigorú módú függvényeken +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. Egy profilon egy Forgatás tengely körül effektus lehet. Inkább a meglévő effektus beállításait szerkeszd. +@msg Add Points at Crossings: a path went missing Pontok hozzáadása a kereszteződésekben: egy útvonal eltűnt +@msg Alignment must be one of the choices Az igazítás csak a választási lehetőségek egyike lehet +@msg Boolean.prototype.{name} was called on a value that is not a Boolean a Boolean.prototype.{name} hívása olyan értéken történt, amely nem Boolean +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB Azok a CMYK-képek, amelyek mintái nem voltak CMYK-ban olvashatók, RGB-ben lettek importálva +@msg CSS options: {e} CSS-beállítások: {e} +@msg CSV has a header but no data rows A CSV-ben van fejléc, de nincsenek adatsorok +@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark A CSV NUL bájtokat tartalmaz: mentsd UTF-8-ként, vagy bájtsorrendjellel ellátott UTF-16-ként +@msg CSV is empty A CSV üres +@msg CSV is larger than {MAX_CSV_BYTES} bytes A CSV nagyobb, mint {MAX_CSV_BYTES} bájt +@msg CSV is not UTF-16 A CSV nem UTF-16 kódolású +@msg CSV is not UTF-8 A CSV nem UTF-8 kódolású +@msg CSV would be larger than {MAX_CSV_BYTES} bytes A CSV nagyobb lenne, mint {MAX_CSV_BYTES} bájt +@msg Cannot write to the location of “{name}”; choose where to save it Nem lehet írni ennek a helyére: „{name}”; válaszd ki, hová kerüljön a mentés +@msg Choose original artwork, not a dimension's generated paths Eredeti grafikát válassz, ne egy méretvonal generált útvonalait +@msg Contour: select paths or shapes Kontúr: jelölj ki útvonalakat vagy alakzatokat +@msg Contour: the Offset Path effect is not available Kontúr: az Útvonal eltolása effektus nem érhető el +@msg Create needs fills, strokes or both A létrehozáshoz kitöltések, körvonalak vagy mindkettő kell +@msg DCS files can only be placed linked: use File › Place with Link on A DCS-fájlok csak csatolva helyezhetők el: használd a Fájl › Elhelyezés parancsot bekapcsolt Csatolás beállítással +@msg DXF options: {e} DXF-beállítások: {e} +@msg DXF scale must be a positive number up to {_1}, not {scale} A DXF-lépték pozitív szám lehet, legfeljebb {_1}; ez nem az: {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.prototype metódus hívása olyan értéken történt, amely nem Date objektum +@msg Dimension is locked or hidden A méretvonal zárolt vagy rejtett +@msg Dimension mode does not match its source A méretvonal módja nem egyezik a forrásával +@msg Draw Inside needs exactly one selected path, compound path or text A Rajzolás belülre módhoz pontosan egy kijelölt útvonal, összetett útvonal vagy szöveg kell +@msg EPS Save writes all artwork: use document.export for selected-only EPS Az EPS-mentés a teljes grafikát kiírja: csak a kijelölést tartalmazó EPS-hez használd a document.export parancsot +@msg EPS flattener: {e} EPS-lapító: {e} +@msg EPS options: {e} EPS-beállítások: {e} +@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}`: {allowed} +@msg Error.prototype.toString was called on a value that is not an object az Error.prototype.toString hívása olyan értéken történt, amely nem objektum +@msg Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft Az Exportálás képernyőkre ezeket írja: png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai vagy vectorcraft +@msg Flare click coordinates must be finite numbers A Fényvillanás kattintási koordinátái véges számok legyenek +@msg Function.prototype.toString was called on a value that is not a function a Function.prototype.toString hívása olyan értéken történt, amely nem függvény +@msg HTML Text applies to object slices of type only A HTML-szöveg csak szövegobjektumból készült szeletekre alkalmazható +@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) Az Objektumok azonosítása beállítás XML-azonosító: a névnek érvényes XML-azonosítónak kell lennie (betűvel, aláhúzásjellel vagy kettősponttal kezdődik; szóköz nélkül) +@msg Image Trace failed: internal error: its thread stopped (please report this bug) A képvektorizálás nem sikerült: belső hiba: a szála leállt (kérjük, jelentsd ezt a hibát) +@msg Image Trace failed: {e} A képvektorizálás nem sikerült: {e} +@msg Image Trace finished, but its document was closed A képvektorizálás elkészült, de a dokumentumát bezárták +@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again A képvektorizálás elkészült, de a kép vagy a beállításai közben megváltoztak: vektorizáld újra +@msg Import SVG Filter reads SVG files, not SVGZ Az SVG-szűrő importálása SVG-fájlokat olvas, SVGZ-fájlokat nem +@msg Invalid dimension offset Érvénytelen méretvonal-eltolás +@msg Invalid point Érvénytelen pont +@msg JSON.parse: {why} (at offset {at}) JSON.parse: {why} (eltolás: {at}) +@msg JSON.stringify cannot write a structure that contains itself a JSON.stringify nem tud olyan szerkezetet kiírni, amely önmagát tartalmazza +@msg Join: a subpath went missing Összekapcsolás: egy részútvonal eltűnt +@msg Line Gap must be a number A Sorköz szám kell legyen +@msg Lock Workspace is not available in VectorCraft A Munkaterület zárolása nem érhető el a VectorCraftban +@msg Move Panel to New Window is not available: floating panels stay in the main window A Panel áthelyezése új ablakba nem érhető el: a lebegő panelek a főablakban maradnak +@msg Namespace.prototype.toString needs a Namespace a Namespace.prototype.toString hívásához Namespace kell +@msg New Document profiles need the preferences folder, which this run doesn't have Az új dokumentumok profiljaihoz a beállítások mappája kell, amely ebben a futásban nincs meg +@msg Number.prototype.{name} was called on a value that is not a number a Number.prototype.{name} hívása olyan értéken történt, amely nem szám +@msg Object.create's prototype must be an object or null az Object.create prototípusa objektum vagy null lehet +@msg Object.{function} was given a value that is not an object az Object.{function} olyan értéket kapott, amely nem objektum +@msg Offset Path: the path is too complex to offset Útvonal eltolása: az útvonal túl összetett az eltoláshoz +@msg Offset Path: {e} Útvonal eltolása: {e} +@msg PDF content is incomplete or exceeds the reader's limits; no document was imported A PDF tartalma hiányos, vagy túllépi az olvasó korlátait; nem lett dokumentum importálva +@msg PDF flattener: {e} PDF-lapító: {e} +@msg PDF options: {e} PDF-beállítások: {e} +@msg QName.prototype.toString needs a QName a QName.prototype.toString hívásához QName kell +@msg Recolor Artwork is no longer open A Grafika átszínezése már nincs megnyitva +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object a RegExp.prototype.{name} hívása olyan értéken történt, amely nem RegExp objektum +@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 Az SVG nem UTF-8, UTF-16 vagy ISO 8859-1 kódolású +@msg Save writes one artboard: name one, or export several with document.export A mentés egy rajztáblát ír ki: nevezz meg egyet, vagy exportálj többet a document.export paranccsal +@msg Select a dimension first Előbb jelölj ki egy méretvonalat +@msg Select dimensions to update Jelöld ki a frissítendő méretvonalakat +@msg Select only dimensions Csak méretvonalakat jelölj ki +@msg Select two or more objects, or a group Jelölj ki két vagy több objektumot, vagy egy csoportot +@msg Shaper needs 1–64 source paths Az Alakformálóhoz 1–64 forrásútvonal kell +@msg Stencil Bridges: the selected art is too large or too detailed to bridge Sablonhidak: a kijelölt grafika túl nagy vagy túl részletes a hidak elhelyezéséhez +@msg String.prototype.toString was called on a value that is not a string a String.prototype.toString hívása olyan értéken történt, amely nem karakterlánc +@msg Switch x/y is for scatter graphs Az x/y felcserélése szóródiagramokhoz való +@msg Text options: {e} Szövegbeállítások: {e} +@msg The {name} profile could not be read, so the new document has the default settings: {reason} Ez a profil nem volt olvasható: {name}; ezért az új dokumentum az alapértelmezett beállításokat kapta: {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 Csapdázás: több mint {_1} objektumpár van elég közel a csapdázáshoz, vagy túl sok objektum esik azonos vízszintes tartományba; egyszerre kevesebb objektumot csapdázz +@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off Gyomláló keret és vonalak: a grafikán kívül nem fér el gyomlálóvonal, és a gyomláló keret ki van kapcsolva +@msg \\ at end of pattern \\ a minta végén +@msg `alignOn` cannot be empty `alignOn`: nem lehet üres +@msg `alignOn` must be a string `alignOn`: karakterlánc kell +@msg `align` is start, center or end `align`: start, center vagy end +@msg `align` must be a string `align`: karakterlánc kell +@msg `anchors` must be an array of [subpath, anchor] pairs `anchors`: [subpath, anchor] párok tömbje kell +@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors`: horgonypont-indexek listája kell (0, 1, 2…), ez nem az: {k} +@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors`: horgonypont-indexek listája kell (0, 1, 2…), ez nem az: {v} +@msg `answer` for this {kind} dialog must be one of: {_1} `answer`: ennél a párbeszédablaknál ({kind}) ezek egyike kell: {_1} +@msg `answers` has more than {MAX_ANSWERS} answers `answers`: több mint {MAX_ANSWERS} választ tartalmaz +@msg `answers` is larger than 1 MiB `answers`: nagyobb 1 MiB-nál +@msg `answers` must be a list of answers `answers`: válaszok listája kell +@msg `artboard` must be the index of an artboard of the document `artboard`: a dokumentum egyik rajztáblájának indexe kell +@msg `artboardsAt` must be [x, y] `artboardsAt`: [x, y] kell +@msg `artboards` must be a list of artboard indices `artboards`: rajztáblaindexek listája kell +@msg `artboards` names an artboard the document does not have `artboards`: olyan rajztáblát nevez meg, amely nincs a dokumentumban +@msg `artboards` names an artboard twice `artboards`: egy rajztáblát kétszer nevez meg +@msg `asset` is the asset id (see assets.list) `asset`: az elem azonosítója (lásd: assets.list) +@msg `at` must be [x, y] (got {at}) `at`: [x, y] kell (kapott érték: {at}) +@msg `at` must be a byte offset `at`: bájteltolás kell +@msg `at` must be a point [x, y] `at`: [x, y] pont kell +@msg `autoLeading` must be a number `autoLeading`: szám kell +@msg `background` must be "", "matte" or #rrggbb `background`: "", "matte" vagy #rrggbb kell +@msg `beside` needs `side` `beside`: `side` is kell hozzá +@msg `burasagari` must be "none", "standard" or "forced" `burasagari`: "none", "standard" vagy "forced" kell +@msg `center` must be [x, y] `center`: [x, y] kell +@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign`: "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" vagy "icfBottom" kell +@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection`: "default", "leftToRight" vagy "rightToLeft" kell +@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke`: `stroke: false` és `ids: []` kell hozzá, `stop` nélkül +@msg `clear` must be a list of type object ids, not {v} `clear`: szövegobjektum-azonosítók listája kell, ez nem az: {v} +@msg `clear` must list type objects; {_1} is not one `clear`: szövegobjektumok listája kell; ez nem az: {_1} +@msg `clear` must not list `id` `clear`: nem szerepelhet benne az `id` +@msg `code` must be a whole number `code`: egész szám kell +@msg `colorMode` must be one of rgb, cmyk `colorMode`: ezek egyike kell: rgb, cmyk +@msg `color` needs a `rule` to make the theme from `color`: `rule` is kell hozzá, amelyből a téma készül +@msg `colors` does not apply to the {name} reduction `colors`: ennél a színcsökkentésnél nem használható: {name} +@msg `colors` is empty `colors`: üres +@msg `colors` must be a whole number from 2 to 256 `colors`: 2 és 256 közötti egész szám kell +@msg `colors` must be at least 9 for the uniform reduction `colors`: az egyenletes színcsökkentéshez legalább 9 kell +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors`: 1 és {MAX_THEME_COLORS} közötti érték kell (kapott érték: {n}) +@msg `columnType` is vertical, uniform, repeating or sliding `columnType`: vertical, uniform, repeating vagy sliding +@msg `columns` must be a whole number 1..50 `columns`: egész szám kell, 1..50 +@msg `commands` must not be empty `commands`: nem lehet üres +@msg `contents` goes with `to` `contents`: a `to` kulccsal együtt használható +@msg `corners` must be sharp, round or square, not `{other}` `corners`: sharp, round vagy square kell, ez nem az: `{other}` +@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners`: horgonypont-indexek listája kell (0, 1, 2…), ez nem az: {v} +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count`: 0 és {MAX_COUNT} közötti egész szám kell +@msg `count` must be a whole number from 1 to 100 `count`: 1 és 100 közötti egész szám kell +@msg `dataBase64` is larger than the module limit `dataBase64`: nagyobb a modulok méretkorlátjánál +@msg `dataBase64` is not valid base64 `dataBase64`: nem érvényes base64 +@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly`: nem használható együtt ezekkel: `ids`, `reverse`, `append` +@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth` és `childLimit`: az összegzést szűkítik; add meg ezt: `summary: true` +@msg `design` is a design name, or null for plain columns `design`: diagramminta neve, vagy null az egyszerű oszlopokhoz +@msg `design` is a design name, or null for the default marker `design`: diagramminta neve, vagy null az alapértelmezett jelölőhöz +@msg `destinationFolder` is required when destination is `folder` `destinationFolder`: kötelező, ha a cél `folder` +@msg `destination` must be `none`, `saveAndClose` or `folder` `destination`: `none`, `saveAndClose` vagy `folder` kell +@msg `diameter` must be from 0.1 to 2500 pt `diameter`: 0.1 és 2500 pt közötti érték kell +@msg `digits` must be one of {_1} `digits`: ezek egyike kell: {_1} +@msg `dimImages` is a percentage or false `dimImages`: százalék vagy false +@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction`: "auto", "leftToRight" vagy "rightToLeft" kell +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount`: 0 és 100 közötti szám kell +@msg `each` is the value one design stands for, more than 0 `each`: az az érték, amelyet egy minta jelöl; 0-nál nagyobb +@msg `end` must be "start" or "end" `end`: "start" vagy "end" kell +@msg `ends` applies to joining exactly two paths `ends`: pontosan két útvonal összekapcsolásakor használható +@msg `event` must be one of {_1} `event`: ezek egyike kell: {_1} +@msg `every` must be "all", "odd" or "even", not {v} `every`: "all", "odd" vagy "even" kell, ez nem az: {v} +@msg `expand` must be true or false `expand`: true vagy false kell +@msg `fidelity` must be from {lo} to {hi} pt `fidelity`: {lo} és {hi} pt közötti érték kell +@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files`: a legutóbbi text.findFontFiles keresés által talált betűtípusfájlok 1–1000 elérési útja kell +@msg `focal` and `radius` cannot be used together `focal` és `radius`: nem használhatók együtt +@msg `focal` must be an array of [subpath, anchor] pairs `focal`: [subpath, anchor] párok tömbje kell +@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal`: ezt nevezi meg: [{si}, {ai}], de ez nem az útvonal horgonypontja +@msg `folder` must be an absolute folder path `folder`: abszolút mappaútvonal kell +@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes`: tengelycímkék és értékek objektuma kell, például {"wght"} +@msg `format` must be a string `format`: karakterlánc kell +@msg `format` must be one of {_1} `format`: ezek egyike kell: {_1} +@msg `fractions` is chop or scale `fractions`: chop vagy scale +@msg `from` and `to` must differ `from` és `to`: különbözniük kell +@msg `from` has an empty frame `from`: üres a kerete +@msg `from` must be a non-negative integer `from`: nemnegatív egész szám kell +@msg `from` must be area type or type on a path `from`: területszöveg vagy útvonalszöveg kell +@msg `gid` must be a glyph id, not {v} `gid`: glifaazonosító kell, ez nem az: {v} +@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate`: egyetlen karakterhez tartozik, nem karakterstílushoz (a text.setGlyphAlternate állítja be) +@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate`: csak null adható meg, amely megszünteti a glifaválasztást (a text.setGlyphAlternate hoz létre ilyet) +@msg `group` must be one of {_1} `group`: ezek egyike kell: {_1} +@msg `group` must be the Objects on Path group of `member` and `target` `group`: a `member` és a `target` Objektumok útvonalon csoportja kell +@msg `guide` must be a guide index (guide.list) `guide`: segédvonalindex kell (guide.list) +@msg `hAlign` must be default, left, center or right `hAlign`: default, left, center vagy right kell +@msg `id` is an object id, not {v} `id`: objektumazonosító, ez nem az: {v} +@msg `id` must be a node id `id`: csomópont-azonosító kell +@msg `id` must be an object id, not {v} `id`: objektumazonosító kell, ez nem az: {v} +@msg `ids` must name Objects on Path groups or objects in them `ids`: Objektumok útvonalon csoportokat vagy a bennük lévő objektumokat kell megneveznie +@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids`: legfeljebb {MAX_INSPECT_IDS} azonosítót fogad +@msg `imageMap` must be none, rectangle or polygon `imageMap`: none, rectangle vagy polygon kell +@msg `includeArtboardFill` must be true or false `includeArtboardFill`: true vagy false kell +@msg `includeSubdirs` must be true or false `includeSubdirs`: true vagy false kell +@msg `index` must be a point index `index`: pontindex kell +@msg `index` must be a whole number or null `index`: egész szám vagy null kell +@msg `indices` must be a non-empty array of item indices `indices`: elemindexek nem üres tömbje kell +@msg `item` needs `index` `item`: `index` is kell hozzá +@msg `items` is a list of {kind, item} `items`: {kind, item} elemek listája +@msg `items` is empty `items`: üres +@msg `items` must be a list of {id, points} `items`: {id, points} elemek listája kell +@msg `items` must be a list of {{id, anchors?}}, not {v} `items`: {{id, anchors?}} elemek listája kell, ez nem az: {v} +@msg `items` must be an array of {id, anchors} `items`: {id, anchors} elemek tömbje kell +@msg `items` must be an array of {{id, corners?}}, not {v} `items`: {{id, corners?}} elemek tömbje kell, ez nem az: {v} +@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida`: "none", "short", "medium", "long" vagy "stylistic" kell +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore`: a sor első karakteréhez tartozik, nem karakterstílushoz (a text.setRangeStyle állítja be) +@msg `kind` is one of {_1} `kind`: ezek egyike: {_1} +@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind`: "round", "invertedRound" vagy "chamfer" kell, ez nem az: {v} +@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind`: `text`, `visibility`, `image` vagy `graph` kell +@msg `kind` must be a string `kind`: karakterlánc kell +@msg `kind` must be image, noImage or htmlText `kind`: image, noImage vagy htmlText kell +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType`: "pushInFirst", "pushOutFirst" vagy "pushOutOnly" kell +@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku`: "none", "hard" vagy "soft" kell +@msg `label` must be a non-empty name of at most 200 characters `label`: nem üres, legfeljebb 200 karakteres név kell +@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language`: nyelvnév vagy nyelvcímke kell, például "Turkish" vagy "tr" +@msg `leader` must be a string `leader`: karakterlánc kell +@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel`: "romanBaseline" vagy "emBoxTop" kell +@msg `leading` must be from 0.1 to 5000, or null for Auto `leading`: 0.1 és 5000 közötti érték kell, vagy null az automatikushoz +@msg `length` must be a number of at most 1000000 pt either way `length`: szám kell, bármelyik irányban legfeljebb 1000000 pt +@msg `line` must be true or false `line`: true vagy false kell +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max`: 1 és {MAX_SUGGESTIONS} közötti érték kell +@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle` ({min_angle}°): nem lehet nagyobb ennél: `maxAngle` ({max_angle}°) +@msg `mode` must be "handles", "straight" or "smooth" `mode`: "handles", "straight" vagy "smooth" kell +@msg `mode` must be "straight", "bezier" or "arc" `mode`: "straight", "bezier" vagy "arc" kell +@msg `mode` must be one of {_1} `mode`: ezek egyike kell: {_1} +@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi`: "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" vagy "lineEndHalf" kell +@msg `name` must be a name that is not empty `name`: nem üres név kell +@msg `name` must not be empty `name`: nem lehet üres +@msg `names` is a list of preset names `names`: előbeállítás-nevek listája +@msg `names` lists preset names `names`: előbeállítás-neveket sorol fel +@msg `names` must be a list of names `names`: nevek listája kell +@msg `names` must be a list of swatch names `names`: színmintanevek listája kell +@msg `names` must be an array of style names `names`: stílusnevek tömbje kell +@msg `new` is a design name, or true `new`: diagramminta neve vagy true +@msg `origin` must be [x, y] in points `origin`: [x, y] kell pontban megadva +@msg `path` must be a folder `path`: mappa kell +@msg `path` must be a path object `path`: útvonalobjektum kell +@msg `picks` must be an array (got {v}) `picks`: tömb kell (kapott érték: {v}) +@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks`: 1 és {MAX_PICKS} közötti számú választást kell tartalmaznia (kapott érték: {_1}) +@msg `pivot` must be a whole number from 0 to 8 `pivot`: 0 és 8 közötti egész szám kell +@msg `points` must be a list of [x, y] points `points`: [x, y] pontok listája kell +@msg `points` must be a list of point indexes `points`: pontindexek listája kell +@msg `points` must be an array of point indices `points`: pontindexek tömbje kell +@msg `points` must be point indexes `points`: pontindexek kellenek +@msg `points` must hold from 2 to 100000 points `points`: 2 és 100000 közötti számú pontot kell tartalmaznia +@msg `port` must be "in" or "out" `port`: "in" vagy "out" kell +@msg `position` is above or below `position`: above vagy below +@msg `preset` must be a string `preset`: karakterlánc kell +@msg `radius` must be 0 or more `radius`: 0 vagy nagyobb érték kell +@msg `ratio` must be from 0.1 to 0.5 `ratio`: 0.1 és 0.5 közötti érték kell +@msg `rect` must be [x, y, width, height] `rect`: [x, y, width, height] kell +@msg `rect` must be [x, y, width, height] with a positive size `rect`: [x, y, width, height] kell pozitív mérettel +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect`: [x0, y0, x1, y1] kell (kapott érték: {r}) +@msg `relation` must be overlap, inside or outside `relation`: overlap, inside vagy outside kell +@msg `replace` must be a string `replace`: karakterlánc kell +@msg `reversed` must be true or false `reversed`: true vagy false kell +@msg `rotate` must be a finite number `rotate`: véges szám kell +@msg `rows` must be lists of numbers or nulls, not {r} `rows`: számok vagy null értékek listái kellenek, ez nem az: {r} +@msg `rule` must be nonZero or evenOdd `rule`: nonZero vagy evenOdd kell +@msg `scale` must be a number from 0.001 to 1000 `scale`: 0.001 és 1000 közötti szám kell +@msg `scale` must be a number in (0, 100] `scale`: a (0, 100] tartományba eső szám kell +@msg `scope.{key}` must be a non-empty array `scope.{key}`: nem üres tömb kell +@msg `scope` must be "document", {artboards} or {layers} `scope`: "document", {artboards} vagy {layers} kell +@msg `scope` must be "selection" or "document", not {other} `scope`: "selection" vagy "document" kell, ez nem az: {other} +@msg `script` must be a string `script`: karakterlánc kell +@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes`: adatsorindexek nem üres listája +@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes`: `type` vagy `valueAxis` is kell hozzá +@msg `shift` must be a number of points `shift`: pontban megadott szám kell +@msg `showCenter` must be true or false `showCenter`: true vagy false kell +@msg `side` must be "left", "right", "top" or "bottom" `side`: "left", "right", "top" vagy "bottom" kell +@msg `side` places the artboard: leave out x, y, width and height `side`: ez helyezi el a rajztáblát; hagyd el az x, y, width és height értéket +@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification`: "fullJustify", "alignLeft", "alignCenter" vagy "alignRight" kell +@msg `size` is a percentage of the base text's size, 1 to 100 `size`: az alapszöveg méretének százaléka, 1 és 100 között +@msg `source` and `target` are the same object `source` és `target`: ugyanaz az objektum +@msg `source` is the only path in series group `target` `source`: az egyetlen útvonal ebben a sorozatcsoportban: `target` +@msg `source` must be `folder` or `datasets` `source`: `folder` vagy `datasets` kell +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing`: ezzel több mint {MAX_COUNT} vonal jutna a keretre +@msg `spacing` must be a number `spacing`: szám kell +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing`: pontban megadott, 0 vagy nagyobb szám, vagy "auto" kell +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing`: 0-nál nagyobb és legfeljebb {MAX_DISTANCE} pt érték kell +@msg `start` must be a finite number `start`: véges szám kell +@msg `starts` must be an array of anchor indices (or nulls) `starts`: horgonypont-indexek (vagy null értékek) tömbje kell +@msg `status` must be linked, embedded, missing or modified, or a list of them `status`: linked, embedded, missing vagy modified kell, vagy ezek listája +@msg `stop` must be a stop index `stop`: színátmenetpont-index kell +@msg `story` styles the whole story: give no `start` or `end` `story`: a teljes szövegfolyamot formázza; ne adj meg `start` vagy `end` értéket +@msg `story` takes the whole story: give no `start` or `end` `story`: a teljes szövegfolyamra vonatkozik; ne adj meg `start` vagy `end` értéket +@msg `strokeMode` applies to strokes (stroke: true) `strokeMode`: körvonalakra vonatkozik (stroke: true) +@msg `style` must be roman or japanese `style`: roman vagy japanese kell +@msg `subpaths` must be two integer indices `subpaths`: két egész indexet kell megadni +@msg `symbol` must be a symbol name `symbol`: szimbólumnév kell +@msg `target` must be "object" or "contents", not {v} `target`: "object" vagy "contents" kell, ez nem az: {v} +@msg `target` must be an object of the same Objects on Path group as `member` `target`: ugyanannak az Objektumok útvonalon csoportnak egy objektuma kell, mint a `member` +@msg `tint` applies to global and spot colours `tint`: globális színekre és direktszínekre vonatkozik +@msg `tint` goes with a global or spot `swatch` `tint`: globális vagy direktszínű `swatch` értékkel együtt használható +@msg `tint` must be a number (%) `tint`: szám kell (%) +@msg `tint` needs a global or spot colour `tint`: globális szín vagy direktszín kell hozzá +@msg `to` is already threaded `to`: már láncolva van +@msg `to` is the design's new name `to`: a diagramminta új neve +@msg `to` must be [x, y] `to`: [x, y] kell +@msg `to` must be a closed path, area type or type on a path other than `from` `to`: zárt útvonal, területszöveg vagy útvonalszöveg kell, amely nem azonos ezzel: `from` +@msg `to` must be a whole number `to`: egész szám kell +@msg `to` takes the rows of one object's stack, not aligned rows `to`: egyetlen objektum halmának sorait fogadja, nem igazított sorokat +@msg `tolerance` must be from 0 to 100 pt `tolerance`: 0 és 100 pt közötti érték kell +@msg `useLab` must be true or false `useLab`: true vagy false kell +@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign`: default, top, middle, baseline vagy bottom kell +@msg `values` must be a list `values`: lista kell +@msg `values` must be an object `values`: objektum kell +@msg `values` must name hex, rgb, cmyk, lab or gray `values`: ezek közül kell megneveznie: hex, rgb, cmyk, lab vagy gray +@msg `values` names a kind twice `values`: egy fajtát kétszer nevez meg +@msg `version` must be 0 or {_1} `version`: 0 vagy {_1} kell +@msg `version` must be 0 or {newest} `version`: 0 vagy {newest} kell +@msg `warichuAlign` must be one of {_1} `warichuAlign`: ezek egyike kell: {_1} +@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines`: {_1} és {_2} közötti egész szám kell +@msg `weight` must be from 0.01 to 1000 pt `weight`: 0.01 és 1000 pt közötti érték kell +@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights`: {_1} számból álló lista kell, soronként egy (kapott érték: {v}) +@msg `xml` must be a string `xml`: karakterlánc kell +@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it `{KEY}`, {n}. bejegyzés: más behúzásokat ad a(z) {_1} objektum {start}. bájtjánál kezdődő bekezdésnek, mint egy korábbi bejegyzés +@msg `{KEY}` entry {n} must be an object `{KEY}`, {n}. bejegyzés: objektum kell +@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` `{KEY}`, {n}. bejegyzés: sem a `leftIndent`, sem a `firstLineIndent` értéket nem állítja be +@msg `{KEY}` entry {n}: `id` must be an object id `{KEY}`, {n}. bejegyzés: `id`: objektumazonosító kell +@msg `{KEY}` entry {n}: `start` must be a byte offset `{KEY}`, {n}. bejegyzés: `start`: bájteltolás kell +@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt `{KEY}`, {n}. bejegyzés: `{k}`: -{MAX_PARA} és {MAX_PARA} pt közötti szám kell +@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} `{KEY}`, {n}. bejegyzés: a(z) {_1} objektumnak nincs a(z) {start}. bájtnál kezdődő bekezdése +@msg `{KEY}` entry {n}: unknown key `{k}` `{KEY}`, {n}. bejegyzés: ismeretlen kulcs: `{k}` +@msg `{KEY}` entry {n}: {_1} is not a type object `{KEY}`, {n}. bejegyzés: nem szövegobjektum: {_1} +@msg `{KEY}` is empty `{KEY}`: üres +@msg `{KEY}` must be a list `{KEY}`: lista kell +@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}`: legfeljebb {MAX_PARAGRAPH_INDENTS} bejegyzést fogad (kapott érték: {_1}) +@msg `{KEY}` takes no `{other}` beside it `{KEY}`: mellette nem adható meg ez: `{other}` +@msg `{_1}` can't be edited or removed nem szerkeszthető és nem távolítható el: `{_1}` +@msg `{_1}` is built in and always listed beépített elem, mindig szerepel a listán: `{_1}` +@msg `{_1}` is not a font file the last text.findFontFiles search found nem a legutóbbi text.findFontFiles keresés által talált betűtípusfájl: `{_1}` +@msg `{_1}` would be overwritten by the package: choose a different folder or name a csomag felülírná ezt: `{_1}`; válassz másik mappát vagy nevet +@msg `{base}` is larger than {_1} MB nagyobb, mint {_1} MB: `{base}` +@msg `{base}` is not in the DCS file's folder nincs a DCS-fájl mappájában: `{base}` +@msg `{file}` has no art to place nincs benne elhelyezhető grafika: `{file}` +@msg `{file}` is over {_1} MB nagyobb, mint {_1} MB: `{file}` +@msg `{f}` belongs to a VectorCraft that is running egy futó VectorCrafthoz tartozik: `{f}` +@msg `{f}` is the copy of a document open now egy most megnyitott dokumentum másolata: `{f}` +@msg `{id}` is no writable format (see document.formats) nem írható formátum: `{id}` (lásd: document.formats) +@msg `{id}` isn't a loaded library nem betöltött könyvtár: `{id}` +@msg `{key}` has no colours `{key}`: nincsenek benne színek +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}`: több mint {MAX_EDIT_WORDS} szót tartalmaz +@msg `{key}` is a design name `{key}`: diagramminta neve +@msg `{key}` is a design name or a list of them `{key}`: diagramminta neve vagy ilyen nevek listája +@msg `{key}` is longer than {MAX_NAME} characters `{key}`: hosszabb {MAX_NAME} karakternél +@msg `{key}` is out of range `{key}`: tartományon kívül esik +@msg `{key}` must be 0 or more `{key}`: 0 vagy nagyobb érték kell +@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}`: [min, desired, max] vagy {{min?, desired?, max?}} kell, százalékban +@msg `{key}` must be [x, y] numbers `{key}`: [x, y] számpár kell +@msg `{key}` must be a list of words `{key}`: szavak listája kell +@msg `{key}` must be a list, not {v} `{key}`: lista kell, ez nem az: {v} +@msg `{key}` must be a non-negative integer `{key}`: nemnegatív egész szám kell +@msg `{key}` must be a number or null, not {v} `{key}`: szám vagy null kell, ez nem az: {v} +@msg `{key}` must be a number {_1}..{_2} `{key}`: szám kell, {_1}..{_2} +@msg `{key}` must be a whole number of 0 or more `{key}`: 0 vagy nagyobb egész szám kell +@msg `{key}` must be a whole number of at least 0, not {n} `{key}`: legalább 0 értékű egész szám kell, ez nem az: {n} +@msg `{key}` must be an appearance item index (paint order) or null `{key}`: megjelenési elem indexe (festési sorrend) vagy null kell +@msg `{key}` must be an array of asset ids (see assets.list) `{key}`: elemazonosítók tömbje kell (lásd: assets.list) +@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}`: 0 és 180 fok közötti érték kell, ez nem az: {a} +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}`: 0 és {MAX_DISTANCE} pt közötti érték kell +@msg `{key}` must be from {_1} to {shift_max} `{key}`: {_1} és {shift_max} közötti érték kell +@msg `{key}` must be from {lo} to {hi}, not {v} `{key}`: {lo} és {hi} közötti érték kell, ez nem az: {v} +@msg `{key}` must be one of auto, always, never `{key}`: ezek egyike kell: auto, always, never +@msg `{key}` must be one of miter, round, bevel `{key}`: ezek egyike kell: miter, round, bevel +@msg `{key}` must be one word of at most {_1} characters `{key}`: egyetlen, legfeljebb {_1} karakteres szó kell +@msg `{key}` must hold single words of at most {_1} characters `{key}`: különálló, legfeljebb {_1} karakteres szavakat kell tartalmaznia +@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}`: egy Objektumok útvonalon csoport útvonalához csatolt objektumot kell megneveznie +@msg `{key}` names an object that is hidden or locked `{key}`: rejtett vagy zárolt objektumot nevez meg +@msg `{key}`: not a colour: {v} `{key}`: nem szín: {v} +@msg `{k}.family` is required `{k}.family`: kötelező +@msg `{k}` is a number 0..1 `{k}`: szám, 0..1 +@msg `{k}` is true or false `{k}`: true vagy false +@msg `{k}` isn't an export setting: {_1} `{k}`: nem exportálási beállítás: {_1} +@msg `{k}` must be a byte offset `{k}`: bájteltolás kell +@msg `{k}` must be a number `{k}`: szám kell +@msg `{k}` must be a number of points `{k}`: pontban megadott szám kell +@msg `{k}` must be a string `{k}`: karakterlánc kell +@msg `{k}` must be a whole number `{k}`: egész szám kell +@msg `{k}` must be from {lo} to {hi} for {key} `{k}`: {lo} és {hi} közötti érték kell ehhez: {key} +@msg `{k}` must be {what}, not {v} `{k}`: ez kell: {what}; ez nem az: {v} +@msg `{l}` isn't a global or spot colour nem globális szín és nem direktszín: `{l}` +@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText nem bitkép: `{m}`; használd a clipboard.importSvg, az importPdf vagy az importText parancsot +@msg `{name}` can't be deleted nem törölhető: `{name}` +@msg `{name}` can't be duplicated nem kettőzhető: `{name}` +@msg `{name}` can't be edited nem szerkeszthető: `{name}` +@msg `{name}` can't be edited this way így nem szerkeszthető: `{name}` +@msg `{name}` can't be renamed nem nevezhető át: `{name}` +@msg `{name}` can't name a folder nem nevezhet meg mappát: `{name}` +@msg `{name}` has no art to place nincs benne elhelyezhető grafika: `{name}` +@msg `{name}` has no text to place nincs benne elhelyezhető szöveg: `{name}` +@msg `{name}` in the Word document is not UTF-8 or UTF-16 text a Word-dokumentumban lévő `{name}` nem UTF-8 vagy UTF-16 szöveg +@msg `{name}` in the Word document is not well-formed XML: {e} a Word-dokumentumban lévő `{name}` nem jól formált XML: {e} +@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep a Word-dokumentumban lévő `{name}` elemei {MAX_NESTING} szintnél mélyebben vannak egymásba ágyazva +@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}` beépített előbeállítás, csak olvasható: mentsd más néven +@msg `{name}` is a built-in preset: save under another name `{name}` beépített előbeállítás: mentsd más néven +@msg `{name}` is a built-in profile and stays `{name}` beépített profil, megmarad +@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` külön fájl, amely csak a DCS-fájl elérési útja mellett található meg +@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}` ennek a tónusa: `{base}`; ezt szerkeszd: `{base}` (egy tónusszínminta csak új nevet kaphat) +@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}` munkaszíntér: előbb válassz másikat az edit.colorSettings paranccsal +@msg `{name}` is built in and stays `{name}` beépített elem, megmarad +@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}` csak a saját fajtájának megfelelő festést fogad (szín, színátmenet vagy mintázat) +@msg `{name}`: {_1}: {e} `{name}`: {_1}: {e} +@msg `{n}` can't be deleted nem törölhető: `{n}` +@msg `{n}` can't be merged nem egyesíthető: `{n}` +@msg `{n}` can't be moved nem helyezhető át: `{n}` +@msg `{n}` is not a colour swatch nem színes színminta: `{n}` +@msg `{n}` isn't a solid colour nem egyszínű szín: `{n}` +@msg `{n}`: colour groups hold solid colours only `{n}`: a színcsoportok csak egyszínű színeket tartalmazhatnak +@msg `{other}` doesn't trace an image nem vektorizál képet: `{other}` +@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}`: a lépés hivatkozásai több mint {_1} MB értéket másolnak be +@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" nem tengelycímke: `{tag}`; a címke négy karakterből áll, például "wght" +@msg `{to}` is empty or already used üres vagy már használatban van: `{to}` +@msg `{u}` is no Unicode character nem Unicode-karakter: `{u}` +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}`: hosszabb {MAX_TEXT} bájtnál +@msg `{what}` must be text `{what}`: szöveg kell +@msg `{word}` isn't in the custom dictionary nincs az egyéni szótárban: `{word}` +@msg a CMap has too many mappings egy CMap túl sok leképezést tartalmaz +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard egy DXF-rajznak egy origója van: add meg a useArtboards: true értéket, ha rajztáblánként egy rajz kell +@msg a PDF presets file with over {_1} presets olyan PDF-előbeállításfájl, amelyben több mint {_1} előbeállítás van +@msg a \\u escape needs four hexadecimal digits a \\u escape-sorozathoz négy hexadecimális számjegy kell +@msg a batch preview cannot switch documents a kötegelt előnézet nem válthat dokumentumot +@msg a brush named `{nn}` exists már van ilyen nevű ecset: `{nn}` +@msg a comma in an XPath expression must separate function arguments XPath-kifejezésben a vessző csak függvényargumentumokat választhat el +@msg a container of the listed objects is gone a felsorolt objektumok egyik tárolója már nincs meg +@msg a content stream can't be read egy tartalomfolyam nem olvasható +@msg a content stream isn't a stream egy tartalomfolyam nem adatfolyam +@msg a design holds at most {_1} objects egy diagramminta legfeljebb {_1} objektumot tartalmazhat +@msg a design needs a name a diagrammintának név kell +@msg a dictionary grew too large egy szótár túl nagyra nőtt +@msg a document holds at most {_1} SVG filters egy dokumentum legfeljebb {_1} SVG-szűrőt tartalmazhat +@msg a document keeps at most {_1} graph designs egy dokumentum legfeljebb {_1} diagrammintát tárol +@msg a document keeps at most {_1} saved selections egy dokumentum legfeljebb {_1} mentett kijelölést tárol +@msg a document keeps at most {_1} views egy dokumentum legfeljebb {_1} nézetet tárol +@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files egy dokumentum legfeljebb {MAX_SVG_SCRIPTS} JavaScript-fájlt csatolhat +@msg a document with unsaved changes has no recovery copy a mentetlen módosításokat tartalmazó dokumentumnak nincs helyreállítási másolata +@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes egy feConvolveMatrix szűrő rendje {x}×{y}, ami túlcsordítja az usvg kernelméret-szorzását +@msg a font's data is too large egy betűtípus adatai túl nagyok +@msg a font's subroutines are too large egy betűtípus szubrutinjai túl nagyok +@msg a function declaration without a name név nélküli függvénydeklaráció +@msg a gradient preset is already called {n} már van ilyen nevű színátmenet-előbeállítás: {n} +@msg a graph has at most {_1} series, so it transposes with at most that many categories egy diagramnak legfeljebb {_1} adatsora lehet, ezért legfeljebb ennyi kategóriával cserélhető fel a sor és az oszlop +@msg a host value has too many entries egy gazdaérték túl sok elemet tartalmaz +@msg a host value is nested too deep egy gazdaérték túl mélyen van egymásba ágyazva +@msg a layer can't be merged into its own sublayer egy réteg nem egyesíthető a saját alrétegébe +@msg a layer's form box isn't an array egy réteg formaobjektumának kerete nem tömb +@msg a layer's form can't be read egy réteg formaobjektuma nem olvasható +@msg a layer's form has no box egy réteg formaobjektumának nincs kerete +@msg a layer's form has no dictionary egy réteg formaobjektumának nincs szótára +@msg a legend draws at most {MAX_CHIPS} swatches a jelmagyarázat legfeljebb {MAX_CHIPS} színmintát rajzol ki +@msg a name names one asset: give multiple: false, or one object egy név egy elemet nevez meg: adj meg multiple: false értéket, vagy egyetlen objektumot +@msg a namespace declaration uses a reserved prefix or URI egy névtér-deklaráció foglalt előtagot vagy URI-t használ +@msg a namespace qualifier is undefined egy névtérminősítő nincs definiálva +@msg a namespace with an empty URI cannot have a prefix üres URI-jú névtérnek nem lehet előtagja +@msg a number has no digits egy számban nincsenek számjegyek +@msg a number's exponent has no digits egy szám kitevőjében nincsenek számjegyek +@msg a number's fraction has no digits egy szám törtrészében nincsenek számjegyek +@msg a page can't be read egy oldal nem olvasható +@msg a page object is used by something else egy oldalobjektumot valami más is használ +@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) az oldalbélyegkép RGB-képpontokból álljon, oldalanként 1 és {THUMBNAIL_SIZE} között (kapott méret: {w} × {h}) +@msg a pattern layer doesn't end what it starts egy mintázatréteg nem zárja le, amit megkezd +@msg a pattern layer's colors hold something else egy mintázatréteg színei mást tartalmaznak +@msg a pattern named `{n}` exists már van ilyen nevű mintázat: `{n}` +@msg a plug-in with id {_1} is already installed már telepítve van ilyen azonosítójú bővítmény: {_1} +@msg a preset named `{n}` exists már van ilyen nevű előbeállítás: `{n}` +@msg a print presets file with over {_1} presets olyan nyomtatási előbeállításfájl, amelyben több mint {_1} előbeállítás van +@msg a profile is already called {n} már van ilyen nevű profil: {n} +@msg a property descriptor cannot have both a value or writable and a get or set egy tulajdonságleíróban nem lehet egyszerre value vagy writable és get vagy set +@msg a property descriptor must be an object a tulajdonságleíró objektum kell legyen +@msg a property's {name} must be a function or undefined egy tulajdonság {name} értéke függvény vagy undefined lehet +@msg a row can't go into itself egy sor nem kerülhet önmagába +@msg a series goes on the left or the right value axis egy adatsor a bal vagy a jobb értéktengelyre kerül +@msg a start must be an anchor index ≥ 0 or null a kezdőpont ≥ 0 horgonypontindex vagy null lehet +@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied egy benne lévő adatfolyam kibontva 256 MB-nál nagyobb lenne, vagy a dekódolási paraméterei nem alkalmazhatók +@msg a string holds a control character egy karakterlánc vezérlőkaraktert tartalmaz +@msg a string holds an escape sequence JSON does not have egy karakterlánc olyan escape-sorozatot tartalmaz, amely a JSON-ban nem létezik +@msg a string is not closed egy karakterlánc nincs lezárva +@msg a style named `{name}` already exists már van ilyen nevű stílus: `{name}` +@msg a style named `{to}` already exists már van ilyen nevű stílus: `{to}` +@msg a swatch needs a colour, gradient or pattern a színmintához szín, színátmenet vagy mintázat kell +@msg a symbol named `{n}` already exists már van ilyen nevű szimbólum: `{n}` +@msg a theme has 1 to {THEME_SIZE} colours egy témában 1–{THEME_SIZE} szín lehet +@msg a theme is already called {name} (replace: "{name}" overwrites it) már van ilyen nevű téma: {name} (a replace: "{name}" felülírja) +@msg a toolbar name has at most {MAX_NAME} characters az eszköztár neve legfeljebb {MAX_NAME} karakter lehet +@msg a view named `{_1}` exists már van ilyen nevű nézet: `{_1}` +@msg a view named `{name}` exists már van ilyen nevű nézet: `{name}` +@msg a workspace keeps at most {MAX} toolbars egy munkaterület legfeljebb {MAX} eszköztárat tárol +@msg a {_1} series can't go in a {_2} graph {_1} típusú adatsor nem kerülhet {_2} típusú diagramba +@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v}: "pdf" (PDF-kompatibilis, ez az alapértelmezés) vagy "7" (régi .ai, 7-es verzió) +@msg already editing an opacity mask már folyik egy átlátszósági maszk szerkesztése +@msg already editing another object's opacity mask már folyik egy másik objektum átlátszósági maszkjának szerkesztése +@msg already in pattern editing mode már mintázatszerkesztési módban vagy +@msg an SVG filter is named by its id: give `xml` with `name` az SVG-szűrőt az azonosítója nevezi meg: a `name` mellé add meg az `xml` értéket +@msg an XML element has two attributes with the same name egy XML-elemnek két azonos nevű attribútuma van +@msg an XML value cannot be inserted into itself or its descendants egy XML-érték nem szúrható be önmagába vagy a leszármazottaiba +@msg an XML value's children cannot be assigned by position egy XML-érték gyermekei nem adhatók meg pozíció szerint +@msg an XML value's children cannot be deleted by position egy XML-érték gyermekei nem törölhetők pozíció szerint +@msg an XMLList converts to XML only when it has one item egy XMLList csak akkor alakítható XML-lé, ha egy eleme van +@msg an XPath node test is missing hiányzik egy XPath-csomópontteszt +@msg an XPath operand is missing hiányzik egy XPath-operandus +@msg an XPath operator is missing hiányzik egy XPath-operátor +@msg an XPath predicate or step cannot follow here itt nem következhet XPath-predikátum vagy -lépés +@msg an XPath predicate or step follows a value that is not a node-set egy XPath-predikátum vagy -lépés olyan érték után áll, amely nem csomóponthalmaz +@msg an XPath step is missing hiányzik egy XPath-lépés +@msg an artboard is listed twice egy rajztábla kétszer szerepel +@msg an empty artboard has no width or height to scale to üres rajztáblának nincs szélessége vagy magassága, amelyhez méretezni lehetne +@msg an image couldn't be decoded and is missing from the picture egy képet nem sikerült dekódolni, ezért hiányzik az ábráról +@msg an image is too large for DXF and was left out egy kép túl nagy a DXF-hez, ezért kimaradt +@msg an image of {_1} MB, at most {_2} MB a kép {_1} MB, legfeljebb {_2} MB lehet +@msg an image of {declared_w} × {declared_h} pixels is too large to draw egy {declared_w} × {declared_h} képpontos kép túl nagy a kirajzoláshoz +@msg an image of {width} × {height} pixels is too large to draw egy {width} × {height} képpontos kép túl nagy a kirajzoláshoz +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) egy {w} × {h} képpontos kép túl nagy az átalakításhoz (legfeljebb {_1} képpont) +@msg an index must be a whole number az index egész szám kell legyen +@msg an inline graphic has no glyphs egy szövegközi grafikának nincsenek glifái +@msg an invisible text form can't be read egy láthatatlan szöveg formaobjektuma nem olvasható +@msg an object is missing hiányzik egy objektum +@msg an object's header is missing hiányzik egy objektum fejléce +@msg an overprinting form can't be read egy felülnyomó formaobjektum nem olvasható +@msg an unread object has no readable bounds egy be nem olvasott objektumnak nincsenek olvasható határai +@msg an unread object holds a frame of threaded type egy be nem olvasott objektum láncolt szöveg keretét tartalmazza +@msg an unread object is missing from its layer egy be nem olvasott objektum hiányzik a rétegéből +@msg anchor `smooth` must be true or false horgonypont, `smooth`: true vagy false kell +@msg anchor `{key}` must be [x, y] horgonypont, `{key}`: [x, y] kell +@msg anchor `{key}` must be a number horgonypont, `{key}`: szám kell +@msg angle and roundness must be finite angle és roundness: véges érték kell +@msg angles must be a list as long as pins (degrees or null) angles: a pins listával azonos hosszúságú lista kell (fok vagy null) +@msg appearance must be flat|center|edge appearance: flat|center|edge kell +@msg apply is "fill" or "stroke", not `{other}` apply: "fill" vagy "stroke", ez nem az: `{other}` +@msg apply was called on a value that is not a function az apply hívása olyan értéken történt, amely nem függvény +@msg area must be a closed polygon of 3 to 100000 [x, y] points area: 3 és 100000 közötti számú [x, y] pontból álló zárt sokszög kell +@msg area type in a thread cannot use Dynamic Text: {frames} a láncolt szövegben lévő területszöveg nem használhat dinamikus szöveget: {frames} +@msg area type needs a path that encloses an area a területszöveghez területet körülzáró útvonal kell +@msg area type {_1} is named twice a területszöveg kétszer szerepel: {_1} +@msg array literal too long a tömbliterál túl hosszú +@msg arrowAlign must be extend|tip, got {o} arrowAlign: extend|tip kell, kapott érték: {o} +@msg arrowScale must be [start, end] arrowScale: [start, end] kell +@msg artboard is a 0-based index artboard: 0-tól kezdődő index +@msg artboard must be a 0-based index artboard: 0-tól kezdődő index kell +@msg artboard {i} doesn't print ez a rajztábla nem kerül nyomtatásra: {i} +@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout: {layout?, columns?, spacing?, rightToLeft?} kell +@msg artboardLayout.columns must be 1 or more artboardLayout.columns: 1 vagy nagyobb érték kell +@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout: gridByRow, gridByColumn, row vagy column kell +@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft: true vagy false kell +@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing: 0–{MAX_SPACING} pt kell +@msg artboards must be a non-empty list artboards: nem üres lista kell +@msg artboards must be a number artboards: szám kell +@msg asset `{_1}` has no art to export ebben az elemben nincs exportálható grafika: `{_1}` +@msg assignment to a constant binding értékadás konstans kötésnek +@msg at least one row that isn't excluded needs a weight above 0 legalább egy ki nem zárt sornak 0-nál nagyobb súly kell +@msg at most {MAX_KEYWORDS} keywords legfeljebb {MAX_KEYWORDS} kulcsszó +@msg at most {MAX_MATCHES} matches are kept legfeljebb {MAX_MATCHES} találat marad meg +@msg at most {MAX_POINTS} points legfeljebb {MAX_POINTS} pont +@msg at most {MAX_QUEUE} files legfeljebb {MAX_QUEUE} fájl +@msg at must be [x, y] at: [x, y] kell +@msg at/rect lie outside the canvas at/rect: a vásznon kívül esik +@msg attribute must be size|leading|tracking|kerning|baselineShift attribute: size|leading|tracking|kerning|baselineShift kell +@msg attrs must be an object attrs: objektum kell +@msg automation path rejected: the folder's name isn't valid text az automatizálás útvonala elutasítva: a mappa neve nem érvényes szöveg +@msg automation path rejected: {_1} isn't valid text az automatizálás útvonala elutasítva: nem érvényes szöveg: {_1} +@msg automation path rejected: {why}: {path} az automatizálás útvonala elutasítva: {why}: {path} +@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis: "vertical", "horizontal" vagy fokban megadott szög kell, ez nem az: `{given}` +@msg backgroundContents must be transparent or white backgroundContents: transparent vagy white kell +@msg bad AES key hibás AES-kulcs +@msg bad `runs`: {e} hibás `runs`: {e} +@msg bad arrowScale hibás arrowScale +@msg bad base64 hibás base64 +@msg bad color {c} hibás szín: {c} +@msg bad colour hibás szín +@msg bad colour in `colors` hibás szín itt: `colors` +@msg bad colour {v} hibás szín: {v} +@msg bad colour {v} in groupColors hibás szín a groupColors értékben: {v} +@msg bad cross-reference entry hibás kereszthivatkozási bejegyzés +@msg bad cross-reference table hibás kereszthivatkozási tábla +@msg bad current colour {v} hibás jelenlegi szín: {v} +@msg bad dataBase64 hibás dataBase64 +@msg bad edit hibás szerkesztés +@msg bad fill {v} hibás kitöltés: {v} +@msg bad fit: {e} hibás illesztés: {e} +@msg bad model {other} (keep, new or document) hibás modell: {other} (keep, new vagy document) +@msg bad new colour {v} hibás új szín: {v} +@msg bad object hibás objektum +@msg bad object number hibás objektumszám +@msg bad offset hibás eltolás +@msg bad path data: {e} hibás útvonaladatok: {e} +@msg bad point {v}: expected [x, y] hibás pont: {v}; [x, y] kell +@msg bad point {v}: expected [x, y] or [x, y, pressure] hibás pont: {v}; [x, y] vagy [x, y, pressure] kell +@msg bad reference hibás hivatkozás +@msg bad trailer size hibás trailerméret +@msg bad {k} colour hibás szín: {k} +@msg batches cannot nest kötegek nem ágyazhatók egymásba +@msg bind something to a variable first előbb köss valamit egy változóhoz +@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed: 0–{MAX_BLEED} pt kell: egy szám, [top, bottom, left, right] vagy {{top, bottom, left, right}} +@msg blend must be a mode name blend: keverési mód neve kell +@msg both sides of an XPath | must select nodes az XPath | mindkét oldalának csomópontokat kell kijelölnie +@msg brush must be a name or null brush: név vagy null kell +@msg by must be a number by: szám kell +@msg can't add page thumbnails: {why} nem adhatók hozzá oldalbélyegképek: {why} +@msg can't add the editing data for other apps: {why} nem adhatók hozzá a más alkalmazásoknak szánt szerkesztési adatok: {why} +@msg can't edit the written PDF: {why} a kiírt PDF nem szerkeszthető: {why} +@msg can't encrypt the PDF: {why} a PDF nem titkosítható: {why} +@msg can't linearise the PDF: {why} a PDF nem linearizálható: {why} +@msg can't open `{_1}`: {_2} nem nyitható meg: `{_1}`: {_2} +@msg can't open `{_1}`: {e} nem nyitható meg: `{_1}`: {e} +@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) nem nyitható meg: `{name}`; ezt a formátumot a VectorCraft nem olvassa (lásd: document.formats) +@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) nem helyezhető el: `{name}`; ezt a formátumot a VectorCraft nem olvassa (lásd: document.formats) +@msg can't read back the written PDF: {e} a kiírt PDF nem olvasható vissza: {e} +@msg can't read this Word document: {why} ez a Word-dokumentum nem olvasható: {why} +@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp .{e} képek nem írhatók: használd a .png, .jpg, .gif, .webp, .tif vagy .bmp formátumot +@msg can't write PDF layers, overprint or invisible text: {why} nem írhatók ki a PDF-rétegek, a felülnyomás vagy a láthatatlan szöveg: {why} +@msg can't write the file's copyright: {why} nem írható ki a fájl szerzői jogi adata: {why} +@msg can't write the file's modification date: {why} nem írható ki a fájl módosítási dátuma: {why} +@msg can't write the pages' user units: {why} nem írhatók ki az oldalak felhasználói egységei: {why} +@msg canceled megszakítva +@msg cannot convert the object to a primitive value az objektum nem alakítható primitív értékké +@msg cannot convert undefined or null to an object undefined vagy null nem alakítható objektummá +@msg cannot distort a zero-size or non-finite bounding box nulla méretű vagy nem véges határolókeret nem torzítható +@msg cannot move an object into itself egy objektum nem helyezhető át önmagába +@msg case must be upper|lower|title|sentence case: upper|lower|title|sentence kell +@msg category `{c}`: one of {_1} kategória `{c}`: ezek egyike: {_1} +@msg center must be [x, y] center: [x, y] kell +@msg character dimensions cannot be represented with the source text transform a karakterméretek nem ábrázolhatók a forrásszöveg transzformációjával +@msg character dimensions cannot be represented with the target text transform a karakterméretek nem ábrázolhatók a célszöveg transzformációjával +@msg choose at least one artboard válassz legalább egy rajztáblát +@msg choose at least one status válassz legalább egy állapotot +@msg clip must be true or false clip: true vagy false kell +@msg clippingMask must be true or false clippingMask: true vagy false kell +@msg closed must be a boolean closed: logikai érték kell +@msg collapsed must be true or false collapsed: true vagy false kell +@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile: nincs ilyen nevű RGB- vagy CMYK-profil: „{profile}” +@msg colorMode must be rgb or cmyk, not `{m}` colorMode: rgb vagy cmyk kell, ez nem az: `{m}` +@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel: {own} (a dokumentum módja), grayscale vagy bitmap kell +@msg colour groups can't go into colour groups színcsoport nem kerülhet színcsoportba +@msg colour, mode and spot apply to solid-colour swatches only colour, mode és spot: csak egyszínű színmintákra vonatkozik +@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read {column}. oszlop: `{cell}` a diagramadat-fájlok `%` oszlopa, ezek beolvasása nem történik meg +@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable {column}. oszlop: `{cell}` ezt nevezi meg: `{name}`, amely {_1} típusú változó +@msg column {column}: `{cell}` names no variable {column}. oszlop: `{cell}` nem nevez meg változót +@msg column {column}: the variable name is longer than {MAX_NAME} characters {column}. oszlop: a változó neve hosszabb {MAX_NAME} karakternél +@msg column: true floats the whole column and takes no onto, above or below column: true az egész oszlopot lebegővé teszi, és nem adható meg mellette onto, above vagy below +@msg command must be a shape.* command command: shape.* parancs kell +@msg commands[{i}]: missing `command` commands[{i}]: hiányzik: `command` +@msg composer must be "singleLine" or "everyLine" composer: "singleLine" vagy "everyLine" kell +@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes összetett alakzatba útvonalak, összetett útvonalak, csoportok, összetett alakzatok, szöveg, összemosások és burkolók kerülhetnek +@msg compound {_1} must contain exactly one open path to join az összetett objektumnak ({_1}) pontosan egy összekapcsolható nyitott útvonalat kell tartalmaznia +@msg connect needs a path other than the one extended connect: a meghosszabbított útvonaltól különböző útvonal kell +@msg connect needs id connect: id is kell hozzá +@msg connect path has no open end connect: az útvonalnak nincs nyitott vége +@msg continuation must be an array of path chunks continuation: útvonaldarabok tömbje kell +@msg continuation requires points continuation: points is kell hozzá +@msg copy needs path: the file to copy into the document's folder copy: path is kell hozzá: a dokumentum mappájába másolandó fájl +@msg copy takes images only: relink placed documents where their files are copy: csak képeket fogad; az elhelyezett dokumentumokat ott csatold újra, ahol a fájljaik vannak +@msg copying a file isn't available on the web a webes változatban nem lehet fájlt másolni +@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus: unknown, copyrighted vagy publicDomain kell +@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl: URL kell (szóköz nélkül, legfeljebb 2048 karakter) +@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners: 4 darab [x, y] pont kell (TL, TR, BR, BL) +@msg counterPer `{c}`: item or file counterPer `{c}`: item vagy file +@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}`: csak a PDF-oldalaknak van art, trim, bleed és media kerete; használd a "crop" vagy a "bounding" értéket +@msg crop rectangle doesn't overlap the image a vágási téglalap nem fedi a képet +@msg cross-reference and object streams aren't supported a kereszthivatkozási és objektumfolyamok nem támogatottak +@msg damaged compressed data: {e} sérült tömörített adatok: {e} +@msg dash lengths must be numbers, got {v} dash: a hosszak számok legyenek, kapott érték: {v} +@msg dash must be a list of lengths or null, got {v} dash: hosszak listája vagy null kell, kapott érték: {v} +@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) data kulcs `{k}`: betűk, számjegyek, -, _ és . (nem lehet name vagy vc-…) +@msg data must be an object of key: value, not {v} data: kulcs: érték párokból álló objektum kell, ez nem az: {v} +@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare a(z) `{name}` adatkészlet értéket tartalmaz ehhez: `{var}`, de ezt a könyvtár nem deklarálja +@msg data set `{name}` holds two values for `{var}` a(z) `{name}` adatkészlet két értéket tartalmaz ehhez: `{var}` +@msg data set `{name}` is declared twice az adatkészlet kétszer van deklarálva: `{name}` +@msg data takes at most {MAX_DATA} keys at once data: egyszerre legfeljebb {MAX_DATA} kulcsot fogad +@msg dataset `{name}` already exists már van ilyen adatkészlet: `{name}` +@msg dataset `{to}` already exists már van ilyen adatkészlet: `{to}` +@msg date `{d}`: YYYY-MM-DD dátum `{d}`: YYYY-MM-DD +@msg decoded data is too large a dekódolt adatok túl nagyok +@msg define a variable first előbb definiálj egy változót +@msg detect must be true or false detect: true vagy false kell +@msg direct-select anchor points of a path jelöld ki közvetlen kijelöléssel egy útvonal horgonypontjait +@msg distance must be > 0 distance: > 0 érték kell +@msg dx and dy give the arrow's direction dx és dy: a nyíl irányát adják meg +@msg dx and dy must be numbers dx és dy: számok kellenek +@msg dxf fitTo must be [width, height], positive dxf fitTo: [width, height] kell, pozitív értékekkel +@msg dxf must be an object of DXF import options dxf: a DXF-importálási beállítások objektuma kell +@msg dxf scale must be a positive number up to {_1}, not {s} dxf scale: pozitív szám kell, legfeljebb {_1}; ez nem az: {s} +@msg dxf unit `{u}`: a unit such as mm, cm, in or pt dxf unit `{u}`: mértékegység kell, például mm, cm, in vagy pt +@msg dxf: {e} dxf: {e} +@msg each format is an object {format, scale?, suffix?} minden formátum egy {format, scale?, suffix?} objektum +@msg each item needs an id minden elemnek azonosító kell +@msg each match needs `kind` minden találathoz `kind` kell +@msg each match needs a `name` minden találathoz `name` kell +@msg each of `fonts` is {family, style?}; 1 to 1000 of them a `fonts` minden eleme {family, style?}; 1–1000 darab lehet +@msg each of `items` is {kind, item} az `items` minden eleme {kind, item} +@msg each of `items` is {{id, corners?}} with an object id, not {it} az `items` minden eleme {{id, corners?}} objektumazonosítóval, ez nem az: {it} +@msg each path chunk must contain 1 to 4096 points minden útvonaldarabnak 1–4096 pontot kell tartalmaznia +@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) minden választás {{at: [x, y]}} vagy {{rect: [x0, y0, x1, y1]}} (kapott érték: {v}) +@msg each row needs `from` minden sorhoz `from` kell +@msg each stop needs a position minden színátmenetpontnak pozíció kell +@msg each subpath needs an anchors array minden részútvonalhoz anchors tömb kell +@msg each weight must be a number of 0 or more (got {v}) minden súly 0 vagy nagyobb szám legyen (kapott érték: {v}) +@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType: easeIn, easeOut, easeInOut vagy easeOutIn kell +@msg editingData must be true or false editingData: true vagy false kell +@msg editingData must be true, false or "always" editingData: true, false vagy "always" kell +@msg edits overlap a szerkesztések átfedik egymást +@msg effects of objects whose appearances differ can't be moved together eltérő megjelenésű objektumok effektusai nem helyezhetők át együtt +@msg empty header in column {_1} üres fejléc ebben az oszlopban: {_1} +@msg encoding `{_1}`: utf8 or utf16 kódolás `{_1}`: utf8 vagy utf16 +@msg ends must be two of "first" and "last" ends: két érték kell ezekből: "first" és "last" +@msg every numbered name for the result is taken az eredmény minden számozott neve foglalt +@msg expanded SVG styles exceed the import size limit a kibontott SVG-stílusok túllépik az importálási méretkorlátot +@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes az exportálási előbeállítások fájlja nagyobb, mint {MAX_PRESET_FILE_BYTES} bájt +@msg exportText must be editable or appearance exportText: editable vagy appearance kell +@msg exportType must be movieClip or graphic, not `{s}` exportType: movieClip vagy graphic kell, ez nem az: `{s}` +@msg extend needs id extend: id is kell hozzá +@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges: legfeljebb {MAX_FEATURE_CHANGES} címkét sorolhat fel +@msg featureChanges must be a list of tags featureChanges: címkék listája kell +@msg features must be a list of tags features: címkék listája kell +@msg fidelity must be 0..100 fidelity: 0..100 kell +@msg files must be {name, dataBase64} files: {name, dataBase64} kell +@msg finally table {t} out of range a finally tábla tartományon kívül esik: {t} +@msg fit must be none|autoHeight|shrinkText|block, got {id} fit: none|autoHeight|shrinkText|block kell, kapott érték: {id} +@msg flags cannot be given with a RegExp object RegExp objektum mellett nem adhatók meg jelzők +@msg flattener: {e} lapító: {e} +@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}`: high, medium, low vagy egy mentett előbeállítás (lásd: flattener.presets.list) +@msg flipProfile must be along|across, got {o} flipProfile: along|across kell, kapott érték: {o} +@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}`: none, rows, format, item vagy itemFormat +@msg folders deeper than {MAX_BATCH_DEPTH} levels {MAX_BATCH_DEPTH} szintnél mélyebb mappák +@msg folders deeper than {MAX_DEPTH} levels {MAX_DEPTH} szintnél mélyebb mappák +@msg font family `{family}` is not available a betűcsalád nem érhető el: `{family}` +@msg font family `{tf}` is not available a betűcsalád nem érhető el: `{tf}` +@msg format `{f}`: pdf or postscript format `{f}`: pdf vagy postscript +@msg format `{f}`: text or json format `{f}`: text vagy json +@msg format is `{format}` a formátum: `{format}` +@msg formats must be an array of rows, not {v} formats: sorok tömbje kell, ez nem az: {v} +@msg frequency must be between 1 and 300 lines per inch frequency: 1 és 300 vonal/hüvelyk közötti érték kell +@msg from and to must be finite from és to: véges érték kell +@msg from must be [x0, y0, x1, y1] from: [x0, y0, x1, y1] kell +@msg function declaration index out of range a függvénydeklaráció indexe tartományon kívül esik +@msg give `before` or `after`, not both a `before` vagy az `after` értéket add meg, ne mindkettőt +@msg give `char`, `text`, `code` or `unicode` add meg a `char`, a `text`, a `code` vagy a `unicode` értéket +@msg give `colors`, or `color` and `rule` add meg a `colors` értéket, vagy a `color` és a `rule` értéket +@msg give `count` or `spacing`, not both a `count` vagy a `spacing` értéket add meg, ne mindkettőt +@msg give `event`, such as onclick add meg az `event` értéket, például onclick +@msg give `file`, the JavaScript file's path or URL add meg a `file` értéket: a JavaScript-fájl elérési útját vagy URL-jét +@msg give `fill` and/or `stroke` add meg a `fill` és/vagy a `stroke` értéket +@msg give `fill`, `stroke` or both add meg a `fill` vagy a `stroke` értéket, vagy mindkettőt +@msg give `folder` add meg a `folder` értéket +@msg give `fonts`, or open a document add meg a `fonts` értéket, vagy nyiss meg egy dokumentumot +@msg give `from` or `indices`, not both a `from` vagy az `indices` értéket add meg, ne mindkettőt +@msg give `id` (no text is being edited) add meg az `id` értéket (nincs szerkesztés alatt álló szöveg) +@msg give `id` and `anchors`, or `items`, not both az `id` és az `anchors` értéket, vagy az `items` értéket add meg, ne mindkettőt +@msg give `id` or `guide`, not both az `id` vagy a `guide` értéket add meg, ne mindkettőt +@msg give `length` or `to` add meg a `length` vagy a `to` értéket +@msg give `matches`: [{kind, name, target}] add meg a `matches` értéket: [{kind, name, target}] +@msg give `name` (or edit a pattern) add meg a `name` értéket (vagy szerkessz egy mintázatot) +@msg give `name` or `names` add meg a `name` vagy a `names` értéket +@msg give `name`, an SVG filter's name add meg a `name` értéket: egy SVG-szűrő nevét +@msg give `path` (a .wasm file) or `dataBase64` (the module) add meg a `path` (egy .wasm fájl) vagy a `dataBase64` (a modul) értéket +@msg give `path`, `data` or `dataBase64` add meg a `path`, a `data` vagy a `dataBase64` értéket +@msg give `path`, `data`, `dataBase64` or `csv` add meg a `path`, a `data`, a `dataBase64` vagy a `csv` értéket +@msg give `path`, `xml` or `dataBase64` add meg a `path`, az `xml` vagy a `dataBase64` értéket +@msg give `path`, an SVG file, or `svg`, SVG text add meg a `path` értéket (egy SVG-fájl) vagy az `svg` értéket (SVG-szöveg) +@msg give `script`, the JavaScript the event runs ("" removes the event) add meg a `script` értéket: az esemény által futtatott JavaScriptet (a "" eltávolítja az eseményt) +@msg give `text` (the reading) or `remove` add meg a `text` (az olvasat) vagy a `remove` értéket +@msg give `x` and `y` (and a `tolerance` of 0 or more) add meg az `x` és az `y` értéket (és egy 0 vagy nagyobb `tolerance` értéket) +@msg give `xml`, one SVG element add meg az `xml` értéket: egyetlen SVG elemet +@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments adj meg egy módot, vagy r/g/b, c/m/y/k, gray vagy tint módosításokat +@msg give a name, or paint the active proxy with a saved preset adj meg egy nevet, vagy fesd az aktív mintadobozt egy mentett előbeállítással +@msg give a name, or select a stroke with a saved profile adj meg egy nevet, vagy jelölj ki egy mentett profilú körvonalat +@msg give a pattern name or select an object painted with a pattern adj meg egy mintázatnevet, vagy jelölj ki egy mintázattal festett objektumot +@msg give at least one of clip, invert, disabled, linked adj meg legalább egyet ezek közül: clip, invert, disabled, linked +@msg give color, none, swatch or gradient adj meg color, none, swatch vagy gradient értéket +@msg give dataBase64 add meg a dataBase64 értéket +@msg give either at (a click) or area (a path drawn around the overlaps) add meg az at (egy kattintás) vagy az area (az átfedések köré rajzolt útvonal) értéket +@msg give exactly one of `at`, `rect` and `points` pontosan egyet adj meg ezek közül: `at`, `rect`, `points` +@msg give exactly one of `path`, `data` and `dataBase64` pontosan egyet adj meg ezek közül: `path`, `data`, `dataBase64` +@msg give formats or a preset, not both a formats értéket vagy egy előbeállítást adj meg, ne mindkettőt +@msg give index or indexes, not both az index vagy az indexes értéket add meg, ne mindkettőt +@msg give newName or delete add meg a newName vagy a delete értéket +@msg give offset or by add meg az offset vagy a by értéket +@msg give one element egyetlen elemet adj meg +@msg give one of `to`, `rect` or `at` egyet adj meg ezek közül: `to`, `rect`, `at` +@msg give one of assets, layers, selection or fullDocument egyet adj meg ezek közül: assets, layers, selection, fullDocument +@msg give only one of onto, above, below, icon and a column place csak egyet adj meg ezek közül: onto, above, below, icon vagy egy oszlopbeli hely +@msg give opacity or blend add meg az opacity vagy a blend értéket +@msg give origin: [x, y], or reset: true add meg ezt: origin: [x, y], vagy ezt: reset: true +@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note add meg az overprintFill, overprintStroke, showCenter, imageMap, url vagy note értéket +@msg give path (a file) or folder add meg a path (egy fájl) vagy a folder értéket +@msg give path, or name and dataBase64 add meg a path értéket, vagy a name és a dataBase64 értéket +@msg give paths or files add meg a paths vagy a files értéket +@msg give rows, rowHeight, columns or columnWidth add meg a rows, rowHeight, columns vagy columnWidth értéket +@msg give svg, or dataBase64 of an SVG or SVGZ file add meg az svg értéket, vagy egy SVG- vagy SVGZ-fájl dataBase64 értékét +@msg give text add meg a text értéket +@msg give the asset a name adj nevet az elemnek +@msg give the preset a `name` adj `name` értéket az előbeállításnak +@msg give the preset a name adj nevet az előbeállításnak +@msg give the preset a name (1–64 characters) adj nevet az előbeállításnak (1–64 karakter) +@msg give the settings to change, e.g. {"formats"}]} add meg a módosítandó beállításokat, például {"formats"}]} +@msg give the style a name adj nevet a stílusnak +@msg give two or more style `names` adj meg két vagy több stílusnevet a `names` értékben +@msg give two or more swatches adj meg két vagy több színmintát +@msg give x, y, or a handle's length or angle add meg az x, az y értéket, vagy egy fogantyú hosszát vagy szögét +@msg give {key, value} or {values} add meg ezt: {key, value} vagy {values} +@msg global must be true or false global: true vagy false kell +@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) a(z) {_1} glifa nem ehhez a karakterhez rajzolódik ki a karakter stílusával: {ch} (ilyen például a Csupa nagybetű, a Kiskapitális vagy egy számrendszer) +@msg glyph {g} is not an alternate of {ch} in {_1} {_2} a(z) {g} glifa nem alternatívája ennek: {ch} (betűtípus: {_1} {_2}) +@msg gradient must be "objects" or "mesh" gradient: "objects" vagy "mesh" kell +@msg grays must be "profile" or "black", not `{g}` grays: "profile" vagy "black" kell, ez nem az: `{g}` +@msg gridColors must be a preset name or two colours gridColors: előbeállítás neve vagy két szín kell +@msg gridSize must be small, medium or large gridSize: small, medium vagy large kell +@msg guide {g} is not selected a segédvonal nincs kijelölve: {g} +@msg handle must be 0..3 handle: 0..3 kell +@msg handler at {_1} expects {_2} values but its range starts with {d} a(z) {_1} helyen lévő kezelő {_2} értéket vár, de a tartománya ennyivel kezdődik: {d} +@msg handler depth too large a kezelő mélysége túl nagy +@msg handler range {_1}..{_2} out of range a kezelő tartománya ({_1}..{_2}) tartományon kívül esik +@msg hiddenLayers must be true or false hiddenLayers: true vagy false kell +@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v}: "layer" (ez az alapértelmezés), "omit" vagy "visible" +@msg highlight a layer to merge into emelj ki egy réteget, amelybe az egyesítés történjen +@msg highlight two or more rows in one layer or group emelj ki két vagy több sort egyetlen rétegben vagy csoportban +@msg highlight two or more rows to merge emelj ki két vagy több egyesítendő sort +@msg hvcC hvcC +@msg icon docks a panel in the icon column: use window.panel.dock icon: a panelt az ikonoszlopba dokkolja; használd a window.panel.dock parancsot +@msg id must be a dimension id id: méretvonal-azonosító kell +@msg image has no bounds a képnek nincsenek határai +@msg image {_1} is linked already a kép már csatolva van: {_1} +@msg imageMap `{m}`: none, client or server imageMap `{m}`: none, client vagy server +@msg images in a spot ink whose samples couldn't be read were imported in RGB azok a direktszínű képek, amelyek mintái nem voltak olvashatók, RGB-ben lettek importálva +@msg images past the most pixels one file may hold were left out kimaradtak azok a képek, amelyek túllépték az egy fájlban megengedett legnagyobb képpontszámot +@msg images too large to read were left out a beolvasáshoz túl nagy képek kimaradtak +@msg index out of range az index tartományon kívül esik +@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes: legfeljebb {MAX_GUIDE_INDEXES} elemet tartalmazhat +@msg indexes must be an array of guide indexes indexes: segédvonalindexek tömbje kell +@msg instances must be 1..1000 instances: 1..1000 kell +@msg instruction out of range az utasítás tartományon kívül esik +@msg instruction {pc} ({op}) pops below the stack a(z) {pc}. utasítás ({op}) a verem alja alól venne ki értéket +@msg instruction {pc} reached with stack depths {seen} and {d} a(z) {pc}. utasítás kétféle veremmélységgel érhető el: {seen} és {d} +@msg invalid `color` érvénytelen `color` +@msg invalid anchor index {v}: expected a non-negative integer érvénytelen horgonypontindex: {v}; nemnegatív egész szám kell +@msg invalid anchor reference {v}: expected [subpath, anchor] érvénytelen horgonypont-hivatkozás: {v}; [subpath, anchor] kell +@msg invalid angle érvénytelen szög +@msg invalid array length érvénytelen tömbhossz +@msg invalid brush: {e} érvénytelen ecset: {e} +@msg invalid color érvénytelen szín +@msg invalid colour {c} érvénytelen szín: {c} +@msg invalid delta érvénytelen eltérés +@msg invalid group érvénytelen csoport +@msg invalid object id {v}: expected a non-negative integer érvénytelen objektumazonosító: {v}; nemnegatív egész szám kell +@msg invalid point érvénytelen pont +@msg invalid regular expression flag '{_1}' érvénytelen jelző a reguláris kifejezésben: '{_1}' +@msg invalid regular expression: {m} érvénytelen reguláris kifejezés: {m} +@msg invalid size érvénytelen méret +@msg invalid t érvénytelen t +@msg invalid time value érvénytelen időérték +@msg invalid tolerance érvénytelen tűrés +@msg is a confirm's answer, whose `value` must be true or false egy megerősítés válasza, amelynek `value` értéke true vagy false lehet +@msg is a prompt's answer, whose `value` must be a string or null egy bekérés válasza, amelynek `value` értéke karakterlánc vagy null lehet +@msg is an alert's answer, whose `value` must be "ok" egy figyelmeztetés válasza, amelynek `value` értéke "ok" lehet +@msg is longer than 16 KiB hosszabb 16 KiB-nál +@msg it draws its image through a clipping path a képét vágóútvonalon keresztül rajzolja ki +@msg it draws no image nem rajzol ki képet +@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes nagyobb, mint {MAX_DICTIONARY_FILE_BYTES} bájt +@msg it isn't a file nem fájl +@msg it names {_1} plates, and at most {MAX_PLATES} are read {_1} színkivonatot nevez meg, de legfeljebb {MAX_PLATES} olvasható be +@msg its art is too long or too detailed for a head a grafikája túl hosszú vagy túl részletes egy nyílhegyhez +@msg its art paints nothing a grafikája semmit sem fest +@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) minden rajztáblája a vásznon kívül esik (több mint {_1} pt távolságra az origótól) +@msg its clipping masks are nested too deeply a vágómaszkjai túl mélyen vannak egymásba ágyazva +@msg its image is too large to read a képe túl nagy a beolvasáshoz +@msg its metadata has no description a metaadataiban nincs leírás +@msg its metadata has no direct length a metaadatainak nincs közvetlenül megadott hossza +@msg its plates are too large: more than {MAX_SAMPLES} pixels together a színkivonatai túl nagyok: együtt több mint {MAX_SAMPLES} képpont +@msg its plates are too large: {_1} plates of {w} × {h} pixels a színkivonatai túl nagyok: {_1} darab {w} × {h} képpontos színkivonat +@msg its plates can't be kept together a színkivonatai nem tarthatók együtt +@msg its shapes could not be combined into one az alakzatait nem sikerült eggyé egyesíteni +@msg join two different paths két különböző útvonalat kapcsolj össze +@msg jump to {pc}, past the end ugrás a végén túlra: {pc} +@msg justify must be auto|left|center|right|justifyAll justify: auto|left|center|right|justifyAll kell +@msg kbps must be a positive number, not {kbps} kbps: pozitív szám kell, ez nem az: {kbps} +@msg keywords must be a list of words or a comma-separated string keywords: szavak listája vagy vesszővel tagolt karakterlánc kell +@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords: legfeljebb {MAX_KEYWORD} karakter lehet, vezérlőkarakterek nélkül +@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind: 1, 2 vagy 3 kell, vagy adj meg egy előbeállítást a `name` értékben (lásd: 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 vagy paraStyle kell +@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout: "on", "off", "neutral" vagy logikai érték kell, ez nem az: {v} +@msg layer `{name}` has no art to export ebben a rétegben nincs exportálható grafika: `{name}` +@msg layer must name a layer or sublayer layer: réteget vagy alréteget kell megneveznie +@msg layers can't be intertwined: select the objects in them rétegek nem fonhatók össze: a bennük lévő objektumokat jelöld ki +@msg layers go only at the top level or in layers, objects in layers and groups rétegek csak a legfelső szintre vagy rétegekbe kerülhetnek, objektumok rétegekbe és csoportokba +@msg layers must be true or false layers: true vagy false kell +@msg layout must be gridByRow, gridByColumn, row or column layout: gridByRow, gridByColumn, row vagy column kell +@msg leading must be a number or "auto" leading: szám vagy "auto" kell +@msg left/right must be non-negative widths left/right: nemnegatív szélességek kellenek +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding: ezek egyike kell: {_1} +@msg level `{t}`: 2 or 3 level `{t}`: 2 vagy 3 +@msg line {line}: more than {MAX_CSV_COLS} columns {line}. sor: több mint {MAX_CSV_COLS} oszlop +@msg line {line}: unclosed quote {line}. sor: lezáratlan idézőjel +@msg line {line}: value for `{var}` must match its variable kind {line}. sor: a(z) `{var}` értékének meg kell felelnie a változó típusának +@msg lineEndings `{other}`: lf or crlf lineEndings `{other}`: lf vagy crlf +@msg loading profiles from disk isn't available in the browser böngészőben nem lehet profilokat lemezről betölteni +@msg localize: the first argument must be a localization object or a string localize: az első argumentum lokalizációs objektum vagy karakterlánc kell legyen +@msg margin: a {m} pt margin leaves no imageable area on the paper margin: {m} pt margó mellett nem marad nyomtatható terület a papíron +@msg matrix must be finite and invertible, depth finite matrix: véges és invertálható kell legyen, a depth véges +@msg matte: {e} matte: {e} +@msg member is for order front, forward, backward or back member: az order front, forward, backward vagy back értékéhez való +@msg missing `artboards` hiányzik: `artboards` +@msg missing `at` [x, y] hiányzik: `at` [x, y] +@msg missing `attrs` hiányzik: `attrs` +@msg missing `color` or `opacity` hiányzik: `color` vagy `opacity` +@msg missing `colors` hiányzik: `colors` +@msg missing `commands` hiányzik: `commands` +@msg missing `destinationFolder` hiányzik: `destinationFolder` +@msg missing `effect` id hiányzik az `effect` azonosítója +@msg missing `family` hiányzik: `family` +@msg missing `find` hiányzik: `find` +@msg missing `gid` (a glyph id, or null for the default glyph) hiányzik: `gid` (glifaazonosító, vagy null az alapértelmezett glifához) +@msg missing `group` hiányzik: `group` +@msg missing `id` hiányzik: `id` +@msg missing `id`: give one, or select one type object hiányzik: `id`; adj meg egyet, vagy jelölj ki egy szövegobjektumot +@msg missing `index` (a point index, or null to clear) hiányzik: `index` (pontindex, vagy null a törléshez) +@msg missing `index` (a stop index, or null to clear) hiányzik: `index` (színátmenetpont-index, vagy null a törléshez) +@msg missing `index` (an item index or null) hiányzik: `index` (elemindex vagy null) +@msg missing `item` hiányzik: `item` +@msg missing `library` hiányzik: `library` +@msg missing `library` (see graphicStyle.libraries) hiányzik: `library` (lásd: graphicStyle.libraries) +@msg missing `library` (see swatch.library.list) hiányzik: `library` (lásd: swatch.library.list) +@msg missing `map` (rows [{from, to}] or {current}) hiányzik: `map` ([{from, to}] sorok vagy {current}) +@msg missing `matrix` [a,b,c,d,e,f] hiányzik: `matrix` [a,b,c,d,e,f] +@msg missing `name` (see gradient.presets.list) hiányzik: `name` (lásd: gradient.presets.list) +@msg missing `offset` (a distance in points) hiányzik: `offset` (távolság pontban) +@msg missing `path` hiányzik: `path` +@msg missing `path` (a path object id) hiányzik: `path` (egy útvonalobjektum azonosítója) +@msg missing `picks` hiányzik: `picks` +@msg missing `rule` hiányzik: `rule` +@msg missing `source` id hiányzik a `source` azonosítója +@msg missing `start` (the byte offset of a character) hiányzik: `start` (egy karakter bájteltolása) +@msg missing `status`: linked, embedded, missing or modified hiányzik: `status`; linked, embedded, missing vagy modified lehet +@msg missing `target` id hiányzik a `target` azonosítója +@msg missing `text` hiányzik: `text` +@msg missing `to` hiányzik: `to` +@msg missing `to` (an index) hiányzik: `to` (egy index) +@msg missing `to`: a non-negative integer hiányzik: `to`; nemnegatív egész szám kell +@msg missing `type` hiányzik: `type` +@msg missing `value` hiányzik: `value` +@msg missing `word` hiányzik: `word` +@msg missing `{key}` hiányzik: `{key}` +@msg missing `{k}` hiányzik: `{k}` +@msg missing anchor hiányzik a horgonypont +@msg missing anchors hiányoznak a horgonypontok +@msg missing anchors [[subpath, anchor]…] hiányoznak a horgonypontok: [[subpath, anchor]…] +@msg missing array `{key}` hiányzik a tömb: `{key}` +@msg missing case hiányzik: case +@msg missing color hiányzik a szín +@msg missing commands hiányoznak a parancsok +@msg missing destination folder hiányzik a célmappa +@msg missing from hiányzik: from +@msg missing group hiányzik a csoport +@msg missing id or ids hiányzik: id vagy ids +@msg missing ids hiányzik: ids +@msg missing indexes hiányzik: indexes +@msg missing matrix [a,b,c,d,e,f] hiányzik: matrix [a,b,c,d,e,f] +@msg missing name hiányzik a név +@msg missing op hiányzik: op +@msg missing or invalid `color` hiányzó vagy érvénytelen `color` +@msg missing or unknown `tool` hiányzó vagy ismeretlen `tool` +@msg missing point hiányzik a pont +@msg missing point [x,y] hiányzik a pont: [x,y] +@msg missing point `{key}` ([x, y]) hiányzik a pont: `{key}` ([x, y]) +@msg missing points hiányoznak a pontok +@msg missing points [[x,y]…] hiányoznak a pontok: [[x,y]…] +@msg missing segment hiányzik a szakasz +@msg missing stops hiányoznak a színátmenetpontok +@msg missing subpaths hiányoznak a részútvonalak +@msg missing target hiányzik a cél +@msg missing to hiányzik: to +@msg missing x, y hiányzik: x, y +@msg mode is set, add or toggle, not `{other}` mode: set, add vagy toggle, ez nem az: `{other}` +@msg mode must be "cmyk" or "rgb" mode: "cmyk" vagy "rgb" kell +@msg mode must be area|onPath, got {o} mode: area|onPath kell, kapott érték: {o} +@msg more than 10000 linked files: package dependencies in smaller parts több mint 10000 csatolt fájl: kisebb részletekben csomagold a függőségeket +@msg more than 256 commands több mint 256 parancs +@msg more than {MAX_BATCH_FILES} data sets több mint {MAX_BATCH_FILES} adatkészlet +@msg more than {MAX_BATCH_FILES} files több mint {MAX_BATCH_FILES} fájl +@msg more than {MAX_CSV_ROWS} data rows több mint {MAX_CSV_ROWS} adatsor +@msg more than {MAX_FILES} images több mint {MAX_FILES} kép +@msg more than {MAX_IMPORT} presets több mint {MAX_IMPORT} előbeállítás +@msg moved must be [[x,y]…] moved: [[x,y]…] kell +@msg must be an object with `kind` and `value` `kind` és `value` kulcsot tartalmazó objektum kell +@msg must have `kind` alert, confirm or prompt a `kind` értéke alert, confirm vagy prompt kell legyen +@msg must have exactly `kind` and `value` pontosan a `kind` és a `value` kulcsot kell tartalmaznia +@msg name (a toolbar's name) is required name (egy eszköztár neve): kötelező +@msg name (a toolbar's name) must be a string name (egy eszköztár neve): karakterlánc kell +@msg name must be a toolbar's name name: egy eszköztár neve kell +@msg names is empty names: üres +@msg names must be an array of symbol names names: szimbólumnevek tömbje kell +@msg need `anchor` or `segment` + `t` `anchor` vagy `segment` + `t` kell +@msg need at least one anchor legalább egy horgonypont kell +@msg need at least one point legalább egy pont kell +@msg need at least two points legalább két pont kell +@msg needs a closing quote after the name a név után záró idézőjel kell +@msg needs a name név kell +@msg needs a name of at most 64 bytes legfeljebb 64 bájtos név kell +@msg needs a name of letters, digits and _, or a quoted name betűkből, számjegyekből és _ jelből álló név vagy idézőjelbe tett név kell +@msg needs a name without control characters vezérlőkarakterek nélküli név kell +@msg needs points pontok kellenek +@msg nesting too deep túl mély egymásba ágyazás +@msg nineSlice must be true or false nineSlice: true vagy false kell +@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides: [left, top, right, bottom] kell, mindegyik -16..16, ahol left + right < 1 és top + bottom < 1 +@msg no HP-GL format nincs HP-GL formátum +@msg no New Document profile called `{shown}` ({_1}) nincs ilyen nevű új dokumentumprofil: `{shown}` ({_1}) +@msg no PDF preset named `{n}` nincs ilyen nevű PDF-előbeállítás: `{n}` +@msg no SVG filter at index {k} nincs SVG-szűrő ennél az indexnél: {k} +@msg no action set is current nincs aktuális műveletkészlet +@msg no active data set nincs aktív adatkészlet +@msg no appearance item {index} nincs ilyen megjelenési elem: {index} +@msg no arrowheads file here itt nincs nyílhegyfájl +@msg no artboard {_1} nincs ilyen rajztábla: {_1} +@msg no artboard {artboard} (0-based; the document has {_1}) nincs ilyen rajztábla: {artboard} (0-tól számozva; a dokumentumban {_1} van) +@msg no asset {id} nincs ilyen elem: {id} +@msg no asset {id} (see assets.list) nincs ilyen elem: {id} (lásd: assets.list) +@msg no assets to export nincs exportálható elem +@msg no assets yet: collect art for export first (assets.add) még nincsenek elemek: előbb gyűjts grafikát az exportáláshoz (assets.add) +@msg no brush named `{n}` nincs ilyen nevű ecset: `{n}` +@msg no character at byte {start} nincs karakter ennél a bájtnál: {start} +@msg no character has code {code} egyetlen karakternek sem ez a kódja: {code} +@msg no colour nincs szín +@msg no colour group `{group}` nincs ilyen színcsoport: `{group}` +@msg no colour group `{name}` nincs ilyen színcsoport: `{name}` +@msg no custom arrowhead {name} (see stroke.arrowheads.list) nincs ilyen egyéni nyílhegy: {name} (lásd: stroke.arrowheads.list) +@msg no data sets nincsenek adatkészletek +@msg no dataset named `{from}` nincs ilyen nevű adatkészlet: `{from}` +@msg no dataset named `{name}` nincs ilyen nevű adatkészlet: `{name}` +@msg no datasets nincsenek adatkészletek +@msg no design named `{name}` nincs ilyen nevű diagramminta: `{name}` +@msg no design named `{n}` nincs ilyen nevű diagramminta: `{n}` +@msg no design named `{old}` nincs ilyen nevű diagramminta: `{old}` +@msg no encoder for {_1} yet ehhez még nincs kódoló: {_1} +@msg no export preset `{name}` (see document.exportPresets.list) nincs ilyen exportálási előbeállítás: `{name}` (lásd: document.exportPresets.list) +@msg no file to open nincs megnyitandó fájl +@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder nincs mappa: add meg a `path` értéket, vagy állítsd be ezt: Beállítások › Bővítmények & Munkalemezek › További bővítmények mappája +@msg no fonts to look for: they are all available nincs keresendő betűtípus: mindegyik elérhető +@msg no formats to export nincs exportálandó formátum +@msg no gradient preset `{name}` (see gradient.presets.list) nincs ilyen színátmenet-előbeállítás: `{name}` (lásd: gradient.presets.list) +@msg no graphic style `{n}` in `{_1}` nincs ilyen grafikai stílus: `{n}` (itt: `{_1}`) +@msg no graphic style library `{key}` (see graphicStyle.libraries) nincs ilyen grafikaistílus-könyvtár: `{key}` (lásd: graphicStyle.libraries) +@msg no guide selected nincs kijelölt segédvonal +@msg no guide selected (or give an index) nincs kijelölt segédvonal (vagy adj meg egy indexet) +@msg no guide {i} nincs ilyen segédvonal: {i} +@msg no images in the folder a mappában nincsenek képek +@msg no instructions nincsenek utasítások +@msg no item at index {_1} nincs elem ennél az indexnél: {_1} +@msg no item at index {from} nincs elem ennél az indexnél: {from} +@msg no item at one of the indices {idx} az indexek egyikénél nincs elem: {idx} +@msg no layer nincs réteg +@msg no layer to rename nincs átnevezhető réteg +@msg no layer {id} (see layer.list) nincs ilyen réteg: {id} (lásd: layer.list) +@msg no layers to export nincs exportálható réteg +@msg no library `{key}` nincs ilyen könyvtár: `{key}` +@msg no metafile format nincs metafájlformátum +@msg no object {_1} nincs ilyen objektum: {_1} +@msg no objects nincsenek objektumok +@msg no parent layer nincs szülőréteg +@msg no perspective grid preset named `{b}` (see perspective.presets.list) nincs ilyen nevű perspektivikusrács-előbeállítás: `{b}` (lásd: perspective.presets.list) +@msg no perspective grid preset named `{n}` nincs ilyen nevű perspektivikusrács-előbeállítás: `{n}` +@msg no point selected (give `index`) nincs kijelölt pont (add meg az `index` értéket) +@msg no preset called `{from}` (see file.newPresets) nincs ilyen nevű előbeállítás: `{from}` (lásd: file.newPresets) +@msg no preset called `{name}` (see file.newPresets) nincs ilyen nevű előbeállítás: `{name}` (lásd: file.newPresets) +@msg no preset named `{n}` nincs ilyen nevű előbeállítás: `{n}` +@msg no print preset named `{b}` (see print.presets.list) nincs ilyen nevű nyomtatási előbeállítás: `{b}` (lásd: print.presets.list) +@msg no print preset named `{name}` (presets: {_1}) nincs ilyen nevű nyomtatási előbeállítás: `{name}` (előbeállítások: {_1}) +@msg no print preset named `{n}` nincs ilyen nevű nyomtatási előbeállítás: `{n}` +@msg no profile named `{name}` nincs ilyen nevű profil: `{name}` +@msg no random numbers ({e}) nincsenek véletlenszámok ({e}) +@msg no readable files in the folder a mappában nincsenek olvasható fájlok +@msg no recovery copy `{f}` (see file.recovery.list) nincs ilyen helyreállítási másolat: `{f}` (lásd: file.recovery.list) +@msg no rows nincsenek sorok +@msg no saved PDF preset named `{name}` nincs ilyen nevű mentett PDF-előbeállítás: `{name}` +@msg no saved export preset `{name}` nincs ilyen mentett exportálási előbeállítás: `{name}` +@msg no saved gradient preset called {name} nincs ilyen nevű mentett színátmenet-előbeállítás: {name} +@msg no saved gradient presets nincs mentett színátmenet-előbeállítás +@msg no saved preset called `{name}` nincs ilyen nevű mentett előbeállítás: `{name}` +@msg no saved presets to export (name [Default] in `names` to export it) nincs exportálható mentett előbeállítás (az exportálásához nevezd meg a [Default] előbeállítást a `names` értékben) +@msg no saved presets to export (name built-in ones in `names`) nincs exportálható mentett előbeállítás (a beépítetteket a `names` értékben nevezd meg) +@msg no saved print preset named `{name}` nincs ilyen nevű mentett nyomtatási előbeállítás: `{name}` +@msg no saved profile called {name} nincs ilyen nevű mentett profil: {name} +@msg no saved selection `{name}` nincs ilyen mentett kijelölés: `{name}` +@msg no saved theme called {name} (see colorTheme.list) nincs ilyen nevű mentett téma: {name} (lásd: colorTheme.list) +@msg no saved views nincsenek mentett nézetek +@msg no selected or given object has a visible 3D effect egyik kijelölt vagy megadott objektumnak sincs látható 3D effektusa +@msg no series {i} nincs ilyen adatsor: {i} +@msg no slice {n} in the image (see slice.list) a képen nincs ilyen szelet: {n} (lásd: slice.list) +@msg no source art nincs forrásgrafika +@msg no stop {i} (the gradient has {n}) nincs ilyen színátmenetpont: {i} (a színátmenetnek {n} van) +@msg no style `{name}` nincs ilyen stílus: `{name}` +@msg no such artboard {v} nincs ilyen rajztábla: {v} +@msg no such assets (see assets.list) nincsenek ilyen elemek (lásd: assets.list) +@msg no such frame anchor, or the frame would reach past the canvas nincs ilyen kerethorgonypont, vagy a keret túlnyúlna a vásznon +@msg no such objects nincsenek ilyen objektumok +@msg no such path anchor, or the path would reach past the canvas nincs ilyen útvonal-horgonypont, vagy az útvonal túlnyúlna a vásznon +@msg no such rows nincsenek ilyen sorok +@msg no swatch `{keep}` nincs ilyen színminta: `{keep}` +@msg no swatch `{n}` nincs ilyen színminta: `{n}` +@msg no swatch `{n}` (swatches and colour groups move separately) nincs ilyen színminta: `{n}` (a színminták és a színcsoportok külön mozgathatók) +@msg no swatch library `{key}` (see swatch.library.list) nincs ilyen színmintakönyvtár: `{key}` (lásd: swatch.library.list) +@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) nincs ilyen színmintakönyvtár: `{key}` (lásd: swatch.library.list; "document": a dokumentum színmintái) +@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) nincs ilyen színmintakönyvtár: `{key}` (lásd: swatch.library.list; "{DOCUMENT_SWATCHES}": a dokumentum színmintái) +@msg no swatch named `{n}` nincs ilyen nevű színminta: `{n}` +@msg no swatch or colour group `{n}` nincs ilyen színminta vagy színcsoport: `{n}` +@msg no swatch or colour group `{n}` in `{_1}` nincs ilyen színminta vagy színcsoport: `{n}` (itt: `{_1}`) +@msg no symbol named `{_1}` nincs ilyen nevű szimbólum: `{_1}` +@msg no symbol named `{name}` nincs ilyen nevű szimbólum: `{name}` +@msg no text objects selected nincs kijelölt szövegobjektum +@msg no toolbar named `{_1}` nincs ilyen nevű eszköztár: `{_1}` +@msg no toolbar named `{name}` nincs ilyen nevű eszköztár: `{name}` +@msg no tracing preset `{preset}` (see imageTrace.presets) nincs ilyen vektorizálási előbeállítás: `{preset}` (lásd: imageTrace.presets) +@msg no user library folder here (save with a path, or without one to get the data) itt nincs felhasználói könyvtármappa (ments elérési úttal, vagy anélkül, hogy megkapd az adatokat) +@msg no variable named `{from}` nincs ilyen nevű változó: `{from}` +@msg no variable named `{name}` nincs ilyen nevű változó: `{name}` +@msg no variable named `{var}` nincs ilyen nevű változó: `{var}` +@msg no variable named `{v}` nincs ilyen nevű változó: `{v}` +@msg no view named `{name}` nincs ilyen nevű nézet: `{name}` +@msg node {_1} already has the text variable `{_2}` a(z) {_1} csomópontnak már van szövegváltozója: `{_2}` +@msg node {_1} is not a graph nem diagram ez a csomópont: {_1} +@msg node {_1} is not an image nem kép ez a csomópont: {_1} +@msg node {_1} is not type nem szöveg ez a csomópont: {_1} +@msg not a DCS file nem DCS-fájl +@msg not a PDF presets file (format `{_1}`) nem PDF-előbeállításfájl (formátum: `{_1}`) +@msg not a PDF presets file: {e} nem PDF-előbeállításfájl: {e} +@msg not a RegExp object nem RegExp objektum +@msg not a Word document: its WordDocument stream doesn't start with a file information block nem Word-dokumentum: a WordDocument adatfolyama nem fájlinformációs blokkal kezdődik +@msg not a Word document: its compound file has no WordDocument stream nem Word-dokumentum: az összetett fájljában nincs WordDocument adatfolyam +@msg not a Word document: its main part is not a WordprocessingML document nem Word-dokumentum: a fő része nem WordprocessingML-dokumentum +@msg not a Word document: its package has no main document part nem Word-dokumentum: a csomagjában nincs fő dokumentumrész +@msg not a closed path nem zárt útvonal +@msg not a flattener presets file (format `{_1}`) nem lapítási előbeállításfájl (formátum: `{_1}`) +@msg not a flattener presets file: {e} nem lapítási előbeállításfájl: {e} +@msg not a layer or group nem réteg és nem csoport +@msg not a library file: {e} nem könyvtárfájl: {e} +@msg not a mesh envelope point nem hálós burkoló pontja +@msg not a perspective grid presets file (format `{_1}`) nem perspektivikusrács-előbeállításfájl (formátum: `{_1}`) +@msg not a perspective grid presets file: {e} nem perspektivikusrács-előbeállításfájl: {e} +@msg not a print presets file (format `{_1}`) nem nyomtatási előbeállításfájl (formátum: `{_1}`) +@msg not a print presets file: {e} nem nyomtatási előbeállításfájl: {e} +@msg not a regular expression nem reguláris kifejezés +@msg not a stored or deflated central record nem tárolt vagy deflate-tel tömörített központi rekord +@msg not a valid SVGZ file: {e} nem érvényes SVGZ-fájl: {e} +@msg not an RTF file: it doesn't start with {\\rtf nem RTF-fájl: nem ezzel kezdődik: {\\rtf +@msg not an action set: the text doesn't start with /version and a number nem műveletkészlet: a szöveg nem a /version szóval és egy számmal kezdődik +@msg not an image VectorCraft reads (see document.formats) nem olyan kép, amelyet a VectorCraft olvas (lásd: document.formats) +@msg not area type (text in a frame) outside perspective nem perspektíván kívüli területszöveg (keretben lévő szöveg) +@msg not editing an opacity mask nem folyik átlátszósági maszk szerkesztése +@msg not type on a path outside perspective nem perspektíván kívüli útvonalszöveg +@msg nothing prints semmi sem kerül nyomtatásra +@msg nothing to bind nincs mit kötni +@msg nothing to change nincs mit módosítani +@msg nothing to change: give dx, dy, hScale, vScale or rotation nincs mit módosítani: add meg a dx, dy, hScale, vScale vagy rotation értéket +@msg nothing to crop nincs mit körülvágni +@msg nothing to export nincs mit exportálni +@msg nothing to export: the document has no visible art nincs mit exportálni: a dokumentumban nincs látható grafika +@msg nothing to mark nincs mit megjelölni +@msg nothing to print: every artboard picked is blank nincs mit nyomtatni: minden kiválasztott rajztábla üres +@msg nothing to print: the document has no art that prints nincs mit nyomtatni: a dokumentumban nincs nyomtatásra kerülő grafika +@msg nothing to repeat nincs mit ismételni +@msg numbers out of order in {_1} quantifier a számok rossz sorrendben vannak ebben a kvantorban: {_1} +@msg object is not a path az objektum nem útvonal +@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object az objektum átlátszósága nem vehető át kiemelt karakterekre; kapcsold ki az Átlátszóság beállítást a Pipetta beállításaiban, vagy jelöld ki a teljes objektumot +@msg object {_1} can't be set as point type ez az objektum nem állítható be pontszövegként: {_1} +@msg object {_1} has lost its file's contents: relink it ez az objektum elvesztette a fájlja tartalmát: {_1}; csatold újra +@msg object {_1} has no path ennek az objektumnak nincs útvonala: {_1} +@msg object {_1} has no size ennek az objektumnak nincs mérete: {_1} +@msg object {_1} is hidden or locked, or on a locked artboard ez az objektum rejtett vagy zárolt, vagy zárolt rajztáblán van: {_1} +@msg object {_1} is not a placed document ez az objektum nem elhelyezett dokumentum: {_1} +@msg object {_1} is not an Intertwine group ez az objektum nem összefonási csoport: {_1} +@msg object {_1} is not an image ez az objektum nem kép: {_1} +@msg object {_1} is not an image or a linked file ez az objektum nem kép és nem csatolt fájl: {_1} +@msg object {_1} is not an image or a placed document ez az objektum nem kép és nem elhelyezett dokumentum: {_1} +@msg object {_1} is not in this Intertwine group ez az objektum nincs ebben az összefonási csoportban: {_1} +@msg object {_1} needs a 3D effect ehhez az objektumhoz 3D effektus kell: {_1} +@msg object {_1} needs a 3D effect at index {index} a(z) {_1} objektumhoz 3D effektus kell ennél az indexnél: {index} +@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic {_1} objektum: az üreges Kihúzás és élletörés effektusnak nincsenek fedőlapjai; grafika leképezéséhez kapcsold be a Fedőlap beállítást +@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front {_1} objektum: 0 mélységnél az objektumnak egy lapja van; a grafikáit az előlapra képezd le +@msg object {_1}: {_2} {_1} objektum: {_2} +@msg object {_1}: {e} {_1} objektum: {e} +@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams {_1} objektum: a(z) {index} indexű {name} effektusnak nincsenek megvilágítási előbeállításai; a fényeit az effect.setParams paranccsal állítsd be +@msg object {id} has anchors 0–{_1} for `corners`, not {past} a(z) {id} objektum horgonypontjai a `corners` értékhez: 0–{_1}; ilyen nincs: {past} +@msg object {id} has {n} anchors, so no anchor {past} a(z) {id} objektumnak {n} horgonypontja van, ezért ilyen nincs: {past} +@msg objects must be none, move or copy objects: none, move vagy copy kell +@msg objects were left out objektumok maradtak ki +@msg offset out of range az eltolás tartományon kívül esik +@msg on must be true or false on: true vagy false kell +@msg only groups, layers, type and images have a Contents (Characters) row csak a csoportoknak, a rétegeknek, a szövegnek és a képeknek van Tartalom (Karakterek) sora +@msg only interior mesh lines can be deleted csak belső hálóvonalak törölhetők +@msg only solid colours can be added (not gradients or patterns) csak egyszínű színek adhatók hozzá (színátmenetek és mintázatok nem) +@msg only solid-colour swatches can be spot colours csak egyszínű színminták lehetnek direktszínek +@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 ennek az Affinity-fájlnak csak a beágyazott előnézete volt olvasható: a Fájl › Megnyitás paranccsal megnézheted a figyelmeztetésével együtt, vagy előbb exportálj SVG-t vagy PDF-et az Affinityből +@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning ennek az Affinity-fájlnak csak a beágyazott előnézete volt olvasható; a Fájl › Megnyitás paranccsal megnézheted a figyelmeztetésével együtt +@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon és az oszlopbeli hely: a window.panel.float és a window.panel.dock parancshoz tartoznak +@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon és az oszlopbeli hely: nem adható meg mellettük anchor, side, zone vagy before +@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon és az oszlopbeli hely: nem adható meg mellettük x, y, width vagy height +@msg opacity must be a number opacity: szám kell +@msg open a document to look for its linked files a csatolt fájlok kereséséhez nyiss meg egy dokumentumot +@msg operand too large az operandus túl nagy +@msg options must be an object (see file.formatOptions) options: objektum kell (lásd: file.formatOptions) +@msg order is a whole number from 0 to {others}, not `{v}` order: 0 és {others} közötti egész szám, ez nem az: `{v}` +@msg order must be leftToRight or rightToLeft order: leftToRight vagy rightToLeft kell +@msg orientation must be portrait or landscape, not `{o}` orientation: portrait vagy landscape kell, ez nem az: `{o}` +@msg outlineText {v}: true or false outlineText {v}: true vagy false +@msg output intent profile: {e} kimeneti cél profilja: {e} +@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination: nincs ilyen nevű RGB- vagy CMYK-profil: „{name}” +@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination: a(z) {_1} fájlok CMYK színmódúak, „{name}” pedig RGB-profil +@msg output.destination: {e} output.destination: {e} +@msg output.inks: no ink is set to print output.inks: egyetlen festék sincs nyomtatásra állítva +@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent: nincs ilyen nevű profil: „{name}”, a(z) {std} kimeneti cél pedig beágyazza a profilját +@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: nincs ilyen nevű profil: „{name}”; a(z) {std} kimeneti cél beágyaz egy profilt, vagy megnevez egy regisztrált nyomtatási feltételt (outputConditionId és registry) +@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent: a(z) {std} fájlok CMYK-ban nyomtatódnak, „{name}” pedig RGB-profil +@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent: „{name}” szürke profil, amely nem lehet {std} kimeneti cél +@msg overlap: {o} pt is half a tile or more overlap: {o} pt egy fél csempe vagy annál több +@msg overlapping Shaper paths must be in the same layer or group az egymást átfedő Alakformáló-útvonalaknak ugyanabban a rétegben vagy csoportban kell lenniük +@msg overlapping edits egymást átfedő szerkesztések +@msg page art conflicts with image data for {key} az oldal grafikája ütközik ennek a képadataival: {key} +@msg page art conflicts with pattern {name} az oldal grafikája ütközik ezzel a mintázattal: {name} +@msg page art conflicts with swatch {name} az oldal grafikája ütközik ezzel a színmintával: {name} +@msg page art is missing image data for {key} az oldal grafikájából hiányoznak ennek a képadatai: {key} +@msg page art is missing pattern {name} az oldal grafikájából hiányzik ez a mintázat: {name} +@msg page art is missing swatch {name} az oldal grafikájából hiányzik ez a színminta: {name} +@msg page must be a whole number from 1 page: 1-től kezdődő egész szám kell +@msg page thumbnail: {e} oldalbélyegkép: {e} +@msg page {page} of the PDF has no objects a PDF {page}. oldalán nincsenek objektumok +@msg page {page}: `{file}` has {n} {artboards} {page}. oldal: `{file}` tartalma: {n} {artboards} +@msg page {page}: `{name}` has {n} {pages} {page}. oldal: `{name}` tartalma: {n} {pages} +@msg pages must be a range such as "2-3" or a page number, not {v} pages: tartomány (például "2-3") vagy oldalszám kell, ez nem az: {v} +@msg pages: {e} pages: {e} +@msg panel position must be finite a panel pozíciója véges érték kell legyen +@msg parameters must be an object parameters: objektum kell +@msg params must be an object params: objektum kell +@msg passwords can have up to {MAX_PASSWORD} bytes a jelszavak legfeljebb {MAX_PASSWORD} bájtosak lehetnek +@msg path chunks must share their boundary point az útvonaldarabok határpontjának közösnek kell lennie +@msg path has no open end az útvonalnak nincs nyitott vége +@msg paths must be strings paths: karakterláncok kellenek +@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false esetén minden rajztábla kell ahhoz, hogy a szerkeszthető grafika megmaradjon +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false esetén a rajztáblák kellenek a szerkeszthető grafika megtartásához, ha a useArtboards értéke false +@msg pdfs `{m}`: single or multiple pdfs `{m}`: single vagy multiple +@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom: "artwork" vagy "swatches", ez nem az: {v} +@msg pins and moved must be non-empty lists of the same length pins és moved: azonos hosszúságú, nem üres listák kellenek +@msg pins must be [[x,y]…] pins: [[x,y]…] kell +@msg pins must be finite pins: véges értékek kellenek +@msg place is above, below or inside, not `{other}` place: above, below vagy inside, ez nem az: `{other}` +@msg plane must be left|right|ground plane: left|right|ground kell +@msg plane must be left|right|ground|none plane: left|right|ground|none kell +@msg point is outside the mesh a pont a hálón kívül esik +@msg point x must be a number a pont x értéke szám kell legyen +@msg point y must be a number a pont y értéke szám kell legyen +@msg points must be [t, left, right] with non-negative widths points: [t, left, right] kell nemnegatív szélességekkel +@msg points must be [x,y] pairs points: [x,y] párok kellenek +@msg points must be a non-empty [[x,y,pressure?]…] list points: nem üres [[x,y,pressure?]…] lista kell +@msg points must be a non-empty list of finite [x,y] points: véges [x,y] értékek nem üres listája kell +@msg points must be an array or null points: tömb vagy null kell +@msg points must be finite points: véges értékek kellenek +@msg points must be finite [x,y] pairs points: véges [x,y] párok kellenek +@msg points must be finite canvas points points: véges vászonpontok kellenek +@msg points must contain 1–4096 coordinates points: 1–4096 koordinátát kell tartalmaznia +@msg position must be normal, superscript or subscript position: normal, superscript vagy subscript kell +@msg position must be topLeft, topRight, bottomLeft or bottomRight position: topLeft, topRight, bottomLeft vagy bottomRight kell +@msg positions must be between 0 and 100000 pt positions: 0 és 100000 pt közötti értékek kellenek +@msg ppi must be a positive number, not {ppi} ppi: pozitív szám kell, ez nem az: {ppi} +@msg ppi must be between 1 and 2400 ppi: 1 és 2400 közötti érték kell +@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}`: nem transforms|bounds|fileDimensions|fit|fill +@msg preserveShape must be clippingMask|transparency preserveShape: clippingMask|transparency kell +@msg preset `{id}`: mobile or density preset `{id}`: mobile vagy density +@msg preset must be standard, diffuse, topLeft or right preset: standard, diffuse, topLeft vagy right kell +@msg preset must be the name of a print preset preset: egy nyomtatási előbeállítás neve kell +@msg previewMode must be default, pixel or overprint previewMode: default, pixel vagy overprint kell +@msg print settings: {e} nyomtatási beállítások: {e} +@msg procedures nest too deeply az eljárások túl mélyen ágyazódnak egymásba +@msg procedures nested too deeply az eljárások túl mélyen vannak egymásba ágyazva +@msg quotes must be {double?, single?} quotes: {double?, single?} kell +@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k}: egy nyitó és egy záró idézőjel kell, például "“”" +@msg range out of order in character class rossz sorrendű tartomány a karakterosztályban +@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi: 1–2400 kell (72 képernyő, 150 közepes, 300 magas) +@msg rating must be a whole number from 0 to {MAX_RATING} rating: 0 és {MAX_RATING} közötti egész szám kell +@msg rect and {k} contradict each other: give the area or the artboards rect és {k}: ellentmondanak egymásnak; a területet vagy a rajztáblákat add meg +@msg rect applies to raster formats, not {_1} rect: raszteres formátumokra vonatkozik, erre nem: {_1} +@msg rect is [x, y, width, height] in points rect: [x, y, width, height] pontban megadva +@msg rect must be [x, y, width, height] rect: [x, y, width, height] kell +@msg rect must be [x, y, width, height] with a positive width and height rect: [x, y, width, height] kell pozitív szélességgel és magassággal +@msg rect needs finite numbers and a positive width and height rect: véges számok, valamint pozitív szélesség és magasság kell +@msg redraw needs id redraw: id is kell hozzá +@msg redraw within must be a finite distance of 0 or more redraw within: 0 vagy nagyobb véges távolság kell +@msg reduce of an empty array with no initial value reduce hívása üres tömbön, kezdőérték nélkül +@msg registration must be 0..8 registration: 0..8 kell +@msg regular expression flag '{_1}' given twice a reguláris kifejezés jelzője kétszer szerepel: '{_1}' +@msg release a layer or a group réteget vagy csoportot oldj fel +@msg replace keeps the replaced object's place and transform: drop template, at and rect replace: megtartja a lecserélt objektum helyét és transzformációját; hagyd el a template, at és rect értéket +@msg replace: select the one object to replace replace: jelöld ki azt az egy objektumot, amelyet le kell cserélni +@msg resolution must be a number or screen|medium|high resolution: szám vagy screen|medium|high kell +@msg resolution must be between 1 and 2400 ppi resolution: 1 és 2400 ppi közötti érték kell +@msg rotation must be a number of degrees rotation: fokban megadott szám kell +@msg rotation must be finite rotation: véges érték kell +@msg rows and cols must be 1..50 rows és cols: 1..50 kell +@msg rows and columns must be 1..500 rows és columns: 1..500 kell +@msg rows of objects whose appearances differ can't be reordered together eltérő megjelenésű objektumok sorai nem rendezhetők át együtt +@msg sampled text would exceed the canvas (coordinates out of range) a mintavételezett szöveg túllépné a vásznat (a koordináták tartományon kívül esnek) +@msg save the document first: Package collects a saved document előbb mentsd a dokumentumot: a Csomagolás mentett dokumentumot gyűjt össze +@msg save the document first: copy puts the file in its folder előbb mentsd a dokumentumot: a copy a dokumentum mappájába teszi a fájlt +@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}`: szorzó (például 2x), szélesség (100w), magasság (100h) vagy felbontás (72ppi) kell +@msg scale must be a number above 0, at most 64 scale: 0-nál nagyobb, legfeljebb 64 értékű szám kell +@msg scale must be non-zero scale: nem lehet nulla +@msg scope must be selection or all (got {other}) scope: selection vagy all kell (kapott érték: {other}) +@msg scribble is too long a firka túl hosszú +@msg segment does not exist a szakasz nem létezik +@msg select a Shaper group jelölj ki egy alakformáló csoportot +@msg select a blend to set easing or color shift a gyorsítás vagy a színeltolás beállításához jelölj ki egy összemosást +@msg select a compound shape jelölj ki egy összetett alakzatot +@msg select a graph jelölj ki egy diagramot +@msg select a group or layer with visible, unlocked members jelölj ki egy csoportot vagy réteget, amelynek látható, nem zárolt tagjai vannak +@msg select a layer jelölj ki egy réteget +@msg select a text object jelölj ki egy szövegobjektumot +@msg select an SVG filter jelölj ki egy SVG-szűrőt +@msg select an image (or an Image Trace group) jelölj ki egy képet (vagy egy képvektorizálási csoportot) +@msg select an image, or pass id jelölj ki egy képet, vagy add meg az id értéket +@msg select an object (or pass `id`) jelölj ki egy objektumot (vagy add meg az `id` értéket) +@msg select an object with a 3D effect jelölj ki egy 3D effektussal rendelkező objektumot +@msg select an object with a 3D effect, or give its id in ids jelölj ki egy 3D effektussal rendelkező objektumot, vagy add meg az azonosítóját az ids értékben +@msg select an object with a freeform gradient jelölj ki egy szabad formájú színátmenettel rendelkező objektumot +@msg select an object with an opacity mask jelölj ki egy átlátszósági maszkkal rendelkező objektumot +@msg select area type jelölj ki területszöveget +@msg select area type (text in a frame) jelölj ki területszöveget (keretben lévő szöveget) +@msg select area type and closed paths (at least one area type object) jelölj ki területszöveget és zárt útvonalakat (legalább egy területszöveg-objektumot) +@msg select area type whose first line can fit its frame jelölj ki olyan területszöveget, amelynek az első sora elfér a keretében +@msg select artwork to add its colours jelölj ki grafikát a színei hozzáadásához +@msg select artwork to make a group of its colours jelölj ki grafikát, hogy a színeiből csoport készüljön +@msg select artwork to recolour, or give a colour `group` jelölj ki átszínezendő grafikát, vagy adj meg színcsoportot a `group` értékben +@msg select exactly two path ends to join pontosan két összekapcsolandó útvonalvéget jelölj ki +@msg select members of a compound shape jelöld ki egy összetett alakzat tagjait +@msg select objects or give ids jelölj ki objektumokat, vagy add meg az ids értéket +@msg select objects to align to the guide jelöld ki a segédvonalhoz igazítandó objektumokat +@msg select one Intertwine group egyetlen összefonási csoportot jelölj ki +@msg select one image or pass id egyetlen képet jelölj ki, vagy add meg az id értéket +@msg select one layer, group or object to rename egyetlen átnevezendő réteget, csoportot vagy objektumot jelölj ki +@msg select paths jelölj ki útvonalakat +@msg select paths or compound paths jelölj ki útvonalakat vagy összetett útvonalakat +@msg select rows or objects to collect jelöld ki az összegyűjtendő sorokat vagy objektumokat +@msg select something to export jelölj ki valamit az exportáláshoz +@msg select something to export (or turn off selectedOnly) jelölj ki valamit az exportáláshoz (vagy kapcsold ki a selectedOnly beállítást) +@msg select text to redefine the style from jelölj ki szöveget, amelyből a stílus újradefiniálható +@msg select the art and, on top of it, the mask object jelöld ki a grafikát és a fölötte lévő maszkobjektumot +@msg select the art for the design jelöld ki a diagrammintához szánt grafikát +@msg select the art for the pattern tile jelöld ki a mintacsempéhez szánt grafikát +@msg select the art to add first előbb jelöld ki a hozzáadandó grafikát +@msg select the art to collect (or give its ids) jelöld ki az összegyűjtendő grafikát (vagy add meg az azonosítóit) +@msg select the art to weed jelöld ki a gyomlálandó grafikát +@msg select the content and a path on top jelöld ki a tartalmat és egy fölötte lévő útvonalat +@msg select the images to relink, or pass ids jelöld ki az újracsatolandó képeket, vagy add meg az ids értéket +@msg select the images, or pass ids jelöld ki a képeket, vagy add meg az ids értéket +@msg select the linked images or placed documents, or pass ids jelöld ki a csatolt képeket vagy az elhelyezett dokumentumokat, vagy add meg az ids értéket +@msg select the object to copy from jelöld ki azt az objektumot, amelyről a másolás történjen +@msg select the objects (or give their ids) jelöld ki az objektumokat (vagy add meg az azonosítóikat) +@msg select the objects to attach to the path jelöld ki az útvonalhoz csatolandó objektumokat +@msg select the objects to bind jelöld ki a kötendő objektumokat +@msg select two or more objects to intertwine jelölj ki két vagy több összefonandó objektumot +@msg select two or more slices to combine jelölj ki két vagy több egyesítendő szeletet +@msg select type on a path jelölj ki útvonalszöveget +@msg select type on a path or area type jelölj ki útvonalszöveget vagy területszöveget +@msg selection `{m}`: one or each selection `{m}`: one vagy each +@msg settings is an object of document.exportForScreens params settings: a document.exportForScreens paramétereiből álló objektum +@msg settings must be an object settings: objektum kell +@msg show `{show}`: all, missing, modified or embedded show `{show}`: all, missing, modified vagy embedded +@msg show must be true or false show: true vagy false kell +@msg side is left or right, not `{v}` side: left vagy right, ez nem az: `{v}` +@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size: "small", "medium", "large" vagy 0-nál nagyobb, legfeljebb {_1} pontérték kell +@msg size must be positive size: pozitív érték kell +@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode: "fixed" vagy "pressure", ez nem az: `{other}` +@msg sizeMode is "fixed" or "pressure", not {other} sizeMode: "fixed" vagy "pressure", ez nem az: {other} +@msg sizeVariation must be 0 or more sizeVariation: 0 vagy nagyobb érték kell +@msg smallCaps must be true or false smallCaps: true vagy false kell +@msg sort `{x}`: name, kind or status sort `{x}`: name, kind vagy status +@msg source paths are too large for Shaper a forrásútvonalak túl nagyok az Alakformálóhoz +@msg source text transform is collapsed or unrepresentable a forrásszöveg transzformációja összeesett vagy nem ábrázolható +@msg spot, mode and group apply to solid colours only spot, mode és group: csak egyszínű színekre vonatkozik +@msg status `{s}`: linked, embedded, missing or modified status `{s}`: linked, embedded, missing vagy modified +@msg steps must be 1..1000 steps: 1..1000 kell +@msg steps must be 1..{MAX_STEPS} steps: 1..{MAX_STEPS} kell +@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) {i}. színátmenetpont: `plates`: négy 0 és 1 közötti szám kell (C, M, Y, K) +@msg stopped before the archive was finished leállt, mielőtt az archívum elkészült volna +@msg strokeMode must be within|along|across, got {m} strokeMode: within|along|across kell, kapott érték: {m} +@msg strokeOptions must be an object of stroke.set options strokeOptions: a stroke.set beállításaiból álló objektum kell +@msg strokeWidth must be a positive number of pixels strokeWidth: képpontban megadott pozitív szám kell +@msg subpath {si} is not an open subpath ez a részútvonal nem nyitott: {si} +@msg subpaths required when joining the same path ugyanazon útvonal összekapcsolásakor a subpaths kötelező +@msg swatchConflict must be "merge" or "add", not {v} swatchConflict: "merge" vagy "add" kell, ez nem az: {v} +@msg table must be true or false table: true vagy false kell +@msg takes one name egy nevet fogad +@msg target text transform is collapsed or unrepresentable a célszöveg transzformációja összeesett vagy nem ábrázolható +@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text: {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} kell +@msg text transform is collapsed or unrepresentable a szöveg transzformációja összeesett vagy nem ábrázolható +@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces: 2 és 100 közötti egész szám kell +@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}`: a "{_1}" vagy a "{_2}" értéket használd +@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage: add meg egy képfájl {path} vagy {name, dataBase64} értékét, vagy egy textúrát úgy, ahogy az effect.list megadja +@msg textureImage: {e} textureImage: {e} +@msg that makes more than {MAX_PIECES} slices ez több mint {MAX_PIECES} szeletet eredményezne +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer ez túl sok darabot (legfeljebb 100000) vagy szaggatási lépést eredményezne: egyszerűsítsd a szaggatási mintát, vagy növeld a vonalkák hosszát +@msg that object is not in the overlap ez az objektum nincs az átfedésben +@msg the Affinity document has no page with a size az Affinity-dokumentumban nincs mérettel rendelkező oldal +@msg the CMYK image couldn't be written a CMYK-képet nem sikerült kiírni +@msg the Layers panel is not shown a Rétegek panel nem látható +@msg the Layers panel's search, filter or Show Layers Only hides its row a Rétegek panel keresése, szűrője vagy a Csak rétegek megjelenítése beállítás elrejti a sorát +@msg the Objects on Path group is hidden or locked az Objektumok útvonalon csoport rejtett vagy zárolt +@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected a PDF biztonsági beállításai a jelszavak nélkül lettek mentve: add meg újra az openPassword vagy a permissionsPassword értéket, vagy a védelem nélküli exportáláshoz a security értéket passwordRequired nélkül +@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) az RTF-fájlban túl sok a bekezdés vagy a formázásváltás ahhoz, hogy szövegként elhelyezhető legyen (legfeljebb {MAX_PARAGRAPHS} bekezdés és {MAX_RUNS} szövegrész) +@msg the RTF file is too large to place as type (at most {_1} MB) az RTF-fájl túl nagy ahhoz, hogy szövegként elhelyezhető legyen (legfeljebb {_1} MB) +@msg the SVG expands to more than {0} nested SVG images az SVG kibontva több mint {0} beágyazott SVG-képet tartalmaz +@msg the SVG expands to more than {0} objects az SVG kibontva több mint {0} objektumot tartalmaz +@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes az SVG-fájl nagyobb, mint {MAX_IMPORT_BYTES} bájt +@msg the SVG has no drawable objects az SVG-ben nincsenek kirajzolható objektumok +@msg the SVGZ file unpacks to more than {_1} MB az SVGZ-fájl kicsomagolva nagyobb, mint {_1} MB +@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place a Word-dokumentumban több mint {MAX_PAGES} oldalnyi tulajdonság van, ez túl sok az elhelyezéshez +@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place a Word-dokumentumban több mint {MAX_RUNS} karakter- vagy bekezdéstulajdonság-szakasz van, ez túl sok az elhelyezéshez +@msg the Word document has more than {_1} MB of formatting properties, too many to place a Word-dokumentumban több mint {_1} MB formázási tulajdonság van, ez túl sok az elhelyezéshez +@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) a Word-dokumentumban túl sok a szöveg vagy a formázás ahhoz, hogy szövegként elhelyezhető legyen (legfeljebb {MAX_PARAGRAPHS} bekezdés, {MAX_RUNS} szövegrész és {_1} MB szöveg) +@msg the Word document has too much text to place as type (at most {_1} MB) a Word-dokumentumban túl sok a szöveg ahhoz, hogy szövegként elhelyezhető legyen (legfeljebb {_1} MB) +@msg the Word document is encrypted or protected with a password; save it without one to place it a Word-dokumentum titkosított vagy jelszóval védett; az elhelyezéshez mentsd jelszó nélkül +@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) a Word-dokumentum túl nagy az elhelyezéshez (a(z) {name} adatfolyama több mint {_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 a Word-dokumentum utoljára gyorsmentéssel lett mentve, amelyet a VectorCraft nem olvas; mentsd újra kikapcsolt gyorsmentéssel +@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 a Word-dokumentumot a Word 6.0 vagy a Word 95 mentette, ezt a VectorCraft nem olvassa; mentsd Word 97-2003 dokumentumként +@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place a Word-dokumentum szövege több mint {MAX_PIECES} darabból áll, ez túl sok az elhelyezéshez +@msg the XML operation needs an XML or XMLList value az XML-művelethez XML vagy XMLList érték kell +@msg the XPath expression does not evaluate to a node-set az XPath-kifejezés eredménye nem csomóponthalmaz +@msg the XPath expression ends early az XPath-kifejezés túl korán ér véget +@msg the XPath expression has an unmatched ) az XPath-kifejezésben pár nélküli ) van +@msg the XPath expression has an unmatched ] az XPath-kifejezésben pár nélküli ] van +@msg the XPath expression has an unmatched closing bracket az XPath-kifejezésben pár nélküli záró zárójel van +@msg the XPath expression selects a namespace node, which is not an XML value az XPath-kifejezés névtércsomópontot jelöl ki, amely nem XML-érték +@msg the XPath expression selects the root node, which is not an XML value az XPath-kifejezés a gyökércsomópontot jelöli ki, amely nem XML-érték +@msg the XPath function {name} needs a node-set ehhez az XPath-függvényhez csomóponthalmaz kell: {name} +@msg the XPath function {name} takes the wrong number of arguments ez az XPath-függvény nem megfelelő számú argumentumot kapott: {name} +@msg the XPath name functions need a node-set az XPath névfüggvényeihez csomóponthalmaz kell +@msg the XPath prefix {shown} is not bound to a namespace ez az XPath-előtag nincs névtérhez kötve: {shown} +@msg the XPath variable ${shown} is not bound ez az XPath-változó nincs kötve: ${shown} +@msg the action set ids are not a list a műveletkészlet-azonosítók nem alkotnak listát +@msg the active paint is not a gradient az aktív festés nem színátmenet +@msg the argument list is not an object az argumentumlista nem objektum +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size a rajztáblák nem férnek el a vásznon: kevesebb rajztábla, kisebb térköz vagy kisebb méret kell +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out a grafika túl nagy az EPS-hez: ami 256 MB PostScript után következik, az kimaradt +@msg the bitmaps hold more pixels than one file may a bitképek több képpontot tartalmaznak, mint amennyi egy fájlban megengedett +@msg the character's size can't be measured a karakter mérete nem mérhető meg +@msg the code runs past its last instruction a kód túlfut az utolsó utasításán +@msg the compound shape is too complex to expand az összetett alakzat túl bonyolult a kibontáshoz +@msg the container of the listed objects is gone a felsorolt objektumok tárolója már nincs meg +@msg the copy `{user_id}` can't be read as a library a másolat nem olvasható be könyvtárként: `{user_id}` +@msg the copy would leave the canvas a másolat kikerülne a vászonról +@msg the curve tolerance must be from 0.01 to 10 points a görbetűrés 0,01 és 10 pont között lehet +@msg the custom dictionary couldn't be saved to {_1}: {e} az egyéni szótárat nem sikerült ide menteni: {_1}: {e} +@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is az egyéni szótár nem olvasható: {_1} ({why}), ezért változatlan marad +@msg the cut cannot be made on this art: {e} a vágás ezen a grafikán nem végezhető el: {e} +@msg the data isn't an EMF or WMF picture az adatok nem EMF- vagy WMF-képet alkotnak +@msg the data isn't an EPS or PostScript picture az adatok nem EPS- vagy PostScript-képet alkotnak +@msg the deferred paste is no longer available a halasztott beillesztés már nem érhető el +@msg the design is too big to draw at every data point of this graph a diagramminta túl nagy ahhoz, hogy a diagram minden adatpontjánál kirajzolható legyen +@msg the design is too big to draw in every column of this graph a diagramminta túl nagy ahhoz, hogy a diagram minden oszlopában kirajzolható legyen +@msg the dialog it was opened over is no longer open a párbeszédablak, amely fölött megnyílt, már nincs nyitva +@msg the dictionary stack overflowed a szótárverem túlcsordult +@msg the document already has an SVG filter named {name} a dokumentumban már van ilyen nevű SVG-szűrő: {name} +@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it a dokumentumban van egy másik ilyen nevű SVG-szűrő: {name}; használd az svgFilter.apply parancsot, vagy a szerkesztéséhez az svgFilter.define parancsot a `replace` értékkel +@msg the document has no SVG filter named {name} a dokumentumban nincs ilyen nevű SVG-szűrő: {name} +@msg the document has no SVG filter named {r} a dokumentumban nincs ilyen nevű SVG-szűrő: {r} +@msg the document has no color swatches a dokumentumban nincsenek színes színminták +@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 a dokumentum színmintái {_1} színt tartalmaznak; a document színcsökkentés legfeljebb 255-öt fogad +@msg the editing data's hash has no place a szerkesztési adatok kivonatának nincs helye +@msg the entities the document declares expand to too much text a dokumentumban deklarált entitások túl sok szöveggé bomlanak ki +@msg the envelope's content has no bounds a burkoló tartalmának nincsenek határai +@msg the envelope's mesh is malformed a burkoló hálója hibás szerkezetű +@msg the file holds no `variableSets` element a fájlban nincs `variableSets` elem +@msg the file is larger than {_1} MB a fájl nagyobb, mint {_1} MB +@msg the file is too large a fájl túl nagy +@msg the file isn't valid SVG: {e} a fájl nem érvényes SVG: {e} +@msg the file names no plate a fájl nem nevez meg színkivonatot +@msg the file was updated or encrypted already a fájl már frissítve vagy titkosítva lett +@msg the first character of a line has no character before it to kern: it moves only up and down (dy) a sor első karaktere előtt nincs karakter, amelyhez alávágható lenne: csak felfelé és lefelé mozdul (dy) +@msg the font {_1} is not available ez a betűtípus nem érhető el: {_1} +@msg the format has no file extension a formátumnak nincs fájlkiterjesztése +@msg the function's prototype is not an object a függvény prototípusa nem objektum +@msg the graphic can't be read: {e} a grafika nem olvasható: {e} +@msg the graphic is empty a grafika üres +@msg the graphic's data is damaged a grafika adatai sérültek +@msg the graphic's document has no artboard a grafika dokumentumában nincs rajztábla +@msg the guide is hidden or locked a segédvonal rejtett vagy zárolt +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them a(z) `{name}` változó `{cell}` fejléce a(z) `{other}` változóként olvasódna vissza: nevezd át az egyiket +@msg the image `{path}` isn't followed: {r} a kép nem lesz követve: `{path}`: {r} +@msg the image has no embedded pixels a képnek nincsenek beágyazott képpontjai +@msg the image is empty a kép üres +@msg the image is fully transparent a kép teljesen átlátszó +@msg the image is not on an artboard; pass rect a kép nincs rajztáblán; add meg a rect értéket +@msg the image's pixels are missing a kép képpontjai hiányoznak +@msg the image's pixels aren't in the document a kép képpontjai nincsenek a dokumentumban +@msg the image's pixels can't be read: {e} a kép képpontjai nem olvashatók: {e} +@msg the image's transparency doesn't fit it a kép átlátszósága nem illik a képhez +@msg the job would print more than {MAX_PAGES} pages a feladat több mint {MAX_PAGES} oldalt nyomtatna +@msg the job would print more than {_1} pages a feladat több mint {_1} oldalt nyomtatna +@msg the layers hold no art to mask a rétegekben nincs maszkolható grafika +@msg the layout moved az elrendezés elmozdult +@msg the library has a group `{name}` already a könyvtárban már van ilyen csoport: `{name}` +@msg the library has no group `{name}` a könyvtárban nincs ilyen csoport: `{name}` +@msg the library has no item `{item}` a könyvtárban nincs ilyen elem: `{item}` +@msg the library has no item `{other}` a könyvtárban nincs ilyen elem: `{other}` +@msg the library holds {MAX_GROUPS} groups already a könyvtár már {MAX_GROUPS} csoportot tartalmaz +@msg the library holds {MAX_ITEMS} items of this kind already a könyvtár már {MAX_ITEMS} ilyen fajta elemet tartalmaz +@msg the library's file is outside the read roots a könyvtár fájlja kívül esik az olvasási gyökereken +@msg the line is too long, or the paths it passes too many, to check a vonal túl hosszú, vagy túl sok útvonalon halad át az ellenőrzéshez +@msg the method needs an XML value a metódushoz XML-érték kell +@msg the name is empty a név üres +@msg the object has no fill, stroke or effect az objektumnak nincs kitöltése, körvonala vagy effektusa +@msg the object has no graphic style (pass `name`) az objektumnak nincs grafikai stílusa (add meg a `name` értéket) +@msg the objects don't match the plan az objektumok nem egyeznek a tervvel +@msg the open and permissions passwords must differ a megnyitási és a jogosultsági jelszónak különböznie kell +@msg the operand stack overflowed az operandusverem túlcsordult +@msg the page has no area az oldalnak nincs területe +@msg the page's type in an unread object's box isn't in its page art az oldal egy be nem olvasott objektum keretében lévő szövege nincs benne az oldal grafikájában +@msg the paint behind the proxy is not a gradient a mintadoboz mögötti festés nem színátmenet +@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) a festés nem szabad formájú színátmenet (alkalmazz egyet a paint.editGradient {kind} paranccsal) +@msg the parameters or body do not form one function a paraméterek vagy a törzs nem alkotnak egyetlen függvényt +@msg the parent must be a layer or sublayer a szülő réteg vagy alréteg kell legyen +@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it a beillesztett SVG-szűrő ({name}) másképp rajzol, mint a dokumentum azonos nevű szűrője, és nem jut neki szabad név +@msg the path can't be extended az útvonal nem hosszabbítható meg +@msg the path can't be inside one of the objects to attach az útvonal nem lehet az egyik csatolandó objektum belsejében +@msg the path has no segments to measure az útvonalnak nincs mérhető szakasza +@msg the path is empty az útvonal üres +@msg the path is too short to shorten that much az útvonal túl rövid ahhoz, hogy ennyivel rövidíthető legyen +@msg the paths are too many or too tangled to find where they cross túl sok az útvonal, vagy túlságosan összegabalyodtak ahhoz, hogy a metszéspontjaik megtalálhatók legyenek +@msg the pen table holds more than 255 pens a tolltábla több mint 255 tollat tartalmaz +@msg the picture has no objects az ábrán nincsenek objektumok +@msg the plates can't be kept a színkivonatok nem tarthatók meg +@msg the position table does not match the instructions a pozíciótábla nem egyezik az utasításokkal +@msg the program runs too long a program túl sokáig fut +@msg the program uses too much memory a program túl sok memóriát használ +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes a bekérés szövege hosszabb {MAX_PROMPT_BYTES} bájtnál +@msg the radix must be between 2 and 36 a számrendszer alapja 2 és 36 között lehet +@msg the rectangle must have a width and a height a téglalapnak szélesség és magasság kell +@msg the rectangle needs a size and must lie on the canvas a téglalapnak méret kell, és a vásznon kell lennie +@msg the rectangle or line reaches too many paths to check a téglalap vagy a vonal túl sok útvonalat ér el az ellenőrzéshez +@msg the right side of 'in' is not an object az 'in' jobb oldala nem objektum +@msg the right side of 'instanceof' is not a function az 'instanceof' jobb oldala nem függvény +@msg the selected art has no width a kijelölt grafikának nincs szélessége +@msg the selected art is nested more than {MAX_DEPTH} levels deep a kijelölt grafika {MAX_DEPTH} szintnél mélyebben van egymásba ágyazva +@msg the selected art is too large to measure a kijelölt grafika túl nagy a méréshez +@msg the selection draws nothing a kijelölés semmit sem rajzol ki +@msg the selection has no area a kijelölésnek nincs területe +@msg the selection has no art to export a kijelölésben nincs exportálható grafika +@msg the selection has no bounds a kijelölésnek nincsenek határai +@msg the selection has no graphic style a kijelölésnek nincs grafikai stílusa +@msg the selection has no outlines to weed around a kijelölésnek nincs körvonala, amely körül gyomlálni lehetne +@msg the slice would leave the canvas a szelet kikerülne a vászonról +@msg the spot colour couldn't be added a direktszínt nem sikerült hozzáadni +@msg the subpath has no ends to extend (it is closed or a single point) a részútvonalnak nincs meghosszabbítható vége (zárt, vagy egyetlen pontból áll) +@msg the symbols are nested too deeply to expand their instances a szimbólumok túl mélyen vannak egymásba ágyazva a példányaik kibontásához +@msg the text file is too large to place as type (at most {_1} MB) a szövegfájl túl nagy ahhoz, hogy szövegként elhelyezhető legyen (legfeljebb {_1} MB) +@msg the text is empty a szöveg üres +@msg the text is over {_1} MB a szöveg nagyobb, mint {_1} MB +@msg the top object must be a path a legfelső objektum útvonal kell legyen +@msg the trace was cancelled a vektorizálás megszakítva +@msg the trailer has no size a trailernek nincs mérete +@msg the type object is flattened to nothing a szövegobjektumból a lapítás után semmi sem marad +@msg the value bind was called on is not a function az érték, amelyen a bind hívása történt, nem függvény +@msg the value is not a constructor az érték nem konstruktor +@msg the value is not a function az érték nem függvény +@msg the value is not a valid XML name az érték nem érvényes XML-név +@msg the value is not an XML attribute name az érték nem XML-attribútumnév +@msg the value is not an XML name az érték nem XML-név +@msg the value of axis `{tag}` must be a number or null a(z) `{tag}` tengely értéke szám vagy null lehet +@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets a változókönyvtár több mint {MAX_LIBRARY_DATASETS} adatkészletet deklarál +@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities a változókönyvtár több mint {MAX_LIBRARY_ENTITIES} entitást deklarál +@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables a változókönyvtár több mint {MAX_LIBRARY_VARIABLES} változót deklarál +@msg the variable library declares no variables a változókönyvtár nem deklarál változókat +@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes a változókönyvtár nagyobb, mint {MAX_LIBRARY_BYTES} bájt +@msg the variable library is not UTF-8 a változókönyvtár nem UTF-8 kódolású +@msg the variable library is not well-formed XML: {e} a változókönyvtár nem jól formált XML: {e} +@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep a változókönyvtár elemei {MAX_LIBRARY_NESTING} szintnél mélyebben vannak egymásba ágyazva +@msg the variable library's document type declaration holds something other than entities of plain text a változókönyvtár dokumentumtípus-deklarációja mást is tartalmaz, mint egyszerű szöveges entitásokat +@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes a változókönyvtár entitásai kibontva nagyobbak, mint {MAX_LIBRARY_BYTES} bájt +@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines a gyomlálóvonalak túl sokszor metszik a grafika körvonalait a méréshez; használj kevesebb vonalat +@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap a gyomlálóvonalak túl sok körvonalszakaszhoz haladnak túl közel a méréshez; használj kevesebb vonalat vagy kisebb hézagot +@msg the weeding lines would be more than {max_pieces} pieces a gyomlálóvonalak több mint {max_pieces} darabból állnának +@msg the window has no dock az ablaknak nincs dokkja +@msg the written PDF has no catalog a kiírt PDF-nek nincs katalógusa +@msg the written PDF has no cross-reference table a kiírt PDF-nek nincs kereszthivatkozási táblája +@msg the written PDF's metadata can't be edited a kiírt PDF metaadatai nem szerkeszthetők +@msg the {_1} plate lies outside the file ez a színkivonat a fájlon kívül esik: {_1} +@msg there are no colour swatches to draw nincs kirajzolható színes színminta +@msg there is already a design named `{name}` már van ilyen nevű diagramminta: `{name}` +@msg there is already a design named `{to}` már van ilyen nevű diagramminta: `{to}` +@msg there is already a toolbar named `{name}` már van ilyen nevű eszköztár: `{name}` +@msg there is already a toolbar named `{to}` már van ilyen nevű eszköztár: `{to}` +@msg there is no library: create one first nincs könyvtár: előbb hozz létre egyet +@msg these paths are too degenerate for Shaper ezek az útvonalak túlságosan elfajultak az Alakformálóhoz +@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 ez bináris DXF-fájl, amely nem olvasható: mentsd ASCII DXF-ként a CAD-alkalmazásból, és azt nyisd meg +@msg this warp's points can't be edited: use Reset with Mesh first ennek a hajlításnak a pontjai nem szerkeszthetők: előbb használd a Visszaállítás hálóval parancsot +@msg thumbnail: no page {v} (the {what} has {count}) bélyegkép: nincs ilyen oldal: {v} (a(z) {what} oldalszáma: {count}) +@msg tileRange of {what}: {e} tileRange ({what}): {e} +@msg title can't be empty title: nem lehet üres +@msg to (the new name) is required to (az új név): kötelező +@msg to must be fill or stroke to: fill vagy stroke kell +@msg to must be smooth or corner to: smooth vagy corner kell +@msg toExponential() digits must be between 0 and 20 toExponential(): a számjegyek száma 0 és 20 között lehet +@msg toFixed() digits must be between 0 and 20 toFixed(): a számjegyek száma 0 és 20 között lehet +@msg toISOString is not a function a toISOString nem függvény +@msg toPrecision() argument must be between 1 and 21 toPrecision(): az argumentum 1 és 21 között lehet +@msg toSource: a getter cannot be written as source toSource: getter nem írható ki forrásként +@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource: nem írható ki forrásként az a tömb, amelynek az elemein kívül tulajdonságai is vannak, nem felsorolható tulajdonsága van, vagy más a prototípusa +@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource: nem írható ki forrásként az az objektum, amelynek nem felsorolható tulajdonsága van, vagy a prototípusa nem Object.prototype +@msg toSource: the source is too long toSource: a forrás túl hosszú +@msg toSource: the value contains itself toSource: az érték önmagát tartalmazza +@msg toSource: the value has too many entries toSource: az érték túl sok elemet tartalmaz +@msg toSource: the value is nested too deep toSource: az érték túl mélyen van egymásba ágyazva +@msg toString is not a function a toString nem függvény +@msg too large for a zip archive: package to a folder túl nagy egy zip-archívumhoz: csomagolj mappába +@msg too many Shaper edits túl sok Alakformáló-szerkesztés +@msg too many `points` (at most 100000) túl sok `points` elem (legfeljebb 100000) +@msg too many arguments túl sok argumentum +@msg too many characters: at most {MAX_CHARS} túl sok karakter: legfeljebb {MAX_CHARS} +@msg too many dots for this size: lower the frequency túl sok raszterpont ehhez a mérethez: csökkentsd a vonalsűrűséget +@msg too many filters over one file túl sok szűrő egyetlen fájlon +@msg too many overlapping paths for Shaper túl sok egymást átfedő útvonal az Alakformálóhoz +@msg too many quantifiers túl sok kvantor +@msg too many saved clips túl sok mentett vágóútvonal +@msg too many saved graphics states túl sok mentett grafikus állapot +@msg toolbar (a toolbar's name) must be a string toolbar (egy eszköztár neve): karakterlánc kell +@msg toolbar name can't be empty az eszköztár neve nem lehet üres +@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v}: "flatten" (ez az alapértelmezés) vagy "omit" +@msg two saved selections would be named `{_1}` két mentett kijelölés neve lenne ugyanaz: `{_1}` +@msg type dimension cannot be represented with this text transform a szövegméret nem ábrázolható ezzel a szövegtranszformációval +@msg type dimension must be finite a szövegméret véges érték kell legyen +@msg unexpected resources váratlan erőforrások +@msg unknown CMYK profile `{name}` ismeretlen CMYK-profil: `{name}` +@msg unknown File Info field `{k}` ismeretlen fájlinformációs mező: `{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`) ismeretlen OpenType-funkció: `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; a - előtag kikapcsolja; a swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom és aalt egy alternatíva számát `tag=N` alakban fogadja, ahol N 0 és {_1} között lehet, például `salt=2`) +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) ismeretlen OpenType-funkció a featureChanges értékben: `{t}` (a `features` által fogadott címkék használhatók) +@msg unknown PDF preset `{name}` (presets: {_1}) ismeretlen PDF-előbeállítás: `{name}` (előbeállítások: {_1}) +@msg unknown Warp style ismeretlen hajlítási stílus +@msg unknown alignToPath `{a}` ismeretlen alignToPath: `{a}` +@msg unknown arrowhead {v} ismeretlen nyílhegy: {v} +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) ismeretlen artboardLayout kulcs: `{k}` (layout, columns, spacing, rightToLeft) +@msg unknown attribute `{attribute}` ismeretlen attribútum: `{attribute}` +@msg unknown attribute `{k}` ismeretlen attribútum: `{k}` +@msg unknown axis `{other}` (vertical|horizontal|free) ismeretlen tengely: `{other}` (vertical|horizontal|free) +@msg unknown blend mode `{b}` ismeretlen keverési mód: `{b}` +@msg unknown blend mode {b} ismeretlen keverési mód: {b} +@msg unknown brush type `{o}` ismeretlen ecsettípus: `{o}` +@msg unknown category `{c}` ismeretlen kategória: `{c}` +@msg unknown category `{c}` ({_1}) ismeretlen kategória: `{c}` ({_1}) +@msg unknown character `{c}` ismeretlen karakter: `{c}` +@msg unknown character style number field ismeretlen karakterstílus-számmező +@msg unknown dither `{d}` (diffusion, pattern, noise or none) ismeretlen dither: `{d}` (diffusion, pattern, noise vagy none) +@msg unknown effect `{e}` ismeretlen effektus: `{e}` +@msg unknown end {e} ismeretlen end: {e} +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) ismeretlen flattenerPreset: `{name}` (High Resolution, Medium Resolution, Low Resolution vagy egy mentett előbeállítás: lásd: flattener.presets.list) +@msg unknown format `{f}` (vcswatches, gpl, ase or css) ismeretlen formátum: `{f}` (vcswatches, gpl, ase vagy css) +@msg unknown from {f} ismeretlen from: {f} +@msg unknown graph type `{t}` ismeretlen diagramtípus: `{t}` +@msg unknown grid colours `{name}` (one of {_1}) ismeretlen rácsszínek: `{name}` (ezek egyike kell: {_1}) +@msg unknown gridType `{t}` ismeretlen gridType: `{t}` +@msg unknown highlight `{v}` ismeretlen highlight: `{v}` +@msg unknown intent `{i}` ismeretlen intent: `{i}` +@msg unknown issue kind {id} ismeretlen problémafajta: {id} +@msg unknown issue kind {v} ismeretlen problémafajta: {v} +@msg unknown knockout state {k} ismeretlen kiütési állapot: {k} +@msg unknown language `{language}` (one of {_1}) ismeretlen nyelv: `{language}` (ezek egyike kell: {_1}) +@msg unknown language `{l}` (one of {_1}) ismeretlen nyelv: `{l}` (ezek egyike kell: {_1}) +@msg unknown method `{m}` ({_1}) ismeretlen módszer: `{m}` ({_1}) +@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) ismeretlen mód: `{other}` (gray, rgb, hsb, lab, cmyk vagy web) +@msg unknown mode `{other}` (mono|cmyk) ismeretlen mód: `{other}` (mono|cmyk) +@msg unknown mode `{other}` (rgb|cmyk|gray|global) ismeretlen mód: `{other}` (rgb|cmyk|gray|global) +@msg unknown mode `{other}` (set, add, subtract) ismeretlen mód: `{other}` (set, add, subtract) +@msg unknown mode `{o}` ismeretlen mód: `{o}` +@msg unknown mode {m} ismeretlen mód: {m} +@msg unknown model `{other}` ismeretlen modell: `{other}` +@msg unknown model `{other}` (keep, new or document) ismeretlen modell: `{other}` (keep, new vagy document) +@msg unknown op {o} ismeretlen op: {o} +@msg unknown order `{o}`: toggle, front, forward, backward or back ismeretlen order: `{o}`; toggle, front, forward, backward vagy back lehet +@msg unknown overprints `{v}` (preserve, simulate or discard) ismeretlen overprints: `{v}` (preserve, simulate vagy discard) +@msg unknown panel ismeretlen panel +@msg unknown panel `{panel}` ismeretlen panel: `{panel}` +@msg unknown parameter `{key}` ismeretlen paraméter: `{key}` +@msg unknown parameter `{within}settings.{format}`; {why} ismeretlen paraméter: `{within}settings.{format}`; {why} +@msg unknown parameter `{within}{key}`; {why} ismeretlen paraméter: `{within}{key}`; {why} +@msg unknown pieLegend `{v}` (none, standard or wedges) ismeretlen pieLegend: `{v}` (none, standard vagy wedges) +@msg unknown piePosition `{v}` (even, ratio or stacked) ismeretlen piePosition: `{v}` (even, ratio vagy stacked) +@msg unknown pieSort `{v}` (none, all or first) ismeretlen pieSort: `{v}` (none, all vagy first) +@msg unknown plate `{n}` ismeretlen színkivonat: `{n}` +@msg unknown preference `{k}` ismeretlen beállítás: `{k}` +@msg unknown profile {id} (see stroke.widthProfile.list) ismeretlen profil: {id} (lásd: stroke.widthProfile.list) +@msg unknown proof target `{t}` ismeretlen proofcél: `{t}` +@msg unknown quotes key `{k}` (double, single) ismeretlen quotes kulcs: `{k}` (double, single) +@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) ismeretlen színcsökkentés: `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform vagy document) +@msg unknown resolution `{v}` ismeretlen felbontás: `{v}` +@msg unknown rule `{rule}` ({_1}) ismeretlen szabály: `{rule}` ({_1}) +@msg unknown rule `{r}` (see color.harmony) ismeretlen szabály: `{r}` (lásd: color.harmony) +@msg unknown setting `{k}` ismeretlen beállítás: `{k}` +@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) ismeretlen alakzat: `{v}` (circle|ellipse|square|diamond|line) +@msg unknown spacing `{o}` (smooth|steps|distance) ismeretlen spacing: `{o}` (smooth|steps|distance) +@msg unknown tileType `{t}` ({_1}) ismeretlen tileType: `{t}` ({_1}) +@msg unknown units ismeretlen mértékegység +@msg unknown units `{u}` ismeretlen mértékegység: `{u}` +@msg unknown value axis `{v}` (left, right or both) ismeretlen értéktengely: `{v}` (left, right vagy both) +@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) ismeretlen variáció: `{v}` (tintsShades|warmCool|vividMuted) +@msg unknown warp style `{s}` ismeretlen hajlítási stílus: `{s}` +@msg unknown {key} `{v}` (none, short or full) ismeretlen {key}: `{v}` (none, short vagy full) +@msg unknown {key} profile `{n}` ismeretlen {key} profil: `{n}` +@msg unknown {k} key `{key}` (size, position) ismeretlen {k} kulcs: `{key}` (size, position) +@msg unmatched ')' pár nélküli ')' +@msg unreadable object olvashatatlan objektum +@msg unsafe destination file name nem biztonságos célfájlnév +@msg unterminated character class lezáratlan karakterosztály +@msg unterminated group lezáratlan csoport +@msg use paths or compound paths útvonalakat vagy összetett útvonalakat használj +@msg useArtboards must be true or false, not {v} useArtboards: true vagy false kell, ez nem az: {v} +@msg value for `{var}` must match its variable kind a(z) `{var}` értékének meg kell felelnie a változó típusának +@msg variable `{_1}` is already defined a változó már definiálva van: `{_1}` +@msg variable `{name}` has no `trait` a(z) `{name}` változónak nincs `trait` értéke +@msg variable `{name}` is already defined a változó már definiálva van: `{name}` +@msg variable `{name}` is declared twice a változó kétszer van deklarálva: `{name}` +@msg variable `{to}` already exists már van ilyen változó: `{to}` +@msg view is required a view kötelező +@msg view must be one of {_1} view: ezek egyike kell: {_1} +@msg warpStyle can only edit an applied Warp effect warpStyle: csak már alkalmazott Hajlítás effektust szerkeszthet +@msg width and height must be more than 0 and at most {limit} width és height: 0-nál nagyobb és legfeljebb {limit} érték kell +@msg width factors must be finite a szélességszorzók véges értékek legyenek +@msg width must be positive width: pozitív érték kell +@msg width out of range a szélesség tartományon kívül esik +@msg width/height must be > 0 width/height: > 0 érték kell +@msg wordGap and lineGap need area type in the selection wordGap és lineGap: területszöveg kell a kijelölésben +@msg x and y must be finite numbers x és y: véges számok kellenek +@msg x and y must be given together x és y: együtt kell megadni +@msg xpath needs its variables as an object xpath: a változóit objektumként kell megadni +@msg xpath needs the expression as a string xpath: a kifejezést karakterláncként kell megadni +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone: anchor is kell hozzá; dokkolj egy panel mellé az anchor értékkel, vagy új dokkoszlopba a side értékkel (Start vagy End) +@msg zoom must be positive zoom: pozitív érték kell +@msg {C}: no effect at index {list} {C}: nincs effektus ennél az indexnél: {list} +@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET} védett, megmarad +@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET} védett: mentsd a beállításokat más néven +@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED}, a beállításai és a gyorsítótára pedig nem helyezhetők vissza innen: {_1}, ide: {_2}: {e} +@msg {LABEL} produced an empty result {LABEL}: az eredmény üres +@msg {_1} MB of document, at most {_2} MB a dokumentum {_1} MB, legfeljebb {_2} MB lehet +@msg {_1} MB of images, at most {_2} MB a képek összesen {_1} MB, legfeljebb {_2} MB lehet +@msg {_1} bytes of RGBA data for a {width} × {height} image {_1} bájt RGBA-adat egy {width} × {height} méretű képhez +@msg {_1} can't be read, or changed since it was read: relink or update it first {_1}: nem olvasható, vagy a beolvasása óta megváltozott; előbb csatold újra vagy frissítsd +@msg {_1} can't go into {into} {_1}: nem kerülhet ide: {into} +@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1}: nem korlátozhatja a nyomtatást kis felbontásra; válassz PDF 1.4 vagy újabb verziót, vagy engedélyezd a nagy felbontású nyomtatást +@msg {_1} files can't be opened yet ezek a fájlok még nem nyithatók meg: {_1} +@msg {_1} files can't be password-protected: remove the passwords or choose no standard {_1} fájlok nem védhetők jelszóval: távolítsd el a jelszavakat, vagy ne válassz szabványt +@msg {_1} files can't be {_2} {_1} fájlokra ez nem lehetséges: {_2} +@msg {_1} files can't carry editing data: turn off preserveEditing {_1} fájlok nem tartalmazhatnak szerkesztési adatokat: kapcsold ki a preserveEditing beállítást +@msg {_1} files can't have PDF layers: turn off createLayers {_1} fájlokban nem lehetnek PDF-rétegek: kapcsold ki a createLayers beállítást +@msg {_1} files need a creation date, and this platform has no clock to give one {_1} fájlokhoz létrehozási dátum kell, ezen a platformon pedig nincs óra, amely megadná +@msg {_1} is not a folder: {e} nem mappa: {_1}: {e} +@msg {_1} isn't a path nem útvonal: {_1} +@msg {_1} options: {e} {_1} beállításai: {e} +@msg {_1} paints nothing to rasterize {_1}: semmi raszterizálhatót nem fest +@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) {_1}: a jelszavak legfeljebb {MAX_LEGACY_PASSWORD} ASCII-karakterből állhatnak (a PDF 1.7 és az újabb verziók bármilyet elfogadnak) +@msg {_1} with linked images or an image map writes several files: use document.export {_1}: csatolt képekkel vagy képtérképpel több fájlt ír ki; használd a document.export parancsot +@msg {_1} writes one file per artboard here: use document.export, or name one artboard {_1}: itt rajztáblánként egy fájlt ír ki; használd a document.export parancsot, vagy nevezz meg egy rajztáblát +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} képpont túl nagy ehhez: {_3} (oldalanként legfeljebb {max} képpont); csökkentsd a léptéket +@msg {_1}: belongs to another account {_1}: másik fiókhoz tartozik +@msg {_1}: not a folder of its own {_1}: nem saját mappa +@msg {_1}: not a plain file {_1}: nem egyszerű fájl +@msg {_1}: {_2} and {_3} are in the way {_1}: útban van: {_2} és {_3} +@msg {code}: {0} {code}: {0} +@msg {dest}: {e} {dest}: {e} +@msg {dir}: {e} {dir}: {e} +@msg {doc} has no folder nincs mappája: {doc} +@msg {file} isn't linked nincs csatolva: {file} +@msg {file} names more than one linked JavaScript file {file}: egynél több csatolt JavaScript-fájlt nevez meg +@msg {folder} is not a package folder nem csomagmappa: {folder} +@msg {folder}: more than {max} files and folders {folder}: több mint {max} fájl és mappa +@msg {folder}: {e} {folder}: {e} +@msg {g} is not a Live Paint group nem Élő festés csoport: {g} +@msg {id} is not a slice nem szelet: {id} +@msg {key} must be 0–{MAX_ADD_AROUND} pt {key}: 0–{MAX_ADD_AROUND} pt kell +@msg {key} must be [x,y] {key}: [x,y] kell +@msg {key} must be a percentage {min}..100 {key}: százalék kell, {min}..100 +@msg {key} must be a positive number, not {n} {key}: pozitív szám kell, ez nem az: {n} +@msg {key} must be a positive number, not {v} {key}: pozitív szám kell, ez nem az: {v} +@msg {key} must be one of {_1} {key}: ezek egyike kell: {_1} +@msg {key} must be {_1}, got {s} {key}: ez kell: {_1}; kapott érték: {s} +@msg {key} must name a floating panel {key}: lebegő panelt kell megneveznie +@msg {key} must name a panel in another group {key}: másik csoportban lévő panelt kell megneveznie +@msg {key} must name a panel that shows {key}: látható panelt kell megneveznie +@msg {key} must name a registered panel {key}: regisztrált panelt kell megneveznie +@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) {kind} fájlokban nincs átlátszóság, a fájlban viszont ez lenne: {what}; előbb lapítsd (object.flattenTransparency) +@msg {kind} images can't be opened (see document.formats) {kind} képek nem nyithatók meg (lásd: document.formats) +@msg {k} `{v}` is not one of the choices {k} `{v}`: nem a választási lehetőségek egyike +@msg {k} has control characters{_1} {k}: vezérlőkaraktereket tartalmaz{_1} +@msg {k} is too long (at most {MAX_TEXT} characters) {k}: túl hosszú (legfeljebb {MAX_TEXT} karakter) +@msg {k} must be > 0 {k}: > 0 érték kell +@msg {k} must be a number (pt) or a length such as "210 mm" {k}: szám (pt) vagy hosszúság kell, például "210 mm" +@msg {k} must be a number from {min} to {max} {k}: {min} és {max} közötti szám kell +@msg {k} must be a number or "auto" {k}: szám vagy "auto" kell +@msg {k} must be a string {k}: karakterlánc kell +@msg {k} must be text {k}: szöveg kell +@msg {k} must be true or false {k}: true vagy false kell +@msg {k} must be {{size?, position?}} {k}: {{size?, position?}} kell +@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}: az egyéni nyílhegy útvonalának kitölthető terület kell, legfeljebb {most} horgonyponttal és {most} részútvonallal, és minden horgonypont legfeljebb {far} nyílhegyegységre lehet a csúcsától +@msg {k}: bad custom arrowhead path: {e} {k}: hibás egyéni nyílhegyútvonal: {e} +@msg {k}: bad custom arrowhead: {e} {k}: hibás egyéni nyílhegy: {e} +@msg {k}: can't read `{s}` as a length {k}: nem olvasható hosszúságként: `{s}` +@msg {k}: custom must be a name {k}: custom: név kell +@msg {label} must be a number at most {max} {label}: legfeljebb {max} értékű szám kell +@msg {label} must be a number from -1296 to 1296 {label}: -1296 és 1296 közötti szám kell +@msg {label} must be a number from 1 to 10000 {label}: 1 és 10000 közötti szám kell +@msg {name} `{s}`: {ids} {name} `{s}`: {ids} +@msg {name} is built in and can't be deleted {name}: beépített elem, nem törölhető +@msg {name} must be a number {name}: szám kell +@msg {name} must be {lo}–{hi}{unit} (got {v}) {name}: {lo}–{hi}{unit} kell (kapott érték: {v}) +@msg {name}: CRC or size mismatch {name}: a CRC vagy a méret nem egyezik +@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}: ennek az Affinity-fájlnak csak a beágyazott előnézete volt olvasható; a Fájl › Megnyitás paranccsal megnézheted a figyelmeztetésével együtt, vagy az elhelyezés előtt exportálj SVG-t vagy PDF-et az Affinityből +@msg {name}: {e} {name}: {e} +@msg {native}; its embedded preview can't be read either ({e}) {native}; a beágyazott előnézete sem olvasható ({e}) +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: a copy csak akkor fogad DCS-fájlt, ha a színkivonatai benne vannak; ott csatold újra, ahol a színkivonatfájljai vannak +@msg {path}: larger than {_1} MB {path}: nagyobb, mint {_1} MB +@msg {shown} is not an XPath 1.0 function nem XPath 1.0-függvény: {shown} +@msg {std} files are CMYK, and the file would have colours in {_1} {std} fájlok CMYK színmódúak, a fájlban viszont ilyen színek is lennének: {_1} +@msg {std} files embed their fonts, and the file would have one that isn't {std} fájlok beágyazzák a betűtípusaikat, a fájlban viszont lenne olyan, amely nincs beágyazva +@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}: ebből a fájlból lett beolvasva ez a dokumentum, és a beolvasás során egyes részek kimaradtak ({what}): a felülírásával ezek végleg elvesznének. Írj inkább másik fájlt, vagy add meg az acknowledgeLoss: true értéket, ha mégis le akarod cserélni +@msg {ty} brushes need selected art (or params.{k}) {ty} ecsetekhez kijelölt grafika kell (vagy params.{k}) +@msg {v} is not an asset id nem elemazonosító: {v} +@msg {v} names no layer or sublayer {v}: nem nevez meg réteget vagy alréteget +@msg {what} can't be intertwined nem fonható össze: {what} +@msg {what} doesn't fit the paper with its bleed and marks {what}: a kifutóval és a jelekkel együtt nem fér el a papíron +@msg {what} is not a function nem függvény: {what} +@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what}: egy ívre több mint {MAX_COPIES_PER_SHEET} példány kerülne; növeld a méretét +@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what}: több mint {MAX_TILES} csempére kerülne nyomtatásra; csökkentsd a méretét +@msg {what} {i} out of range {what} {i}: tartományon kívül esik +@msg `{key}` must be from 0.01 to 1000 pt `{key}`: 0.01 és 1000 pt közötti érték kell +@msg select the banner, or mark every artboard jelöld ki a molinót, vagy jelöld meg az összes rajztáblát +@msg the banner has no width or height a molinónak nincs szélessége vagy magassága +@msg the grommet inset is too large for the banner a ringlik széltől mért távolsága túl nagy a molinóhoz +@msg the hem is too deep for the banner a szegés túl mély a molinóhoz +@msg the spacing makes more than {MAX_GROMMETS} grommets ezzel a térközzel több mint {MAX_GROMMETS} ringli keletkezne +@msg there is no banner to mark nincs megjelölhető molinó +@msg turn on at least one side kapcsolj be legalább egy oldalt +@msg with no hem and no grommets there is nothing to mark szegés és ringlik nélkül nincs mit megjelölni +@msg {saving} failed: {e} {saving}: sikertelen: {e} diff --git a/docs/localization-parity.md b/docs/localization-parity.md index cd8dc33f30..43e94d787c 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -55,7 +55,7 @@ plus review. Native-speaker review is human work and isn't in the hours. | Danish | `da` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one/other plurals | full | | Russian | `ru` | 4,009 / 4,020 | full | | Ukrainian | `uk` | 4,009 / 4,020 (one/few/many plurals) | full | -| Hungarian | `hu` | 5,253 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | full | +| Hungarian | `hu` | 9,166 rows, 3,444 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 40 missing messages for it, and lists 47 menu strings, 25 `tl!` strings, 11 undo labels and 45 swatch names without a row; one plural form | full | | Polish | `pl` | 5,130 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/few/many plurals | full; native review pending | | Finnish | `fi` | 5,318 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/other plurals, 797 `@msg` rows | full; AI-assisted, native proofreading pending | | Czech | `cs` | 5,240 catalog entries, including 795 `@msg` rows (counted 2026-10-21); menus, literals and messages complete when counted | catalog complete; render-site gaps reduced; native review pending | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f3707717e6..20b3bf5941 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -885,6 +885,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The status bar translates the limits a plug-in run can exceed (its instruction, time and memory budgets and its output size) and the error of a plug-in removal that leaves the plug-in's grant in the plug-in store, in every language that translates every message. The untranslated-strings report also lists messages that an error or a status takes from a `&str` constant (`plugin_limit_errors_are_translated`, `message_scan_reads_messages_held_in_constants`). - The status bar translates the plug-in host's other errors (manifest, module and run checks, the output an importer, exporter or effect returns, and the approval a plug-in's permissions need) in every language that translates every message. A JSON object or array inside a message stays as written, and a message that wraps another one, such as `plug-in manifest: {0}`, is matched by its own row before a row that starts with a placeholder. The reasons inside these errors (what is wrong with a parameter's spec or dialog keys, the value a parameter needs, what is wrong with an object, stroke, paint or effect in a plug-in's output) are translated with them, and the untranslated-strings report lists the reasons the plug-in host's error helpers take (`plugin_messages_are_translated_in_every_complete_catalog`, `plugin_errors_translate_with_their_values`, `plugin_error_reasons_are_translated_with_the_error`, `message_scan_reads_plugin_helpers`, `templates_that_start_with_text_match_first`). - The status bar translates the errors a script's `File` and `Folder` objects, `vc.run`, `$.evalFile`, the `$` object's properties and functions, `alert`, `confirm` and `prompt`, `vc.document` and `vc.log` raise, the limits a run can pass, and the errors of Drop Script Engine, and the line for a run that fails (a syntax error, a thrown error, a limit, a stop or an internal error, with the script's name, the line and column, the included file it is in and the files left written), in every language that translates every message. The script's name, paths, numbers, command ids and the text a script throws itself are shown as they are. A failure with no line and column, such as an error in code `eval` compiled, is shown with one colon after the script's name (`an_error_without_a_place_names_the_script_and_kind`). A message template that names a value twice matches only when both places hold the same text, so the line for a failed run is not taken for the message inside it (`a_repeated_placeholder_matches_the_same_text`). The untranslated-strings report also lists the messages the script host's `fail`, `value` and `over` helpers take and the errors the script interpreter throws, and a constant's text comes from the file or crate that declares it, so the Scripts menu's headers are not listed as messages (`script_errors_are_translated`, `script_host_errors_are_translated`, `message_scan_reads_script_interpreter_errors`, `message_scan_reads_constants_of_the_file_and_its_crate`, `message_scan_skips_test_modules_with_any_test_cfg`). + - The Hungarian catalog (`crates/ui-egui/src/i18n/hu.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,444 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report lists 40 Hungarian messages: the composite font, justification order and connect end errors that open #7211, #7116 and #7330 translate, and two messages that no catalog has a row for. The status bar does not use the rows for ``unknown parameter `{within}{key}`; {why}`` and `{name} must be {lo}–{hi}{unit} (got {v})`, because a message with two placeholders in a row cannot be matched (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`). The row for `{k} has control characters{_1}` does not match the File Info error for a multi-line field, where the last placeholder is empty. A native speaker has not reviewed the wording. - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian, Georgian and Catalan ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). 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`). From 4819fdbc0a6bf011aaebeb32856d875e0cb35f33 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 19:45:29 -0800 Subject: [PATCH 09/44] Type: Justification, OpenType, text styles and Convert to Text change every frame of a selected story With the Selection tool, the Justification dialog, the OpenType panel, a Character or Paragraph Styles row (Alt-click and Clear Overrides included) and Type > Bullets and Numbering > Convert to Text now change every frame of each selected threaded story, locked and hidden frames included, in one undo step. Before, they changed only the selected frames. - The Justification dialog keeps the frames of the stories selected when it opened for its preview and OK. - The OpenType panel's Figure menu shows Mixed and its checkboxes show a dash when the characters differ; a click on a dash turns the feature on everywhere. - The style panels highlight a style only when all of the text has it, and show "+" when any of it overrides the style. - Convert to Text takes the same stories as Apply Bullets and Apply Numbers. --- crates/mcp/src/ui_catalog.rs | 2 +- crates/ui-egui/src/dialogs/justification.rs | 77 +++++++++- crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/panels/opentype.rs | 145 +++++++++++++++--- crates/ui-egui/src/panels/paragraph.rs | 21 +-- crates/ui-egui/src/panels/paragraph/tests.rs | 44 ++++++ crates/ui-egui/src/panels/text_styles.rs | 63 +++++--- .../ui-egui/src/panels/text_styles/tests.rs | 74 ++++++++- docs/mcp.md | 4 +- docs/target-app-parity.md | 1 + 10 files changed, 371 insertions(+), 62 deletions(-) diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index e3b57166b7..9eb39e056f 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -260,7 +260,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "type.convertListToText", "Convert to Text", "", - "{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs (or of those the Type tool's selection touches) become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type", + "{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs, in every frame of a threaded story (or of those the Type tool's selection touches), become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type", ), ( "align.horizontalLeft", diff --git a/crates/ui-egui/src/dialogs/justification.rs b/crates/ui-egui/src/dialogs/justification.rs index dc42dcff87..168f5cfda1 100644 --- a/crates/ui-egui/src/dialogs/justification.rs +++ b/crates/ui-egui/src/dialogs/justification.rs @@ -9,8 +9,10 @@ //! Fields: `wordMin`, `wordDesired`, `wordMax` (0..1000), `letterMin`, `letterDesired`, //! `letterMax` (-100..500), `glyphMin`, `glyphDesired`, `glyphMax` (50..200), `autoLeading` //! (0..500), `singleWord` (`fullJustify`, `alignLeft`, `alignCenter`, `alignRight`) and `preview`, -//! with `__initial` (the values it opened with), `__targets` (the type and range it acts on) and, -//! after Select All in threaded text, `__story` (the edited frame, whose story it acts on). +//! with `__initial` (the values it opened with), `__targets` (the type and range it acts on; with +//! the Selection tool, every frame of each story selected when the dialog opened, locked and hidden +//! frames included) and, after Select All in threaded text, `__story` (the edited frame, whose +//! story it acts on). //! //! Behaviour reference (public documentation): //! . @@ -95,6 +97,9 @@ pub fn open(app: &mut VectorcraftApp) -> Result { targets = json!({"ids": [id.0], "start": a, "end": b}); // After Select All in threaded text: every paragraph of every frame ([`params`]). story = crate::panels::character::text_story(app).map(|_| id.0); + } else { + // With the Selection tool: every frame of each selected story. + crate::panels::character::target_selected_stories(app, &mut targets); } let mut values = Map::new(); for (_, prefix, _) in ROWS { @@ -423,6 +428,74 @@ mod tests { } } + /// A threaded story of three frames, its second frame locked and its third hidden, a type object + /// of its own, and the first frame selected with the Selection tool → the app, the frames and + /// the other type object. + fn story_selected_in_its_first_frame() -> (VectorcraftApp, [u64; 3], u64) { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 900, "height": 300})).unwrap(); + let story = "The quick brown fox jumps over the lazy dog.\n".repeat(12); + let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}}); + let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap(); + let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap(); + let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap(); + let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [b]})).unwrap(); + app.run("object.lock", json!({})).unwrap(); + app.run("select.set", json!({"ids": [c]})).unwrap(); + app.run("object.hide", json!({})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + (app, [a, b, c], other) + } + + /// Every paragraph's Auto Leading in `id`. + fn auto_leadings(app: &VectorcraftApp, id: u64) -> Vec { + match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) => (0..t.paragraph_count()).map(|i| t.para_at(i).justification.auto_leading).collect(), + _ => panic!("not text"), + } + } + + /// With the Selection tool a selected frame of threaded text stands for its whole story: the + /// dialog opens blank where a locked or hidden frame differs, previews on every frame, Cancel + /// restores them, and OK changes every frame of the story as it was when the dialog opened, in + /// one undo step, leaving other type and a selection made later alone. + #[test] + fn selection_tool_justification_reaches_every_frame_of_the_story() { + let (mut app, frames, other) = story_selected_in_its_first_frame(); + let [a, _, c] = frames; + for id in frames { + assert!(!auto_leadings(&app, id).is_empty(), "frame {id} holds text"); + } + app.run("text.setFormat", json!({"ids": [c], "autoLeading": 150})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + app.run("ui.justification", json!({})).unwrap(); + assert!(!app.ui.dialog.as_ref().unwrap().fields.contains_key("autoLeading"), "the hidden frame differs"); + set(&mut app, "autoLeading", json!(200)); + set(&mut app, "preview", json!(true)); + frame(&mut app); + for id in frames { + assert!(auto_leadings(&app, id).iter().all(|l| *l == 200.0), "the preview shows on frame {id}"); + } + super::super::cancel(&mut app); + assert!(auto_leadings(&app, a).iter().all(|l| *l == 120.0)); + assert!(auto_leadings(&app, c).iter().all(|l| *l == 150.0), "Cancel restores the hidden frame"); + + app.run("ui.justification", json!({})).unwrap(); + // A selection made while the dialog is open does not change what it acts on. + app.run("select.set", json!({"ids": [other]})).unwrap(); + set(&mut app, "autoLeading", json!(140)); + let undo = app.session.active().unwrap().history.undo.len(); + super::super::confirm(&mut app).unwrap(); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1, "one undo step"); + for id in frames { + assert!(auto_leadings(&app, id).iter().all(|l| *l == 140.0), "frame {id} takes the change"); + } + assert!(auto_leadings(&app, other).iter().all(|l| *l == 120.0), "other type keeps its paragraphs"); + app.run("edit.undo", json!({})).unwrap(); + assert!(auto_leadings(&app, c).iter().all(|l| *l == 150.0), "undo restores the hidden frame"); + } + /// When paragraphs in scope disagree, only shared justification values are filled in. #[test] fn justification_dialog_mixed_paragraph_fields_show_blank() { diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index cd311d45a8..f56755e184 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -261,7 +261,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "type.convertListToText", "Convert to Text", "", - "{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs (or of those the Type tool's selection touches) become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type", + "{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs, in every frame of a threaded story (or of those the Type tool's selection touches), become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type", ), ( "align.horizontalLeft", diff --git a/crates/ui-egui/src/panels/opentype.rs b/crates/ui-egui/src/panels/opentype.rs index 64daf1ac76..5718aea0ab 100644 --- a/crates/ui-egui/src/panels/opentype.rs +++ b/crates/ui-egui/src/panels/opentype.rs @@ -1,6 +1,6 @@ //! OpenType panel: per-character OpenType features (ligatures, alternates, swashes, small caps, -//! ordinals, fractions, figure style, position) for the selected text or the Type tool's selected -//! characters. +//! ordinals, fractions, figure style, position) for the selected text (every frame of each +//! selected threaded story) or the Type tool's selected characters. use egui::Ui; use serde_json::json; @@ -112,6 +112,22 @@ fn position_label(shown: Option) -> &'static str { shown.and_then(|i| POSITIONS.get(i).copied()).unwrap_or("Mixed") } +/// The Figure menu entry for the characters the panel acts on, or `None` when they differ. +fn shown_figure(app: &VectorcraftApp) -> Option { + shared(app, |s| figure(&OtFeatures::from_tags(s.features.iter().map(String::as_str)))) +} + +/// Whether `flag` is on for every character the panel acts on (`Some(true)`), for none of them +/// (`Some(false)`), or only for some (`None`). +fn shown_flag(app: &VectorcraftApp, flag: Flag) -> Option { + shared(app, |s| flag.get(&OtFeatures::from_tags(s.features.iter().map(String::as_str)))) +} + +/// The Figure menu's text for entry `shown`: Mixed when the characters differ. +fn figure_label(shown: Option) -> &'static str { + shown.and_then(|i| FIGURES.get(i).copied()).unwrap_or("Mixed") +} + /// A change one of the panel's controls makes to the features of each character it acts on. #[derive(Clone, Copy)] enum Change { @@ -161,7 +177,8 @@ fn set(app: &mut VectorcraftApp, f: OtFeatures) { } /// Send `tags` as the `key` param (`features` or `featureChanges`) for the Type tool's range or -/// caret, or else for the selected type. +/// caret, or else for every frame of the selected stories +/// ([`super::character::selected_stories`]). fn run_features(app: &mut VectorcraftApp, key: &str, tags: Vec) { // After Select All in threaded text: every frame of the story. let story = super::character::text_story(app).and_then(|story| story.first().copied()); @@ -169,7 +186,11 @@ fn run_features(app: &mut VectorcraftApp, key: &str, tags: Vec) { (Some(_), Some(first)) => ("text.setRangeStyle", json!({ "id": first.0, "story": true })), (Some((id, a, b)), None) if b > a => ("text.setRangeStyle", json!({ "id": id.0, "start": a, "end": b })), (Some((id, _, _)), None) => ("text.setRangeStyle", json!({ "id": id.0 })), - (None, _) => ("text.setStyle", json!({})), + (None, _) => { + let mut p = json!({}); + super::character::target_selected_stories(app, &mut p); + ("text.setStyle", p) + } }; p[key] = json!(tags); if let Err(e) = app.run(cmd, p) { @@ -184,10 +205,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }; let cur = OtFeatures::from_tags(st.features.iter().map(String::as_str)); let mut change = None; + let fig = shown_figure(app); ui.horizontal(|ui| { widgets::dim_label(ui, tl!("Figure:")); - let fig = figure(&cur); - if let Some(i) = widgets::dropdown(ui, "ot-figure", FIGURES[fig], &FIGURES, 170.0) { + if let Some(i) = widgets::dropdown(ui, "ot-figure", figure_label(fig), &FIGURES, 170.0) { change = Some(Change::Figure(i)); } }); @@ -199,26 +220,30 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); widgets::divider(ui); - let mut next = cur; - if let Some(c) = change { - c.apply(&mut next); - } + // A row whose characters differ shows a dash, and a click turns the feature on for all of them. for (label, flag) in rows() { - let on = flag.get(&next); - if widgets::check(ui, label, on, true) { - change = Some(Change::Toggle(flag, !on)); + let shown = shown_flag(app, flag); + if widgets::check3(ui, label, shown, true) { + change = Some(Change::Toggle(flag, shown != Some(true))); } } // The font, not the text, decides which numbered sets can be offered. Each set is an // independent feature, so the menu permits several at once. A font's own label wins over - // the numbered fallback. See the public Stylistic Sets help page. - let sets = offered_sets(&st); + // the numbered fallback. See the public Stylistic Sets help page. A set is checked when every + // character has it. + let sets: Vec<(StylisticSet, Flag, bool)> = offered_sets(&st) + .into_iter() + .map(|set| { + let flag = Flag::Extra(EXTRA_FEATURES[usize::from(set.number - 1)]); + let on = shown_flag(app, flag) == Some(true); + (set, flag, on) + }) + .collect(); ui.add_enabled_ui(!sets.is_empty(), |ui| { ui.menu_button(tl!("Stylistic Sets"), |ui| { widgets::menu_scroll(ui, |ui| { - for set in &sets { - let flag = Flag::Extra(EXTRA_FEATURES[usize::from(set.number - 1)]); - let on = flag.get(&next); + for (set, flag, on) in &sets { + let (flag, on) = (*flag, *on); if widgets::check_name(ui, &set_label(set), on) { change = Some(Change::Toggle(flag, !on)); } @@ -481,6 +506,90 @@ mod tests { } } + /// The features of every run of `id`, each as its tags. + fn frame_features(app: &VectorcraftApp, id: u64) -> Vec> { + match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.runs.iter().map(|r| r.style.features.clone()).collect(), + _ => panic!("not text"), + } + } + + /// With the Selection tool a selected frame of threaded text stands for its whole story: the + /// Figure and Position menus show Mixed when a locked or hidden frame differs, and a choice + /// reaches every frame in one undo step, each keeping its other features, while other type keeps + /// its own. A story selected through two frames and a group is changed once. + #[test] + fn selection_tool_features_reach_every_frame_of_the_story() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 900, "height": 300})).unwrap(); + let story = "The quick brown fox jumps over the lazy dog 1234.\n".repeat(12); + let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}}); + let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap(); + let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap(); + let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap(); + let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other 12", "size": 12})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [b]})).unwrap(); + app.run("object.lock", json!({})).unwrap(); + app.run("select.set", json!({"ids": [c]})).unwrap(); + app.run("object.hide", json!({})).unwrap(); + app.run("text.setStyle", json!({"ids": [c], "features": ["onum", "smcp", "sups", "-liga"]})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + for id in [a, b, c] { + assert!(!frame_features(&app, id).is_empty(), "frame {id} holds text"); + } + assert_eq!(shown_figure(&app), None, "the hidden frame's figures differ"); + assert_eq!(figure_label(shown_figure(&app)), "Mixed"); + assert_eq!(shown_position(&app), None, "the hidden frame's position differs"); + let hidden = frame_features(&app, c); + let has = |app: &VectorcraftApp, id: u64, tag: &str| frame_features(app, id).iter().all(|f| f.iter().any(|t| t == tag)); + + let undo = undo_steps(&app); + edit(&mut app, Change::Figure(1)); + assert_eq!(undo_steps(&app), undo + 1, "one undo step"); + assert_eq!(shown_figure(&app), Some(1)); + for id in [a, b] { + assert!(frame_features(&app, id).iter().all(|f| *f == ["tnum"]), "frame {id}: {:?}", frame_features(&app, id)); + } + assert!( + ["smcp", "sups", "-liga", "tnum"].iter().all(|t| has(&app, c, t)) + && frame_features(&app, c).iter().all(|f| !f.iter().any(|t| t == "onum")), + "{:?}", + frame_features(&app, c) + ); + assert!(frame_features(&app, other).iter().all(Vec::is_empty), "other type keeps its features"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(frame_features(&app, c), hidden, "undo restores the hidden frame"); + + // Standard Ligatures is off in the hidden frame only: its row shows a dash, and a click on + // it turns ligatures on in every frame. + let liga = rows().into_iter().find(|(l, _)| *l == "Standard Ligatures").map(|(_, f)| f); + let Some(liga) = liga else { panic!("no Standard Ligatures row") }; + assert_eq!(shown_flag(&app, liga), None); + let ctx = egui::Context::default(); + let draw = |app: &mut VectorcraftApp, ui: &mut Ui| show(app, ui); + super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], draw); + let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], draw); + let at = super::super::tests_appearance::text_rect(&texts, "Standard Ligatures").center(); + let undo = undo_steps(&app); + super::super::tests_appearance::click(&ctx, &mut app, at, draw); + assert_eq!(undo_steps(&app), undo + 1, "one undo step"); + assert_eq!(shown_flag(&app, liga), Some(true)); + for id in [a, b, c] { + assert!(frame_features(&app, id).iter().all(|f| !f.iter().any(|t| t == "-liga")), "frame {id} has ligatures"); + } + assert!(has(&app, c, "smcp"), "the hidden frame keeps its other features"); + + // Two frames of the story and the other type in a group: each story once. + app.run("object.unlockAll", json!({})).unwrap(); + app.run("select.set", json!({"ids": [a, other]})).unwrap(); + let group = app.run("object.group", json!({})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [group, b]})).unwrap(); + edit(&mut app, Change::Position(1)); + for id in [a, b, c, other] { + assert!(frame_features(&app, id).iter().all(|f| f.iter().filter(|t| *t == "sups").count() == 1), "frame {id} has sups once"); + } + } + /// Type with two positions turned on shows the Position menu as mixed, and the entry of one /// of them turns the other off. #[test] diff --git a/crates/ui-egui/src/panels/paragraph.rs b/crates/ui-egui/src/panels/paragraph.rs index e02d81e14c..e594052f60 100644 --- a/crates/ui-egui/src/panels/paragraph.rs +++ b/crates/ui-egui/src/panels/paragraph.rs @@ -79,13 +79,7 @@ fn para_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) { /// Run `cmd` on the paragraphs the Paragraph panel acts on ([`format`]) and return its result. With /// the Selection tool these are the paragraphs of every frame of the selected stories /// ([`super::character::selected_stories`]). -pub(crate) fn run_on_paragraphs(app: &mut VectorcraftApp, cmd: &str, p: Value) -> Result { - run_on(app, cmd, p, true) -} - -/// [`run_on_paragraphs`], with the Selection tool on the selected type objects alone unless -/// `stories`. -fn run_on(app: &mut VectorcraftApp, cmd: &str, mut p: Value, stories: bool) -> Result { +pub(crate) fn run_on_paragraphs(app: &mut VectorcraftApp, cmd: &str, mut p: Value) -> Result { if let Some((id, a, b)) = super::character::text_editing(app) { super::character::end_typing(app); match super::character::text_story(app) { @@ -97,7 +91,7 @@ fn run_on(app: &mut VectorcraftApp, cmd: &str, mut p: Value, stories: bool) -> R p["end"] = json!(b); } } - } else if stories { + } else { super::character::target_selected_stories(app, &mut p); } app.run(cmd, p) @@ -260,7 +254,7 @@ pub(crate) fn shared_single_word_justify(app: &VectorcraftApp) -> Option(app: &VectorcraftApp, field: impl Fn(&ParaStyle) -> T) -> Option { +pub(crate) fn shared_para(app: &VectorcraftApp, field: impl Fn(&ParaStyle) -> T) -> Option { let mut acc = Shared::None; let mut any = false; visit_paragraphs(app, |p| { @@ -474,12 +468,13 @@ pub(crate) fn number_sample(style: vectorcraft_doc::text::NumberStyle, n: u32) - } /// Type › Bullets and Numbering › Convert to Text (`type.convertListToText`): the markers of the -/// list paragraphs of the selected type, or of those the Type tool's selection touches, become -/// characters of the text and the lists are turned off (`text.convertListToText`, which also moves -/// the Type tool's anchor and caret past the characters inserted at or before them). +/// list paragraphs the Paragraph panel acts on ([`run_on_paragraphs`]: with the Selection tool, +/// every frame of the selected stories) become characters of the text and the lists are turned +/// off (`text.convertListToText`, which also moves the Type tool's anchor and caret past the +/// characters inserted at or before them). pub(crate) fn convert_list_to_text(app: &mut VectorcraftApp) -> Result { text_style(app).ok_or("select some type first")?; - run_on(app, "text.convertListToText", json!({}), false) + run_on_paragraphs(app, "text.convertListToText", json!({})) } /// The Bullets button and the arrow beside it (Paragraph panel). The button makes the paragraphs diff --git a/crates/ui-egui/src/panels/paragraph/tests.rs b/crates/ui-egui/src/panels/paragraph/tests.rs index 180f0b1fa1..f6c9f064dc 100644 --- a/crates/ui-egui/src/panels/paragraph/tests.rs +++ b/crates/ui-egui/src/panels/paragraph/tests.rs @@ -678,6 +678,50 @@ fn convert_to_text_keeps_the_caret_by_its_character_in_threaded_type() { assert_eq!(super::super::character::text_editing(&app).map(|(id, _, _)| id), Some(NodeId(a))); } +/// With the Selection tool, Convert to Text takes the same story as Apply Numbers: the numbers +/// of every frame of the selected story, its locked and hidden frames included, become characters +/// of the text in one undo step, and other type keeps its list. +#[test] +fn selection_tool_convert_to_text_takes_every_frame_of_the_story() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 900, "height": 300})).unwrap(); + let story = "The quick brown fox jumps over the lazy dog.\n".repeat(12); + let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}}); + let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap(); + let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap(); + let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap(); + let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap(); + app.run("text.setFormat", json!({"ids": [other], "list": "bullet"})).unwrap(); + app.run("select.set", json!({"ids": [b]})).unwrap(); + app.run("object.lock", json!({})).unwrap(); + app.run("select.set", json!({"ids": [c]})).unwrap(); + app.run("object.hide", json!({})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + let frame = |app: &VectorcraftApp, id: u64| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) => (t.plain_text(), (0..t.paragraph_count()).map(|i| t.para_at(i).list.is_some()).collect::>()), + _ => panic!("not text"), + }; + let undo_steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len(); + app.run("type.applyNumbers", json!({})).unwrap(); + for id in [a, b, c] { + let (_, lists) = frame(&app, id); + assert!(!lists.is_empty() && lists.iter().all(|l| *l), "Apply Numbers numbers frame {id}"); + } + let before = frame(&app, c); + + let steps = undo_steps(&app); + app.run("type.convertListToText", json!({})).unwrap(); + assert_eq!(undo_steps(&app), steps + 1, "one undo step"); + for id in [a, b, c] { + let (plain, lists) = frame(&app, id); + assert!(lists.iter().all(|l| !*l), "frame {id} leaves the list"); + assert!(plain.contains(".\tThe quick"), "frame {id} holds its numbers as text: {plain:?}"); + } + assert_eq!(frame(&app, other), ("Other".to_string(), vec![true]), "other type keeps its list"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(frame(&app, c), before, "undo restores the hidden frame"); +} + /// Reset Panel turns Hyphenate and Roman Hanging Punctuation off and sets the Hyphenation and /// Justification options back to their defaults in every paragraph it acts on, so the paragraphs /// save neither. diff --git a/crates/ui-egui/src/panels/text_styles.rs b/crates/ui-egui/src/panels/text_styles.rs index 880de94c16..4f8ba904de 100644 --- a/crates/ui-egui/src/panels/text_styles.rs +++ b/crates/ui-egui/src/panels/text_styles.rs @@ -1,10 +1,11 @@ //! Character Styles and Paragraph Styles panels (one implementation, two kinds). //! -//! Click a style to apply it to the selected text (or the Type tool's selected characters); -//! Alt-click applies it and clears overrides. Double-clicking a character style's row opens its -//! Character Style Options (`ui.charStyleOptions`), as the panel menu does; a paragraph style's row -//! opens its Paragraph Style Options (`ui.paraStyleOptions`). The style of the -//! selection is highlighted, with a "+" when the text overrides some of its attributes. New makes +//! Click a style to apply it to the selected text (every frame of each selected threaded story) or +//! the Type tool's selected characters; Alt-click applies it and clears overrides. Double-clicking a +//! character style's row opens its Character Style Options (`ui.charStyleOptions`), as the panel +//! menu does; a paragraph style's row opens its Paragraph Style Options (`ui.paraStyleOptions`). +//! The style all of that text has is highlighted, with a "+" when some of the text overrides some +//! of its attributes; text in several styles highlights none. New makes //! a style from the selected text; Alt-click on New first asks for its name (`ui.newCharStyle`, //! `ui.newParaStyle`). @@ -15,8 +16,9 @@ use serde_json::{Map, Value, json}; use vectorcraft_doc::Document; use vectorcraft_engine::cmd::{fileio, textstyles}; -use super::character::{end_typing, text_editing, text_style}; +use super::character::{end_typing, shared, text_editing, text_style}; use super::memo::Memo; +use super::paragraph::shared_para; use super::{alt_held, pstate, selection_len, set_pstate}; use crate::VectorcraftApp; use crate::theme::Tokens; @@ -172,14 +174,25 @@ thread_local! { static LISTED: std::cell::Cell = const { std::cell::Cell::new(0) }; } -/// The selection's style name (Normal when none) and every one of its attributes, written as a -/// style holds them, if text is selected. -fn current(app: &VectorcraftApp, kind: Kind) -> Option<(Option, Map)> { - let (c, p) = text_style(app)?; - Some(match kind { - Kind::Char => (c.style_name.clone(), textstyles::char_attrs(&c)), - Kind::Para => (p.style_name.clone(), textstyles::para_attrs(&p)), - }) +/// The style name (`None`: the Normal style) that every character the panel acts on has (paragraph +/// styles: every paragraph), with the Selection tool in every frame of the selected stories; +/// `None` when they differ or no type is selected. +fn current(app: &VectorcraftApp, kind: Kind) -> Option> { + match kind { + Kind::Char => shared(app, |s| s.style_name.clone()), + Kind::Para => shared_para(app, |p| p.style_name.clone()), + } +} + +/// Whether any character (paragraph styles: paragraph) the panel acts on differs from the style +/// attributes `attrs` in one of them. +fn has_overrides(app: &VectorcraftApp, kind: Kind, attrs: &Map) -> bool { + let differs = |m: Map| attrs.iter().any(|(k, v)| m.get(k) != Some(v)); + let same = match kind { + Kind::Char => shared(app, |s| differs(textstyles::char_attrs(s))), + Kind::Para => shared_para(app, |p| differs(textstyles::para_attrs(p))), + }; + same != Some(false) } /// Parameters targeting the selection: the Type tool's selected characters (paragraph styles: the @@ -196,14 +209,19 @@ fn target(app: &VectorcraftApp, kind: Kind, mut p: Value) -> Value { } /// [`target`] for applying a style: after Select All in threaded text, all the text of every -/// frame of the story. +/// frame of the story; with the Selection tool, every frame of the selected stories +/// ([`super::character::selected_stories`]). fn apply_target(app: &VectorcraftApp, kind: Kind, mut p: Value) -> Value { + if text_editing(app).is_none() { + super::character::target_selected_stories(app, &mut p); + return p; + } match super::character::text_story(app) { - Some(story) if text_editing(app).is_some() => { + Some(story) => { p["ids"] = json!(story.iter().map(|f| f.0).collect::>()); p } - _ => target(app, kind, p), + None => target(app, kind, p), } } @@ -242,9 +260,8 @@ fn open_style_options(app: &mut VectorcraftApp, kind: Kind, name: &str) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { let t = Tokens::get(ui.ctx()); let all = styles(app, kind); - let cur = current(app, kind); let normal = all.first().map(|s| s.0.clone()).unwrap_or_default(); - let cur_name = cur.as_ref().map(|(n, _)| n.clone().unwrap_or_else(|| normal.clone())); + let cur_name = current(app, kind).map(|n| n.unwrap_or_else(|| normal.clone())); let mut clicked: Option<(String, egui::Modifiers)> = None; // A double-clicked style's row, whose options open after the list is laid out. let mut options: Option = None; @@ -270,8 +287,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { widgets::list_box(ui, |ui| { for (name, attrs, _) in &all { let is_cur = cur_name.as_deref() == Some(name.as_str()); - // Overrides: an attribute the style sets that the selection doesn't match. - let overridden = is_cur && cur.as_ref().is_some_and(|(_, m)| attrs.iter().any(|(k, v)| m.get(k) != Some(v))); + // Overrides: an attribute the style sets that some of the text doesn't match. + let overridden = is_cur && has_overrides(app, kind, attrs); let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click_and_drag()); if resp.drag_started_by(egui::PointerButton::Primary) { egui::DragAndDrop::set_payload(ui.ctx(), RowDrag { kind, name: name.clone() }); @@ -310,7 +327,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { if scroll_to.is_some() { set_pstate::>(ui.ctx(), kind.scroll_key(), None); } - let has_text = cur.is_some(); + let has_text = text_style(app).is_some(); if let Some((name, m)) = clicked { sel = super::swatches::click_selection(ui, kind.anchor_key(), sel, &listed, &name, m); // Shift and Cmd/Ctrl clicks only highlight, as in the Graphic Styles panel. The second @@ -381,7 +398,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { let normal = if kind == Kind::Char { "[Normal Character Style]" } else { "[Normal Paragraph Style]" }; let user = sel.as_ref().is_some_and(|s| s != normal); let doomed: Vec<&String> = many.iter().filter(|s| *s != normal).collect(); - let has_text = current(app, kind).is_some() || selection_len(app) > 0; + let has_text = text_style(app).is_some() || selection_len(app) > 0; let p = kind.prefix(); if menu_item(ui, &crate::i18n::fmt(tl!("New {kind} Style…"), &[("kind", kind.what_label())]), true, false) { // A later typing preview must not restore the document from before this capture. diff --git a/crates/ui-egui/src/panels/text_styles/tests.rs b/crates/ui-egui/src/panels/text_styles/tests.rs index a83103aec8..e550f82ab4 100644 --- a/crates/ui-egui/src/panels/text_styles/tests.rs +++ b/crates/ui-egui/src/panels/text_styles/tests.rs @@ -29,8 +29,8 @@ fn panels_draw_and_apply_headless() { out.textures_delta.clear(); } app.run("charStyle.apply", target(&app, Kind::Char, json!({"name": "Big"}))).unwrap(); - let (name, attrs) = current(&app, Kind::Char).unwrap(); - assert_eq!((name.as_deref(), attrs["size"].as_f64()), (Some("Big"), Some(30.0))); + let (style, _) = text_style(&app).unwrap(); + assert_eq!((current(&app, Kind::Char), style.size), (Some(Some("Big".to_string())), 30.0)); } #[test] @@ -818,6 +818,74 @@ fn the_styles_are_listed_once_per_document_version() { } } +/// The style names of `id`'s runs (`Kind::Para`: of its paragraphs). +fn frame_style_names(app: &VectorcraftApp, kind: Kind, id: u64) -> Vec> { + match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) if kind == Kind::Char => t.runs.iter().map(|r| r.style.style_name.clone()).collect(), + NodeKind::Text(t) => (0..t.paragraph_count()).map(|i| t.para_at(i).style_name.clone()).collect(), + _ => panic!("not text"), + } +} + +/// With the Selection tool a selected frame of threaded text stands for its whole story, in both +/// panels: no row shows as the current style while a locked or hidden frame has another style, a +/// row click applies the style to every frame in one undo step and leaves other type alone, the +/// row shows "+" while a hidden frame overrides the style, and an Alt-click clears that override. +#[test] +fn selection_tool_styles_reach_every_frame_of_the_story() { + use egui::Modifiers; + for kind in [Kind::Char, Kind::Para] { + let what = kind.what(); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 900, "height": 300})).unwrap(); + let story = "The quick brown fox jumps over the lazy dog.\n".repeat(12); + let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}}); + let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap(); + let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap(); + let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap(); + let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [b]})).unwrap(); + app.run("object.lock", json!({})).unwrap(); + app.run("select.set", json!({"ids": [c]})).unwrap(); + app.run("object.hide", json!({})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + app.run(&format!("{}.new", kind.prefix()), json!({"name": "Plain"})).unwrap(); + app.run(&format!("{}.new", kind.prefix()), json!({"name": "Second", "attrs": {}})).unwrap(); + app.run(&format!("{}.apply", kind.prefix()), json!({"name": "Second", "ids": [c]})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + for id in [a, b, c] { + assert!(!frame_style_names(&app, kind, id).is_empty(), "frame {id} holds text"); + } + assert_eq!(current(&app, kind), None, "{what} Styles: the hidden frame has another style"); + + let ctx = egui::Context::default(); + let rows = |app: &mut VectorcraftApp, ui: &mut egui::Ui| { + ui.set_width(280.0); + show(app, ui, kind); + }; + let undo = app.session.doc().unwrap().history.undo.len(); + click_text_held(&ctx, &mut app, "Plain", Modifiers::NONE, rows); + assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "{what} Styles: one undo step"); + for id in [a, b, c] { + assert!(frame_style_names(&app, kind, id).iter().all(|n| n.as_deref() == Some("Plain")), "{what} Styles: frame {id}"); + } + assert!(frame_style_names(&app, kind, other).iter().all(Option::is_none), "{what} Styles: other type keeps its style"); + assert_eq!(current(&app, kind), Some(Some("Plain".to_string()))); + + app.run("text.setFormat", json!({"ids": [c], "kerning": 40, "composer": "singleLine"})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + let shown = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui, kind)); + assert!(shown.lines().any(|line| line == "Plain+"), "{what} Styles: the hidden frame's override shows: {shown}"); + // A new context, so that this click does not count as the second click of a double-click. + click_text_held(&egui::Context::default(), &mut app, "Plain+", Modifiers::ALT, rows); + let shown = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui, kind)); + assert!(shown.lines().any(|line| line == "Plain"), "{what} Styles: Alt-click clears the override: {shown}"); + app.run("edit.undo", json!({})).unwrap(); + let shown = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui, kind)); + assert!(shown.lines().any(|line| line == "Plain+"), "{what} Styles: undo brings the override back: {shown}"); + } +} + /// A row dragged from the highlighted styles carries all of them: dropped on Create New Style it /// duplicates each in one undo step and highlights the copies, and dropped on Delete it deletes each /// but the Normal style in one undo step and clears the highlight. A row from outside the highlight @@ -1201,7 +1269,7 @@ fn alt_clicking_new_paragraph_style_asks_for_its_name() { assert!(app.session.doc().unwrap().doc.para_styles.is_empty(), "no style is added before OK"); set_name(&mut app, "Body Copy"); crate::dialogs::confirm(&mut app).unwrap(); - let (_, attrs) = current(&app, Kind::Para).unwrap(); + let attrs = textstyles::para_attrs(&text_style(&app).unwrap().1); let doc = &app.session.doc().unwrap().doc; assert_eq!(doc.para_styles.iter().map(|s| (s.name.as_str(), &s.attrs)).collect::>(), [("Body Copy", &attrs)]); diff --git a/docs/mcp.md b/docs/mcp.md index 84c75a81d0..c1a2a94a89 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -8946,7 +8946,9 @@ send an Alignment or Level change for paragraphs that stay in no list. With no t with an unknown `tab`, `ui.bulletsAndNumbering` is an error. Type › Bullets and Numbering › Convert to Text runs `type.convertListToText` on the selected type's paragraphs, or on -those the Type tool's selection touches, and that runs `text.convertListToText {ids?, start?, end?}`. The marker of +those the Type tool's selection touches, and that runs `text.convertListToText {ids?, start?, end?}`. With the +Selection tool, a selected frame of threaded text names every frame of its story in `ids`, locked and hidden frames +included, as Apply Bullets and Apply Numbers do. The marker of each list paragraph becomes characters at the start of the paragraph: the bullet, or the number and its period as the paragraph showed it, then a tab. They are set in the marker's font, size and color: the style of the paragraph's first character without the attributes the marker leaves out. The paragraph leaves its list. A paragraph at a list level diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6b2a44966b..8296665950 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -997,6 +997,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - With the whole story of threaded text selected by Select All, the Character, Paragraph, OpenType, Tabs and Text Styles panels, the Justification dialog and the font menu change the frames that are in the edited frame's thread at the time of the change, locked and hidden frames included. A frame that has left the thread is not changed, and a frame added to the thread is (VectorCraft's own behavior; `panels::character::tests::story_changes_follow_the_edited_frames_thread_after_the_first_saved_frame_leaves`, `story_changes_leave_out_a_frame_that_left_and_take_in_a_frame_that_joined`, `story_changes_stay_in_the_edited_frame_once_its_thread_is_removed`, `story_changes_take_in_locked_and_hidden_frames`, `panels::tabs::tests::indent_markers_after_select_all_keep_each_story_paragraphs_indents`, `dialogs::justification::tests::justification_dialog_follows_the_story_thread_at_ok`). - With the whole story of threaded text selected by Select All, the Character, Paragraph, OpenType, Tabs and Text Styles panels, the Justification dialog and the font menu change the frames that are in the edited frame's thread at the time of the change, locked and hidden frames included. A frame that has left the thread is not changed, and a frame added to the thread is (VectorCraft's own behavior; `panels::character::tests::story_changes_follow_the_edited_frames_thread_after_the_first_saved_frame_leaves`, `story_changes_leave_out_a_frame_that_left_and_take_in_a_frame_that_joined`, `story_changes_stay_in_the_edited_frame_once_its_thread_is_removed`, `story_changes_take_in_locked_and_hidden_frames`). - With the Selection tool, a Character or Paragraph panel change to a selected frame of threaded text changes every frame of its story, locked and hidden frames included, as one undo step. The anti-aliasing method, Variable Font axes, the Tabs panel's tab stops and both panels' Reset Panel change the whole story too, and the panels show a value only when every frame of the story has it. A story selected through several of its frames or through a group is changed once, and other type keeps its formatting (VectorCraft's own behavior; `panels::character::tests::selection_tool_changes_reach_every_frame_of_the_story`, `selection_tool_fields_read_every_frame_of_the_story`, `selected_stories_list_each_frame_once`, `panels::paragraph::tests::selection_tool_paragraph_changes_reach_every_frame_of_the_story`, `panels::tabs::tests::selection_tool_tab_stops_reach_every_frame_of_the_story`). + - With the Selection tool, the Justification dialog, the OpenType panel, a Character or Paragraph Styles row and Type › Bullets and Numbering › Convert to Text also change every frame of a selected story, locked and hidden frames included, as one undo step. The Justification dialog acts on the story's frames as they were when it opened. When the frames differ, the OpenType panel's Figure menu shows Mixed, its checkboxes show a dash (a click turns the feature on everywhere), and the style panels highlight no row; a row shows "+" when any frame overrides its style (VectorCraft's own behavior; `dialogs::justification::tests::selection_tool_justification_reaches_every_frame_of_the_story`, `panels::opentype::tests::selection_tool_features_reach_every_frame_of_the_story`, `panels::text_styles::tests::selection_tool_styles_reach_every_frame_of_the_story`, `panels::paragraph::tests::selection_tool_convert_to_text_takes_every_frame_of_the_story`). - Switching from a Type tool to the Eyedropper keeps a whole story selected by Select All: a pick applies to the text of every frame as one undo step, and switching back restores the story selection (VectorCraft's own behavior; `textedit::switching_to_the_eyedropper_keeps_the_whole_story_selected`). - While the Type tool edits text, a character typed with AltGr is typed and runs no command. Firefox on Windows reports AltGr as Ctrl and Alt held, so in the web build AltGr+L (ł on a Polish layout) does not also open Layer Options for a new layer, and AltGr+Q (@ on a German layout) does not also run Reset Tracking and Kerning. A Ctrl+Alt chord that types no character runs its command. - On a layout without Latin letters, a one-key shortcut goes by the position of the key that typed the character, in the web build too, where the character arrives before its key press: on a Russian layout the P key (`з`) chooses the Pen, and two keys pressed in one frame each run their own shortcut. From 6b54622e721b015b5f355431ffd37c0dbce98523 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 19:50:50 -0800 Subject: [PATCH 10/44] Type: a tabbed line that steps back ends at the latest break that fits as drawn A glyph with a negative advance can draw a later break narrower than an earlier one, so bisecting the breaks past the 256 measured one by one could end a tabbed right-to-left line at an earlier break than the latest that fits. Such a line now measures up to 1,024 breaks, or hyphenation points, one by one before bisecting the rest. --- crates/text/src/layout/breaking.rs | 37 +++++++++++------- crates/text/src/layout/wrapping_tests.rs | 49 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/crates/text/src/layout/breaking.rs b/crates/text/src/layout/breaking.rs index 671ebee7a1..9e4cb10e36 100644 --- a/crates/text/src/layout/breaking.rs +++ b/crates/text/src/layout/breaking.rs @@ -256,6 +256,11 @@ pub(super) fn drawn_width(sg: &[SGlyph], drawn: &[usize], tabs: &[vectorcraft_do /// back before bisecting the earlier ones. const MAX_DRAWN_CHECKS: usize = 256; +/// Most breaks, or hyphenation points, [`break_line`] measures as drawn one by one when a glyph of +/// the line has a negative advance: a later break can then be drawn narrower than an earlier one, +/// and bisecting can miss the latest break that fits. +const MAX_DRAWN_CHECKS_STEPPING_BACK: usize = 1024; + /// Most copies of a leader drawn in one tab: a tiny type size and a far stop must not make /// millions of outlines. const MAX_LEADER_COPIES: f64 = 4096.0; @@ -357,8 +362,9 @@ pub(super) fn word_width(g: &[SGlyph], i: usize, line_end: &[f64], kinsoku: Kins /// from `i` ending before glyph `end` (with a hyphen when `hyphenated`) fits `width` as it is drawn: /// once the line holds a tab, whose width depends on what is drawn before it, the line reads on /// while the least it can be drawn at fits `width`, then ends at the latest legal break where it -/// fits as drawn (past [`MAX_DRAWN_CHECKS`] breaks back from the latest, the earlier ones without a -/// hyphen are bisected); hyphenation points are measured the same way. +/// fits as drawn (past [`MAX_DRAWN_CHECKS`] breaks back from the latest, or +/// [`MAX_DRAWN_CHECKS_STEPPING_BACK`] when a glyph of the line has a negative advance, the earlier +/// ones without a hyphen are bisected); hyphenation points are measured the same way. #[allow(clippy::too_many_arguments)] pub(super) fn break_line( text: &str, @@ -377,24 +383,26 @@ pub(super) fn break_line( fits: Option<&dyn Fn(usize, bool) -> bool>, ) -> (usize, bool) { // The line ending at `chosen`, once it holds a tab, when it fits as drawn; otherwise the latest - // of the legal `breaks` before it that fits as drawn, measuring at most MAX_DRAWN_CHECKS of - // them from the latest back and bisecting the earlier ones without a hyphen; the first of them - // when none of those measured fits. - let settle = |tabbed: bool, chosen: (usize, bool), breaks: &[(usize, bool)]| -> (usize, bool) { + // of the legal `breaks` before it that fits as drawn, measuring at most `checks` of them from + // the latest back and bisecting the earlier ones without a hyphen; the first of them when none + // of those measured fits. + let settle = |tabbed: bool, checks: usize, chosen: (usize, bool), breaks: &[(usize, bool)]| -> (usize, bool) { let Some(fits) = fits.filter(|_| tabbed) else { return chosen }; if fits(chosen.0, chosen.1) { return chosen; } let earlier = breaks.get(breaks.partition_point(|b| b.0 <= i)..breaks.partition_point(|b| b.0 < chosen.0)).unwrap_or_default(); - if let Some(&b) = earlier.iter().rev().take(MAX_DRAWN_CHECKS).find(|b| fits(b.0, b.1)) { + if let Some(&b) = earlier.iter().rev().take(checks).find(|b| fits(b.0, b.1)) { return b; } // Without a hyphen, a line that ends later is drawn no narrower, so those breaks bisect. - let rest = earlier.get(..earlier.len().saturating_sub(MAX_DRAWN_CHECKS)).unwrap_or_default(); + let rest = earlier.get(..earlier.len().saturating_sub(checks)).unwrap_or_default(); let plain: Vec<&(usize, bool)> = rest.iter().filter(|b| !b.1).collect(); let fitting = plain.partition_point(|b| fits(b.0, b.1)); fitting.checked_sub(1).and_then(|k| plain.get(k).copied()).or(earlier.first()).copied().unwrap_or(chosen) }; + // How many breaks a line measures as drawn one by one. + let checks = |back: f64| if back > 0.0 { MAX_DRAWN_CHECKS_STEPPING_BACK } else { MAX_DRAWN_CHECKS }; let mut breaks: Vec<(usize, bool)> = Vec::new(); let mut tabbed = false; if !width.is_finite() { @@ -462,7 +470,7 @@ pub(super) fn break_line( let cluster_ink = if is_tab { 0.0 } else { cluster_width }; let cluster_back: f64 = if is_tab { 0.0 } else { g[j..cluster_end].iter().map(|glyph| (-glyph.adv).max(0.0)).sum() }; if gl.ch == '\u{2028}' { - return settle(tabbed, (cluster_end, false), &breaks); + return settle(tabbed, checks(back), (cluster_end, false), &breaks); } // Measured as the line is set when it ends with this cluster: only the cluster's last glyph // is narrowed, and only where the line may end. @@ -509,7 +517,7 @@ pub(super) fn break_line( j = cluster_end; } if j >= g.len() { - return settle(tabbed, (g.len(), false), &breaks); + return settle(tabbed, checks(back), (g.len(), false), &breaks); } // Hyphenate the word that overflows. A list marker starting the line is not part of a word. let word_start = last_break.map_or(first, |b| b.0); @@ -534,18 +542,19 @@ pub(super) fn break_line( .filter(|&k| k > 0 && kinsoku_allows(g, k - 1, kinsoku) && !splits_no_break(g, k)) .collect(); // The points the line may end at, the latest first. Measured as drawn, at most - // MAX_DRAWN_CHECKS of them are measured one by one, and the earlier ones are bisected: + // `checks(back)` of them are measured one by one, and the earlier ones are bisected: // each ends with a hyphen, so a later one is drawn no narrower. let points: Vec = pts.iter().rev().copied().filter(|&k| k <= j && k > i).collect(); let drawn = fits.filter(|_| tabbed); - for &k in points.iter().take(if drawn.is_some() { MAX_DRAWN_CHECKS } else { usize::MAX }) { + let one_by_one = checks(back); + for &k in points.iter().take(if drawn.is_some() { one_by_one } else { usize::MAX }) { let w = x_ws + g[word_start..k].iter().map(|g| g.adv).sum::() + hyphen_glyph(&g[k - 1]).adv; if drawn.map_or(w <= width + EPS, |fits| fits(k, true)) { return (k, true); } } if let Some(fits) = drawn { - let earlier: Vec = points.iter().skip(MAX_DRAWN_CHECKS).rev().copied().collect(); + let earlier: Vec = points.iter().skip(one_by_one).rev().copied().collect(); let fitting = earlier.partition_point(|&k| fits(k, true)); if let Some(&k) = fitting.checked_sub(1).and_then(|n| earlier.get(n)) { return (k, true); @@ -555,7 +564,7 @@ pub(super) fn break_line( // Break candidates and greedy overflow are cluster boundaries; hyphenation also returns only // cluster starts, so the resulting end always keeps each source cluster together. match last_break { - Some(b) if b.0 > i => settle(tabbed, b, &breaks), + Some(b) if b.0 > i => settle(tabbed, checks(back), b, &breaks), _ => (j, false), } } diff --git a/crates/text/src/layout/wrapping_tests.rs b/crates/text/src/layout/wrapping_tests.rs index eae22bab8a..a8630aad5e 100644 --- a/crates/text/src/layout/wrapping_tests.rs +++ b/crates/text/src/layout/wrapping_tests.rs @@ -646,6 +646,55 @@ fn a_negative_advance_after_a_tab_keeps_a_fitting_line_whole() { assert_eq!(ended, (5, false)); } +/// A long tabbed line with a glyph that steps back ends at the latest break that fits as drawn, even +/// where a later break than one that does not fit fits again. +#[test] +fn a_long_tabbed_line_that_steps_back_ends_at_the_latest_break_that_fits_as_drawn() { + // a (0), tab (1), then b and a space for each u: breaks before 4 + 2u. + let text = format!("a\t{}", "b ".repeat(400)); + let specs: Vec<_> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), if byte == 2 { -0.5 } else { 1.0 }, ch)).collect(); + let glyphs = synthetic_glyphs(&specs); + let drawn_fits = |end: usize, _hyphenated: bool| end <= 10 || end == 200; + let fits: Option<&dyn Fn(usize, bool) -> bool> = Some(&drawn_fits); + let ended = + break_line(&text, &glyphs, 0, 1e6, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false, &[], fits); + assert_eq!(ended, (200, false)); +} + +/// A tabbed line with a glyph that steps back, ending in a word with more hyphenation points than +/// are measured one by one, ends at the latest point that fits as drawn. +#[test] +fn a_tabbed_line_that_steps_back_ends_at_the_latest_hyphenation_point_that_fits_as_drawn() { + let opts = HyphenationOptions { words_longer_than: 0, after_first: 1, before_last: 1, ..Default::default() }; + // a (0), tab (1), then a word of 400 syllables of two glyphs from glyph 2: points before 4, 6, ... + let text = format!("a\t{}", "कि".repeat(400)); + let specs: Vec<_> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), if byte == 2 { -1.0 } else { 1.0 }, ch)).collect(); + let mut glyphs = synthetic_glyphs(&specs); + for glyph in &mut glyphs { + glyph.hyphen_language = crate::hyphen::Language::from_tag(Some("hi")); + glyph.sx = 0.001; + } + let drawn_fits = |end: usize, _hyphenated: bool| end <= 20 || end == 50; + let fits: Option<&dyn Fn(usize, bool) -> bool> = Some(&drawn_fits); + let ended = break_line( + &text, + &glyphs, + 0, + 600.0, + &[], + 0.0, + Some((&opts, 0.0)), + Burasagari::None, + Kinsoku::None, + KinsokuShoriType::PushOutOnly, + false, + false, + &[], + fits, + ); + assert_eq!(ended, (50, true)); +} + /// Mojikumi: the greedy breaker measures a line as it is set, with its last character narrowed /// (`line_end`): 」 set half width ends a measure of three and a half characters. Only the last /// glyph of the last cluster counts, and only where the line may end. From d47cbe5f775d3ad0964d3b6134485c694fccf2d1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 20:06:12 -0800 Subject: [PATCH 11/44] 3D: colored-light faces keep unplaced gradients fitted to the face The color cover of a composed face grew past the underlay's seam, and a source gradient without a placement fitted itself to that larger rectangle. The face now pins such a gradient to its bounds grown by 1 pt, as before the cover grew. docs/three-d.md states the measured limits of the 1:1 comparison. --- crates/effects/src/lighting.rs | 34 +++++++++++++++++++++++++++++++++- docs/three-d.md | 6 ++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/effects/src/lighting.rs b/crates/effects/src/lighting.rs index 378bfa5621..257576822a 100644 --- a/crates/effects/src/lighting.rs +++ b/crates/effects/src/lighting.rs @@ -326,7 +326,13 @@ fn composed_art(id: vectorcraft_doc::NodeId, path: &vectorcraft_geom::PathData, use vectorcraft_doc::color::BlendMode; use vectorcraft_doc::{Appearance, Node, NodeKind, OpacityMask}; use vectorcraft_geom::{Rect, shapes}; - let (base, mask) = opaque_paint(paint); + // An unplaced gradient (and a freeform gradient without points) stays fitted to the face's + // bounds grown by 1 pt, not to the larger cover below. + let mut paint = paint.clone(); + if let Paint::Gradient(g) = &mut paint { + g.pin(path.bounds().unwrap_or(Rect::ZERO).inflate(1.0, 1.0)); + } + let (base, mask) = opaque_paint(&paint); // Clip once to the union of this single-contour face and its round seam stroke. let clip = Node::path(id, vectorcraft_geom::PathData::from_bezpath(&footprint(&path.to_bezpath(), seam)), Appearance::default()); // Opaque rectangles cover the complete seam stroke, without separately antialiasing @@ -469,4 +475,30 @@ mod tests { assert_eq!(point.color.to_rgb(), [1.0, 0.25, 0.25]); assert_eq!(shaded.swatch, None, "a changed freeform point detaches the gradient swatch"); } + + /// Under colored light, a face's unplaced source gradient is fitted to the face's bounds grown by + /// 1 pt, whatever the size of the cover rectangle that draws it; a placed one keeps its placement. + #[test] + fn composed_faces_fit_unplaced_gradients_to_the_face_not_the_cover() { + use vectorcraft_doc::NodeKind; + use vectorcraft_geom::{Rect, shapes}; + use vectorcraft_three_d::{RgbGradient, ShadeGradient}; + let face = shapes::rectangle(Rect::new(10.0, 20.0, 50.0, 40.0)); + let ramp = |b| ShadeGradient { start: Point::new(10.0, 20.0), end: Point::new(50.0, 20.0), brightness: [b, 1.0] }; + let light = RgbGradient { channels: [ramp(0.2), ramp(0.5), ramp(0.8)] }; + let stops = vec![GradientStop::new(0.0, Color::WHITE), GradientStop::new(1.0, Color::BLACK)]; + let unplaced = GradientPaint { gradient: Gradient::new(GradientKind::Linear, stops), geom: None, angle: 0.0, swatch: None, freeform: None }; + let placed = GradientGeom { start: Point::new(0.0, 0.0), end: Point::new(80.0, 0.0), aspect: 1.0, focal: None }; + let fitted = unplaced.resolve(Rect::new(9.0, 19.0, 51.0, 41.0)); + for (geom, expected) in [(None, fitted), (Some(placed), placed)] { + let paint = Paint::Gradient(Box::new(GradientPaint { geom, ..unplaced.clone() })); + for seam in [0.05, 0.75] { + let art = facet_art(vectorcraft_doc::NodeId(1), &face, &paint, light, seam); + let NodeKind::Group { children, clip: true } = &art.kind else { panic!("a clipped composite") }; + let base = &children[1].children().unwrap()[0]; + let Paint::Gradient(g) = base.appearance.items[0].paint() else { panic!("the source gradient") }; + assert_eq!(g.geom, Some(expected), "seam {seam}"); + } + } + } } diff --git a/docs/three-d.md b/docs/three-d.md index 4ba4683142..c0c40d4f24 100644 --- a/docs/three-d.md +++ b/docs/three-d.md @@ -123,8 +123,10 @@ How a facet carries its seam depends on the lights and the paint: of themselves whose seams are 2 pt wide, reaching 1 pt past each edge, and one clip to the union of the facets and their seams removes what the underlay paints past the surface. Where two facets' antialiased edges meet, the underlay fills the pixels they leave partly uncovered with the neighbors' own light, so no black channel backdrop - or background shows there. Drawn at 1:1, inside the surface each color channel is within 3 levels of the same art - drawn at 3x and averaged down, and every pixel is opaque (VectorCraft's own measure). + or background shows there. Compared with the same art drawn at 3x and averaged down, away from the surface's outline + and from places where the light changes sharply, a pixel drawn at 1:1 is opaque where the 3x art is opaque, and + otherwise its alpha is within 3 levels. Inflate seen straight on with an opaque fill also has each color channel + within 3 levels (VectorCraft's own measure). - Extrude & Bevel (Classic), Rotate (Classic) and Revolve (Classic): a path and a seam for every face; gradient and pattern faces seam only the base paint, and the shading and highlight layers cover the face alone. The Wireframe surface draws edges only, with no seam. From 90b053f480524d3224f8c51e32827b9a75cee316 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 20:16:18 -0800 Subject: [PATCH 12/44] i18n: Italian translates the messages no catalog had a row for The untranslated report listed 167 status and error messages with no row in any complete-message catalog, and 13 more that other catalogs translate but Italian did not, so with the interface in Italian these messages showed in English. - it.tsv gains 114 `@msg` rows for the listed messages: image export and encoding errors, color profiles, color books and swatch exchange files, binary DXF and DWG import, 3D and Revolve limits, and script, zip and file errors. - it.tsv gains 12 `@msg` rows for the Banner Hems and Grommets errors, three parameter checks and the failed recovery save. - The rows sit in two commented blocks after the existing message rows. - The 53 listed messages that open #7211, #7378 and #7405 translate (composite fonts, SVG filters, read roots and graph designs), and the justification order error that open #7116 translates, are left out, so no row is added twice. - Placeholders, parameter names, command ids and literal values stay as the English writes them. The translations use the catalog's existing terms. - docs/localization-parity.md and docs/target-app-parity.md give the Italian counts: 3,560 `@msg` and 48 `@msgplural` rows, 54 messages still listed. --- crates/ui-egui/src/i18n/it.tsv | 128 +++++++++++++++++++++++++++++++++ docs/localization-parity.md | 2 +- docs/target-app-parity.md | 1 + 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 0ac2816b07..ee47510892 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -8920,6 +8920,134 @@ breakText Glyphs Glifi @msg {what} can't be intertwined {what} non può essere intrecciato @msg {what} doesn't fit the paper with its bleed and marks {what} non entra nella carta con l'abbondanza e i segni @msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} verrebbe stampato su più di {MAX_TILES} riquadri: riducilo +# Messages no catalog translated yet: image export and color profiles, color books and swatch exchange files, 3D and Revolve, and script and file errors. +@msg 3D effects require finite coordinates within 100 million points Gli effetti 3D richiedono coordinate finite entro 100 milioni di punti +@msg 3D effects require one to ten lights Gli effetti 3D richiedono da una a dieci luci +@msg BMP depth {_1}: 1, 4, 8, 16, 24 or 32 bits per pixel Profondità BMP {_1}: 1, 4, 8, 16, 24 o 32 bit per pixel +@msg BMP encoding failed: {width} × {height} pixels doesn't fit the format or the pixel buffer Codifica BMP non riuscita: {width} × {height} pixel non sono compatibili con il formato o con il buffer dei pixel +@msg GIF encoding failed: the palette image is malformed Codifica GIF non riuscita: l'immagine con palette non è valida +@msg GIF encoding failed: {e} Codifica GIF non riuscita: {e} +@msg JPEG colour profile: {e} Profilo colore JPEG: {e} +@msg JPEG encoding failed: pixel buffer doesn't match the image size Codifica JPEG non riuscita: il buffer dei pixel non corrisponde alle dimensioni dell'immagine +@msg JPEG encoding failed: {e} Codifica JPEG non riuscita: {e} +@msg PNG encoding failed: pixel buffer doesn't match the image size Codifica PNG non riuscita: il buffer dei pixel non corrisponde alle dimensioni dell'immagine +@msg PNG encoding failed: the palette image is malformed Codifica PNG non riuscita: l'immagine con palette non è valida +@msg PNG encoding failed: {e} Codifica PNG non riuscita: {e} +@msg PNG encoding failed: {width} × {height} pixels is not a valid image size Codifica PNG non riuscita: {width} × {height} pixel non è una dimensione valida per un'immagine +@msg PSD colour profile: {e} Profilo colore PSD: {e} +@msg PSD encoding failed: the pixel buffer doesn't match the image size Codifica PSD non riuscita: il buffer dei pixel non corrisponde alle dimensioni dell'immagine +@msg Revolve profile contains invalid coordinates Rivoluzione: il profilo contiene coordinate non valide +@msg Revolve profile is too complex Rivoluzione: il profilo è troppo complesso +@msg Revolve projection is not finite Rivoluzione: la proiezione non è finita +@msg Revolve requires finite profile coordinates within 100 million points Rivoluzione richiede coordinate del profilo finite entro 100 milioni di punti +@msg Revolve surface is too complex; reduce Segments or simplify the profile Rivoluzione: la superficie è troppo complessa; riduci Segmenti o semplifica il profilo +@msg Revolve surface is too complex; simplify the profile Rivoluzione: la superficie è troppo complessa; semplifica il profilo +@msg Revolve visibility cannot split the projected boundary Rivoluzione: il calcolo della visibilità non può dividere il contorno proiettato +@msg Revolve visibility creates too many fragments Rivoluzione: il calcolo della visibilità crea troppi frammenti +@msg Revolve visibility creates too many vector anchors Rivoluzione: il calcolo della visibilità crea troppi punti di ancoraggio vettoriali +@msg Revolve visibility exceeds its geometry work limit Rivoluzione: il calcolo della visibilità supera il suo limite di lavoro geometrico +@msg Revolve visibility exceeds its spatial index limit Rivoluzione: il calcolo della visibilità supera il suo limite di indice spaziale +@msg Revolve visibility has too many boundary crossings Rivoluzione: il calcolo della visibilità ha troppi attraversamenti del contorno +@msg Revolve visibility has too many input anchors Rivoluzione: il calcolo della visibilità ha troppi punti di ancoraggio in ingresso +@msg Revolve visibility requires convex cap sectors Rivoluzione: il calcolo della visibilità richiede settori convessi delle basi +@msg Revolve visibility requires finite projected faces Rivoluzione: il calcolo della visibilità richiede facce proiettate finite +@msg SVG options: {e} Opzioni SVG: {e} +@msg Save for Web settings: {e} Impostazioni di Salva per il web: {e} +@msg TIFF colour profile: {e} Profilo colore TIFF: {e} +@msg TIFF encoding failed: the pixel buffer doesn't match the image size Codifica TIFF non riuscita: il buffer dei pixel non corrisponde alle dimensioni dell'immagine +@msg TIFF encoding failed: {e} Codifica TIFF non riuscita: {e} +@msg Targa depth {_1}: 16, 24 or 32 bits per pixel Profondità Targa {_1}: 16, 24 o 32 bit per pixel +@msg Targa encoding failed: the pixel buffer doesn't match the image size Codifica Targa non riuscita: il buffer dei pixel non corrisponde alle dimensioni dell'immagine +@msg The 3D projection is not finite La proiezione 3D non è finita +@msg The 3D shadow is too complex; turn Shadows off or simplify the art L'ombra 3D è troppo complessa; disattiva Ombre o semplifica la grafica +@msg The shape contains invalid coordinates La forma contiene coordinate non valide +@msg Too many Revolve faces for visibility evaluation Troppe facce di Rivoluzione per il calcolo della visibilità +@msg VectorCraft reads at most {ASE_MAX_ENTRIES} colors and groups from a swatch exchange file, and this library has {entries} VectorCraft legge al massimo {ASE_MAX_ENTRIES} colori e gruppi da un file di scambio campioni, e questa libreria ne ha {entries} +@msg WebP encoding failed: {e} Codifica WebP non riuscita: {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 o largeList +@msg a line can't run from a point to itself una linea non può andare da un punto a sé stesso +@msg a line needs at least two points una linea richiede almeno due punti +@msg a number cannot be read impossibile leggere un numero +@msg a swatch exchange block is too long un blocco del file di scambio campioni è troppo lungo +@msg a swatch exchange name is too long un nome nel file di scambio campioni è troppo lungo +@msg a {sw} × {sh} image can't be resized to {width} × {height} pixels un'immagine di {sw} × {sh} non può essere ridimensionata a {width} × {height} pixel +@msg a {width} × {height} image is too large un'immagine di {width} × {height} è troppo grande +@msg a {width} × {height} image: {e} un'immagine di {width} × {height}: {e} +@msg an image could not be written: {e} impossibile scrivere un'immagine: {e} +@msg an image that couldn't be read was left out un'immagine che non è stato possibile leggere è stata omessa +@msg can't open “{name}”: {e} impossibile aprire “{name}”: {e} +@msg cannot align zip entries to {align} bytes impossibile allineare le voci zip a {align} byte +@msg cannot open the automation {kind} root `{_1}`: {e} impossibile aprire la cartella radice {kind} dell'automazione `{_1}`: {e} +@msg cannot read profile: {0} impossibile leggere il profilo: {0} +@msg color book version {version} isn't supported (only 1) la versione {version} del libro colori non è supportata (solo 1) +@msg color books in color space {other} aren't supported (only RGB, CMYK and Lab) i libri colori nello spazio colore {other} non sono supportati (solo RGB, CMYK e Lab) +@msg file name too long to zip: {_1} nome file troppo lungo per un archivio zip: {_1} +@msg file name too long to zip: {name} nome file troppo lungo per un archivio zip: {name} +@msg freeform line {i}: {e} linea libera {i}: {e} +@msg freeform point {i}: {e} punto libero {i}: {e} +@msg give `{key}` or `{other}`, not both indica `{key}` o `{other}`, non entrambi +@msg grayscale profiles can't be working spaces yet i profili in scala di grigi non possono ancora essere spazi di lavoro +@msg input contains NaN or infinite coordinates l'input contiene coordinate NaN o infinite +@msg input path could not be closed impossibile chiudere il tracciato di input +@msg invalid ICC profile: {0} profilo ICC non valido: {0} +@msg invalid scale {scale} scala non valida {scale} +@msg its localized names take more than {_1} KB i suoi nomi localizzati occupano più di {_1} KB +@msg its objects are nested too deep ({e}) i suoi oggetti sono annidati troppo in profondità ({e}) +@msg its tag table lists the `{_1}` tag more than once la sua tabella dei tag elenca il tag `{_1}` più di una volta +@msg not a graphic style library: {e} non è una libreria di stili grafici: {e} +@msg not a swatch library (.vcswatches or .gpl) non è una libreria campioni (.vcswatches o .gpl) +@msg not a swatch library (format `{_1}`) non è una libreria campioni (formato `{_1}`) +@msg not a swatch library: {e} non è una libreria campioni: {e} +@msg not an action set file ({e}) non è un file di set di azioni ({e}) +@msg profile colour space {other} is not RGB, CMYK or Gray lo spazio colore {other} del profilo non è RGB, CMYK o scala di grigi +@msg regular expression too large espressione regolare troppo grande +@msg script file is not UTF-8 text: {value} il file di script non è testo UTF-8: {value} +@msg stop {i}: {e} cursore colore {i}: {e} +@msg swatch exchange version {major}.{minor} isn't supported (only 1.x) la versione {major}.{minor} del file di scambio campioni non è supportata (solo 1.x) +@msg the color book holds more than {ASE_MAX_ENTRIES} colors il libro colori contiene più di {ASE_MAX_ENTRIES} colori +@msg the color book is cut short il libro colori è troncato +@msg the color book's names take more than {_1} MB i nomi del libro colori occupano più di {_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 l'immagine misurerebbe {w} × {h} pixel; le esportazioni raster sono limitate a {MAX_RASTER_SIDE} pixel per lato e {_1} megapixel: riduci la scala o la risoluzione +@msg the optimised image can't be read back: {e} impossibile rileggere l'immagine ottimizzata: {e} +@msg the pixel buffer doesn't match the image size il buffer dei pixel non corrisponde alle dimensioni dell'immagine +@msg the preview: {e} l'anteprima: {e} +@msg the request may not read the New Document profiles folder la richiesta non può leggere la cartella dei profili di nuovo documento +@msg the shapes are too degenerate to combine le forme sono troppo degeneri per essere combinate +@msg the shapes cross themselves and each other too often to combine le forme si intersecano con sé stesse e tra loro troppe volte per essere combinate +@msg the swatch exchange file holds more than {ASE_MAX_ENTRIES} colors and groups il file di scambio campioni contiene più di {ASE_MAX_ENTRIES} colori e gruppi +@msg the swatch exchange file is cut short il file di scambio campioni è troncato +@msg the {_1} plate can't be read: {e} impossibile leggere la lastra {_1}: {e} +@msg this binary DXF file is damaged or cut short questo file DXF binario è danneggiato o troncato +@msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that questo è un disegno DWG, che non può essere letto: salvalo dall'app CAD come DXF e apri quel file +@msg too many classes troppe classi di caratteri +@msg too many groups troppi gruppi +@msg too many layers for PSD (at most 32767) troppi livelli per PSD (al massimo 32767) +@msg too many swatch exchange blocks troppi blocchi nel file di scambio campioni +@msg unknown colour profile: {0} profilo colore sconosciuto: {0} +@msg {_1} clashes with a built-in profile name {_1} coincide con il nome di un profilo incorporato +@msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} pixel è troppo grande per GIF (al massimo 65535 pixel per lato): riduci la risoluzione +@msg {_1}: no usable transform to/from sRGB {_1}: nessuna trasformazione utilizzabile da/verso sRGB +@msg {name} is not a CMYK profile {name} non è un profilo CMYK +@msg {name} is not an RGB profile {name} non è un profilo RGB +@msg {path}: could not be read {path}: lettura non riuscita +@msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} pixel è troppo grande per JPEG (al massimo 65535 pixel per lato): riduci la risoluzione +@msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} pixel è troppo grande per Targa (al massimo 65535 pixel per lato): riduci la risoluzione +@msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} pixel è troppo grande per PSD (al massimo {MAX_SIDE} pixel per lato): riduci la risoluzione +# Messages other catalogs translate and Italian did not: Banner Hems and Grommets, parameter checks and recovery data. +@msg `{key}` must be from 0.01 to 1000 pt `{key}` deve essere compreso tra 0,01 e 1000 pt +@msg select the banner, or mark every artboard seleziona lo striscione, oppure contrassegna ogni tavola da disegno +@msg the banner has no width or height lo striscione non ha larghezza o altezza +@msg the grommet inset is too large for the banner la distanza degli occhielli dal bordo è troppo grande per lo striscione +@msg the hem is too deep for the banner l'orlo è troppo profondo per lo striscione +@msg the spacing makes more than {MAX_GROMMETS} grommets la spaziatura produce più di {MAX_GROMMETS} occhielli +@msg there is no banner to mark non c'è nessuno striscione da contrassegnare +@msg turn on at least one side attiva almeno un lato +@msg unknown parameter `{within}{key}`; {why} parametro sconosciuto `{within}{key}`; {why} +@msg with no hem and no grommets there is nothing to mark senza orlo e senza occhielli non c'è nulla da contrassegnare +@msg {name} must be {lo}–{hi}{unit} (got {v}) {name} deve essere {lo}–{hi}{unit} (ricevuto {v}) +@msg {saving} failed: {e} {saving} non riuscito: {e} Banner Hems and Grommets… Orli e occhielli per striscioni… Banner Hems and Grommets Orli e occhielli per striscioni Hem Orlo diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 6cbad8f5e3..b8f7d8b097 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -50,7 +50,7 @@ plus review. Native-speaker review is human work and isn't in the hours. | Language | Code | Translated | Status | |---|---|---|---| | Traditional Chinese (Taiwan) | `zh-hant` | 4,892 rows, 790 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted (80% overall) | partial | -| Italian | `it` | 8,757 rows, 3,422 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 40 missing messages for it | full | +| Italian | `it` | 8,924 rows, 3,560 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 54 missing messages for it, which open #7116, #7211, #7378 and #7405 translate | full | | Dutch | `nl` | 7,299 rows, 1,609 of them `@msg` and 47 `@msgplural` (counted 2026-11-13 on `main` at `9a1a158a3`): in `COMPLETE_MESSAGES`, and the untranslated report lists the same missing messages for it as for German; one/other plurals | full | | Danish | `da` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one/other plurals | full | | Russian | `ru` | 4,009 / 4,020 | full | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 296bd84b2b..1de4d67e55 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -888,6 +888,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - 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. - Italian messages (2026-11-13): the Italian catalog (`it`) has 1,802 more status and error message rows, and the untranslated report's missing Italian messages go from 1,842 to 40; the translations are VectorCraft's own and await native review; scores and estimates unchanged. See [localization-parity.md](localization-parity.md). + - Italian messages no catalog had (2026-11-13): the Italian catalog has 126 more `@msg` rows, 3,560 in all: the image export, color profile, color book, swatch exchange file, DXF and DWG import, 3D and Revolve, script and zip errors that no other catalog translates yet, and the Banner Hems and Grommets, parameter range and recovery save messages that other catalogs translate. The untranslated report lists 54 Italian messages, which open #7116, #7211, #7378 and #7405 translate; scores and estimates unchanged. - Window › Help Bar shows or hides the tool-help strip at the bottom of the window apart from the status bar (`window.helpBar`). Each setting is kept with the UI preferences and in saved workspaces, and Presentation Mode hides both bars. Layouts saved before the setting existed show the Help Bar where they show the status bar (`help_bar_toggle_is_independent_and_saved_in_workspaces`, `help_bar_and_status_bar_render_independently`, `legacy_help_bar_visibility_follows_each_layouts_status_bar`, `a_help_bar_setting_the_file_lacks_is_saved`). - Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md). - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`). From cf7a72e21bae2c895672d5503cb2b4bf19c0a29d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 20:44:35 -0800 Subject: [PATCH 13/44] Type: the Single-line Composer measures a line's room across its tallest character In area type of a shape other than a rectangle, or with text wrap, the Single-line Composer placed each line at the metrics of its first character and broke it in the room there. When a later character on the line was taller, the baseline moved down by the difference without measuring the room again, so the line could reach into a narrower part of the frame or into a wrap object. - A line that turns out taller than it was placed is placed again at metrics that hold both and broken again in the room there. - When that leaves the taller character for the next line, the line is set at its own metrics where the room there is no narrower, and takes the words before that character that fit there. Otherwise it stays where it was placed. Tests: crates/text/src/tests/combos.rs (a stepped frame and text wrap; the two mixed-size tests now expect the Single-line Composer to break as the Every-line Composer does there). Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/text/src/layout/flow.rs | 72 +++++++++++++++++++++++++++++---- crates/text/src/tests/combos.rs | 41 +++++++++++++++++-- docs/mcp.md | 4 ++ docs/target-app-parity.md | 2 +- 4 files changed, 107 insertions(+), 12 deletions(-) diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index 1ee9a3fe50..2771c26954 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -24,6 +24,10 @@ fn pushes_in(para: &ParaStyle) -> bool { /// Paragraphs longer than this (glyphs) are always broken line by line. const MAX_COMPOSE_GLYPHS: usize = 200_000; +/// The widths tried, halving the difference each time, for the longest break of a line placed again +/// at its own metrics in more room than it was broken in ([`place_flow_line`]). +const REFIT_STEPS: usize = 24; + /// Whether every line of area type in `regions` is as wide however tall it is and wherever it is set: /// rectangular cells without text wrap. fn constant_width(regions: Option<&[Region]>) -> bool { @@ -415,25 +419,31 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC let word = word_width(sg, *i, line_end, para.kinsoku); // A composed line in a frame whose lines are not all as wide is placed at its own tallest // metrics, as the composer placed it (`shaped::Shaped`): its room is the room across its height. - let est = match composed.as_ref().and_then(|lines| lines.get(*para_line)) { + let composed_line = composed.as_ref().and_then(|lines| lines.get(*para_line)).is_some_and(|&(end, _)| end > *i); + let mut est = match composed.as_ref().and_then(|lines| lines.get(*para_line)) { Some(&(end, _)) if end > *i && !constant_width(pen.regions) => sg.get(*i..end).and_then(Metrics::max).unwrap_or(est), _ => est, }; + // A line broken here in such a frame is placed again when it turns out taller than it was + // placed: its room is the room across its own height. + let refit = !composed_line && pen.regions.is_some() && !constant_width(pen.regions); // Place, break, then settle the baseline on the line's real metrics (moving on to the // next row/column if it no longer fits). loop { + let before = refit.then(|| pen.clone()); let first_in_region = pen.prev.is_none(); - let Some((mut baseline, x0, x1, same_baseline)) = pen.place(est, ind_l, ind_r, word) else { + let Some((mut baseline, mut x0, mut x1, same_baseline)) = pen.place(est, ind_l, ind_r, word) else { cx.out.overflow = cx.text.len() > if *i < n { sg[*i].byte } else { pr.start }; return None; }; - let width = x1 - x0 - ind_l - ind_r; let hyphenate = (para.hyphenate && (*hyphen_limit == 0 || *hyphen_run < *hyphen_limit)).then_some((¶.hyphenation, *zone)); - let (end, hyph, pushed_in) = match composed.as_ref().and_then(|lines| lines.get(*para_line)) { + let glyphs: &[SGlyph] = sg; + // The line broken in room `width` wide: its end, whether it ends with a hyphen, and whether + // it pushed characters in. + let break_in = |width: f64| match composed.as_ref().and_then(|lines| lines.get(*para_line)) { Some(&(end, hyph)) if end > *i => (end, hyph, false), _ if *i < n => { let start = *i; - let glyphs: &[SGlyph] = sg; // A horizontal line with right-to-left text is drawn in another order than its // text's, and its tabs take their stops in the order they are drawn: whether the line // ending before glyph `end` fits is measured as it is drawn ([`drawn_width`]). @@ -478,8 +488,56 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC } _ => (n, false, false), }; - let m = Metrics::max(&sg[*i..end]).unwrap_or(*pm); - baseline += if same_baseline || (pen.regions.is_none() && first_in_region) { + let (mut end, mut hyph, mut pushed_in) = break_in(x1 - x0 - ind_l - ind_r); + let metrics = |end: usize| glyphs.get(*i..end).and_then(Metrics::max).unwrap_or(*pm); + let m = metrics(end); + // The baseline moves from where `est` placed it to where the line's metrics set it, unless + // the line was placed at metrics of its own (below). + let mut settle = true; + if let Some(before) = before + && m != est + { + if m.union(est) != est { + // Taller than it was placed: place it again at metrics that hold both, and break it + // again in the room there. + *pen = before; + est = est.union(m); + continue; + } + // Broken again in the room of a taller line, it no longer holds what made that line + // taller. It goes where its own metrics place it when the room there holds the room it + // was broken in, and otherwise stays where it was placed, inside the room measured. + settle = false; + let mut own = before; + if let Some((b, a, z, _)) = own.place(m, ind_l, ind_r, word) + && a <= x0 + EPS + && z >= x1 - EPS + { + // The room there may hold more of the line, up to what would make it taller than + // `m`: the longest break that does not is between the two widths (a line breaks no + // earlier in more room). + let (mut lo, mut hi) = (x1 - x0 - ind_l - ind_r, z - a - ind_l - ind_r); + let wider = break_in(hi); + if metrics(wider.0).union(m) == m { + (end, hyph, pushed_in) = wider; + } else { + for _ in 0..REFIT_STEPS { + let mid = (lo + hi) / 2.0; + let line = break_in(mid); + if metrics(line.0).union(m) == m { + (end, hyph, pushed_in) = line; + lo = mid; + } else { + hi = mid; + } + } + } + *pen = own; + (baseline, x0, x1) = (b, a, z); + } + } + let m = metrics(end); + baseline += if !settle || same_baseline || (pen.regions.is_none() && first_in_region) { 0.0 } else if pen.model == LeadingModel::EmBoxTop { // The em box top is fixed (the frame's top, or the line above's): the baseline diff --git a/crates/text/src/tests/combos.rs b/crates/text/src/tests/combos.rs index 0146871265..26e8f8d63a 100644 --- a/crates/text/src/tests/combos.rs +++ b/crates/text/src/tests/combos.rs @@ -378,8 +378,8 @@ fn x_width() -> f64 { fn composer_x_mixed_sizes_in_a_stepped_frame() { // A frame as wide as the first line down to 40 pt and half as wide below. A 36 pt word makes // the line it is on reach below the step, where the frame is narrow: the every-line composer - // breaks the paragraph with each line's measure where its height sets it, not as the - // single-line composer does, and every line keeps within its room. + // breaks the paragraph with each line's measure where its height sets it, and every line keeps + // within its room. let w = x_width(); let set = |x: CharStyle, composer: Composer| { let mut t = stepped(w, w * 0.5, 40.0, 300.0); @@ -394,8 +394,10 @@ fn composer_x_mixed_sizes_in_a_stepped_frame() { // The 36 pt line reaches below the step and is set at the narrow measure. let tall = &l.lines[1]; assert!(tall.ascent > 30.0 && tall.baseline + tall.descent > 40.0 && tall.avail.1 < w * 0.5 + 0.01, "{:?}", l.lines); + // The single-line composer measures each line across its height too, and breaks this + // paragraph the same way. let (greedy, _) = set(sans(36.0), Composer::SingleLine); - assert_ne!(every, greedy); + assert_eq!(every, greedy); // Uniform sizes keep the breaks they had. let (uniform, _) = set(sans(12.0), Composer::EveryLine); assert_eq!(uniform, ["Destroy all creatures with toughness X or", "less. Then draw a card for each creature", "destroyed this way."]); @@ -435,8 +437,10 @@ fn composer_x_mixed_sizes_around_text_wrap() { 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 single-line composer measures each line across its height too, and breaks this + // paragraph the same way. let (greedy, _, _) = set(Composer::SingleLine); - assert_ne!(every, greedy); + assert_eq!(every, greedy); } #[test] @@ -463,6 +467,35 @@ fn composer_x_mixed_sizes_in_a_frame_that_widens() { assert_eq!(greedy, ["Destroy all", "creatures", "with toughness X or less. Then draw", "a card for each creature destroyed this", "way."]); } +#[test] +fn single_line_composer_measures_a_line_across_its_tallest_glyph() { + // A frame wide enough for "Destroy all creatures X" down to 20 pt and as wide as "Destroy all" + // below, and a rectangle as wide with an object (6 pt wrap offset) beside that part. The first + // line starts with 12 pt words and takes in a 36 pt word, which sets it lower and reaches below + // 20 pt: the single-line composer measures its room across that height, every line that + // reaches the narrow part is set in it, and the first line keeps the 12 pt words that fit + // above it. + let w = natural_width("Destroy all creatures X", 12.0) + 30.0; + let narrow = natural_width("Destroy all", 12.0) + 2.0; + let runs = vec![run("Destroy all creatures ", sans(12.0)), run("X", sans(36.0)), run(" or less. Then draw a card.", sans(12.0))]; + let object = Rect::new(narrow + 6.0, 26.0, w + 10.0, 300.0); + let mut beside = area(runs.clone(), Rect::new(0.0, 0.0, w, 300.0)); + beside.wrap = vec![vectorcraft_doc::WrapShape { path: PathData::from_bezpath(&object.to_path(0.1)), wrap: Default::default() }]; + let mut shaped = stepped(w, narrow, 20.0, 300.0); + shaped.runs = runs; + for (name, mut t) in [("stepped frame", shaped), ("text wrap", beside)] { + t.para.composer = Composer::SingleLine; + let l = layout(db(), &t); + assert!(past_their_room(&t, &l).is_empty() && !l.overflow, "{name}: {:?}", l.lines); + let low = l.lines.iter().filter(|li| li.baseline + li.descent > 20.01); + assert!(low.clone().all(|li| li.avail.1 <= narrow + 0.01), "{name}: {:?}", l.lines); + // The 36 pt word is set, on a line in the narrow part. + assert!(low.clone().any(|li| li.ascent > 30.0), "{name}: {:?}", l.lines); + assert_eq!(line_texts(&t, &l)[0], "Destroy all creatures", "{name}: {:?}", l.lines); + assert!(l.lines[0].baseline < 20.0 && l.lines[0].avail.1 > w - 0.01, "{name}: {:?}", l.lines); + } +} + #[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 diff --git a/docs/mcp.md b/docs/mcp.md index f756bc8c97..d42836a60f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -10448,6 +10448,10 @@ where the lines above it put it, across the line's own height, so a taller line 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). +Single-line measures each line's room the same way: a line that takes in a taller character is placed again across its +new height and broken again in the room there. When that leaves the taller character for the next line, the line is +set at its own height where the room there is no narrower, with the words before that character that fit there, and +otherwise stays where it was placed. 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. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 296bd84b2b..6b5933cbdc 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1299,7 +1299,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. 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. The Single-line Composer measures a line's room across the height of its tallest character too: a line that takes in a taller character is placed again and broken again in the room there. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.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`). From cf250db2e8b70185b9f9b66f14363942ee76a345 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 21:05:44 -0800 Subject: [PATCH 14/44] Paths: Reverse Path Direction turns the stroke's width profile and pen records with the path Reverse Path Direction reversed the anchors but left the stroke's width profile, pressure, tilt, bearing and rotation running the old way, so a tapered or pressure-drawn stroke changed shape. They now turn with the path through the blend code's stroke reorientation (the width profile's sides swap), and the stroke draws as before. A path whose subpaths do not all turn alike keeps its stroke as it was, as blends do. Arrowheads stay at the path's start and end. --- crates/doc/src/blend.rs | 47 ++++++++++++++++++----- crates/engine/src/cmd/path.rs | 30 ++++++++------- crates/engine/src/tests/attributes.rs | 55 +++++++++++++++++++++++++++ docs/mcp.md | 3 +- 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/crates/doc/src/blend.rs b/crates/doc/src/blend.rs index b84de1cac3..56764b6426 100644 --- a/crates/doc/src/blend.rs +++ b/crates/doc/src/blend.rs @@ -606,9 +606,8 @@ fn reoriented(points: &[(f64, V)], o: Orient, at: impl Fn(f64) -> V, wr out } -/// A key's stroke read along its subpath turned as `o` says: its width profile (whose sides swap -/// when reversed), pen pressure, tilt, bearing and rotation, and on a reversed open path its -/// arrowheads. +/// A stroke read along its subpath turned as `o` says: its width profile (whose sides swap when +/// reversed), pen pressure, tilt, bearing and rotation. Its arrowheads stay as they are. fn reorient_stroke(s: &mut StrokeLayer, o: Orient) { if o.is_identity() { return; @@ -648,15 +647,45 @@ fn reorient_stroke(s: &mut StrokeLayer, o: Orient) { let out = reoriented(&p.points, o, |f| p.unwrapped_at(f), |v| v + turns, same); p.points = out; } - if o.reversed && !o.closed { - std::mem::swap(&mut s.start_arrow, &mut s.end_arrow); - s.arrow_scale = (s.arrow_scale.1, s.arrow_scale.0); +} + +/// `n` with every stroke read along its subpaths turned as `o` says, and on a reversed open path +/// its arrowheads swapped. +fn reorient_strokes(n: &mut Node, o: Orient) { + if o.is_identity() { + return; + } + for it in n.appearance.items.iter_mut() { + if let AppearanceItem::Stroke(s) = it { + reorient_stroke(s, o); + if o.reversed && !o.closed { + std::mem::swap(&mut s.start_arrow, &mut s.end_arrow); + s.arrow_scale = (s.arrow_scale.1, s.arrow_scale.0); + } + } } } -/// `n` with every stroke read along its subpaths turned as `o` says. -fn reorient_strokes(n: &mut Node, o: Orient) { - if o.is_identity() { +/// Turn the strokes of path `n` for Reverse Path Direction, before its subpaths flagged in +/// `reversed` are reversed with [`SubPath::reverse`] (a closed one then starts at its last +/// anchor): each stroke's width profile (sides swapped), pen pressure, tilt, bearing and rotation +/// then run the new way, so the stroke draws as it did. A stroke reads one profile along every +/// subpath, so it turns only when all of them turn alike. Arrowheads stay at the path's start and +/// end. +pub fn reverse_stroke_records(n: &mut Node, reversed: &[bool]) { + let NodeKind::Path { path, .. } = &n.kind else { return }; + let mut turns = path.subpaths.iter().enumerate().map(|(i, sp)| { + let mut o = Orient::of(sp); + if reversed.get(i).copied().unwrap_or(false) { + if sp.closed { + o.start_at(sp, sp.anchors.len().saturating_sub(1)); + } + o.reverse(); + } + o + }); + let Some(o) = turns.next() else { return }; + if o.is_identity() || !turns.all(|t| t.same_turn(&o)) { return; } for it in n.appearance.items.iter_mut() { diff --git a/crates/engine/src/cmd/path.rs b/crates/engine/src/cmd/path.rs index 33bde936bf..34f41eda24 100644 --- a/crates/engine/src/cmd/path.rs +++ b/crates/engine/src/cmd/path.rs @@ -77,7 +77,7 @@ pub fn specs() -> Vec { "Reverse Path Direction", ["Object", "Path"], None, - "{ids?|id?, subpath?: usize (reverse only this subpath of each path, 0 included; default: every subpath), reversed?: bool (true: every subpath runs counter-clockwise on screen, false: clockwise, the Attributes panel's Reverse Path Direction On / Off; omitted: flip each path; with subpath, the direction is not read: that subpath is flipped)} the paths in ids or the selection (groups and compound paths: the paths inside), as one undo step → {changed: paths}", + "{ids?|id?, subpath?: usize (reverse only this subpath of each path, 0 included; default: every subpath), reversed?: bool (true: every subpath runs counter-clockwise on screen, false: clockwise, the Attributes panel's Reverse Path Direction On / Off; omitted: flip each path; with subpath, the direction is not read: that subpath is flipped)} the paths in ids or the selection (groups and compound paths: the paths inside), as one undo step; when every subpath of a path turns alike, its strokes' width profiles (sides swapped) and pen pressure, tilt, bearing and rotation turn with it, so they draw as before → {changed: paths}", has_selected_path, reverse ), @@ -462,22 +462,24 @@ fn reverse(s: &mut Session, p: &Value) -> Result { let mut changed = 0; for id in &ids { let path = path_mut(d, *id)?; - if let Some(si) = subpath { - if let Some(sp) = path.subpaths.get_mut(si) { + let flip: Vec = match subpath { + Some(si) => (0..path.subpaths.len()).map(|i| i == si).collect(), + None => path.subpaths.iter().map(|sp| want.is_none_or(|ccw| (sp.signed_area() < 0.0) != ccw)).collect(), + }; + if !flip.contains(&true) { + continue; + } + // The stroke's width profile and pen recordings turn with the path, read before it turns. + if let Some(n) = d.node_mut(*id) { + vectorcraft_doc::blend::reverse_stroke_records(n, &flip); + } + let path = path_mut(d, *id)?; + for (sp, f) in path.subpaths.iter_mut().zip(flip) { + if f { sp.reverse(); - changed += 1; - } - } else { - let flip: Vec = path.subpaths.iter().map(|sp| want.is_none_or(|ccw| (sp.signed_area() < 0.0) != ccw)).collect(); - if flip.contains(&true) { - for (sp, f) in path.subpaths.iter_mut().zip(flip) { - if f { - sp.reverse(); - } - } - changed += 1; } } + changed += 1; } Ok(changed) })?; diff --git a/crates/engine/src/tests/attributes.rs b/crates/engine/src/tests/attributes.rs index d3edf2615b..8ddfc352a1 100644 --- a/crates/engine/src/tests/attributes.rs +++ b/crates/engine/src/tests/attributes.rs @@ -163,6 +163,61 @@ fn reverse_path_direction_on_and_off_set_the_winding() { assert!(s.execute("path.reverse", &json!({})).is_err(), "nothing to reverse"); } +#[test] +fn reverse_path_direction_turns_the_width_profile_and_pen_records_with_the_path() { + use vectorcraft_doc::{AngleProfile, PressureProfile, WidthProfile}; + let mut s = session(); + let open = + NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 0, "y": 50}, {"x": 40, "y": 50}, {"x": 100, "y": 50}]}))["id"].as_u64().unwrap()); + let closed = rect(&mut s, 10.0); + let profile = WidthProfile { points: vec![(0.0, 0.2, 0.6), (0.3, 1.0, 0.4), (1.0, 0.5, 0.9)] }; + let pressure = PressureProfile { points: vec![(0.0, 0.1), (0.25, 0.9), (1.0, 0.4)] }; + let bearing = AngleProfile { points: vec![(0.0, 10.0), (0.6, 80.0), (1.0, 170.0)] }; + s.edit("set", |d, _| { + for id in [open, closed] { + let st = d.node_mut(id).unwrap().appearance.stroke_mut().unwrap(); + st.profile = Some(profile.clone()); + st.pressure = Some(pressure.clone()); + st.tilt = Some(pressure.clone()); + st.bearing = Some(bearing.clone()); + st.rotation = Some(bearing.clone()); + } + Ok(()) + }) + .unwrap(); + // (anchor point, its fraction of the path's length) for each anchor. + let along = |s: &Session, id: NodeId| { + let sp = s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].clone(); + let pos = sp.anchor_arc_positions(); + let total = *pos.last().unwrap(); + sp.anchors.iter().zip(pos).map(|(a, at)| (a.p, at / total)).collect::>() + }; + let before = [along(&s, open), along(&s, closed)]; + run(&mut s, "select.set", json!({"ids": [open.0, closed.0]})); + assert_eq!(run(&mut s, "path.reverse", json!({}))["changed"], 2); + for ((id, before), closed) in [open, closed].into_iter().zip(before).zip([false, true]) { + let after = along(&s, id); + let st = s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().clone(); + // Each anchor keeps the widths it had (its sides swapped, as the path runs the other way) + // and what the pen recorded there. (A closed path's start is where its profile steps.) A + // closed path now starts part way along a span of the width profile, which is resampled + // there, so its smooth curve moves a little. + let near = if closed { 0.02 } else { 1e-6 }; + for (p, t) in before.iter().filter(|(_, t)| !closed || *t > 0.0) { + let u = after.iter().find(|(q, _)| q.distance(*p) < 1e-9).unwrap().1; + let (l, r) = profile.at(*t); + let (l2, r2) = st.profile.as_ref().unwrap().at(u); + assert!((l - r2).abs() < near && (r - l2).abs() < near, "{id:?} at {p:?}: {:?} then {:?}", (l, r), (l2, r2)); + for got in [&st.pressure, &st.tilt] { + assert!((pressure.at(*t) - got.as_ref().unwrap().at(u)).abs() < 1e-6, "{id:?} pressure at {p:?}"); + } + for got in [&st.bearing, &st.rotation] { + assert!((bearing.at(*t) - got.as_ref().unwrap().at(u)).abs() < 1e-6, "{id:?} bearing at {p:?}"); + } + } + } +} + #[test] fn attributes_survive_a_native_round_trip() { let mut s = session(); diff --git a/docs/mcp.md b/docs/mcp.md index f756bc8c97..ab357c50d6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5000,7 +5000,8 @@ The Attributes panel (`window.panel {panel: "attributes"}`, Cmd+F11) reads `attr objects differ) and `recentUrls`. `attributes.set {overprintFill?, overprintStroke?, showCenter?, imageMap?, url?, note?, ids?}` sets them in one undo step; `path.setFillRule {rule: "nonZero"|"evenOdd"}` sets the fill rule of the paths and compound paths, and `path.reverse {reversed?}` makes subpaths run counter-clockwise (true) or clockwise -(false). SVG export wraps an object with a URL in ``, and SVG import reads `` links back. +(false). When every subpath of a path turns alike, its strokes' width profiles (left and right swapped) and pen +pressure, tilt, bearing and rotation turn with it, so the strokes draw as before. SVG export wraps an object with a URL in ``, and SVG import reads `` links back. ```json {"name":"run_command","arguments":{"command":"attributes.set","params":{"ids":[12],"url":"https://example.com","imageMap":"rectangle"}}} From c5e2e9210a7d0ce9a7acd33eeb495ad882c7b30e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 21:16:20 -0800 Subject: [PATCH 15/44] Sort Objects: an opacity sort that gives marks their own opacities changes nothing With apply "opacity", Sort Objects compared each object's opacity with the value it gets as a percentage, within 1e-6. An evenly stepped value can differ from a mark's percentage by more than that while it rounds to the same stored opacity: marks at 30%, 65% and 100% sorted by x with distribute on keep their opacities, yet the 65% mark counted as changed. The sort then made an undo step and replaced the opacity the graph stores for the series (30%) with 65%. The sort now compares each object's stored opacity with the opacity it is given, as stored, both to skip a run that changes nothing and to pick the marks whose opacity the graph keeps. --- crates/engine/src/cmd/addons/arrange.rs | 12 +++++++----- crates/engine/src/cmd/graph/tests.rs | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/addons/arrange.rs b/crates/engine/src/cmd/addons/arrange.rs index d7cae868ed..67cf475979 100644 --- a/crates/engine/src/cmd/addons/arrange.rs +++ b/crates/engine/src/cmd/addons/arrange.rs @@ -516,16 +516,18 @@ fn sort(s: &mut Session, p: &Value) -> Result { } let wanted: Vec<(&Sorted, f64)> = rank.iter().filter_map(|i| items.get(*i)).zip(values).collect(); if apply == "opacity" { - if wanted.iter().all(|(o, v)| (o.opacity - v).abs() < 1e-6) { + // The opacity each object is given, as stored; an object whose stored opacity it already is has not changed. + let given: Vec<(NodeId, f32)> = wanted.iter().map(|(o, v)| (o.id, (v / 100.0) as f32)).collect(); + let changed: Vec = given.iter().filter(|(id, a)| d.node(*id).is_some_and(|n| n.opacity != *a)).map(|(id, _)| *id).collect(); + if changed.is_empty() { return Ok(count); } s.edit("Sort Objects", |d, _| { - for (o, v) in &wanted { - if let Some(n) = d.node_mut(o.id) { - n.opacity = (v / 100.0) as f32; + for (id, a) in &given { + if let Some(n) = d.node_mut(*id) { + n.opacity = *a; } } - let changed: Vec = wanted.iter().filter(|(o, v)| (o.opacity - v).abs() >= 1e-6).map(|(o, _)| o.id).collect(); keep_series_opacity(d, &changed); Ok(()) })?; diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index c997e803d5..0ca3cf1d96 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -757,6 +757,24 @@ fn an_opacity_sort_objects_stores_the_frontmost_mark_it_changes() { assert_eq!(spec(&s, id).series_paints[0].opacity, Some(0.6)); } +/// A Sort Objects that gives series marks the opacities they already have changes nothing: the value the graph stores +/// stays and no undo step is made, even when the evenly stepped percentage differs from a mark's in the last digits. +#[test] +fn an_opacity_sort_objects_that_changes_no_mark_keeps_the_stored_opacity() { + let (mut s, id, _, marks) = column_marks(); + s.execute("object.setProps", &json!({"id": marks[1].0, "opacity": 65})).unwrap(); + s.execute("object.setProps", &json!({"id": marks[0].0, "opacity": 30})).unwrap(); + assert_eq!(spec(&s, id).series_paints[0].opacity, Some(0.3)); + // From left to right the columns have 30%, 65% and 100%, the values an even step from 30% to 100% gives them. + let undo = s.doc().unwrap().history.undo.len(); + let ids = [marks[0].0, marks[1].0, marks[2].0]; + s.execute("addon.arrange.sort", &json!({"ids": ids, "by": "x", "apply": "opacity", "distribute": true})).unwrap(); + let opacities: Vec = marks[..3].iter().map(|m| s.doc().unwrap().doc.node(*m).unwrap().opacity).collect(); + assert_eq!(opacities, [0.3, 0.65, 1.0]); + assert_eq!(spec(&s, id).series_paints[0].opacity, Some(0.3)); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step"); +} + /// An object opacity, blend mode, fill, stroke and weight that Global Edit gives a mark of another series from the mark /// that was edited are stored on the graph for that series in the same undo step, and every mark of the series has them /// when the graph is drawn again. From d64ba43e50cf90e3ab0055b8e8325f85e87864a5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 21:17:21 -0800 Subject: [PATCH 16/44] 3D: Inflate with no volume solves each puff only when its solid is taken At a Volume of 0 % no shape puffs, so the eye does not depend on the other shapes. Each shape's membrane is now solved as its solid is taken, and a caller that stops at the object's face limit solves none of the shapes after it. With any volume every puff is still solved first, since the highest one places the eye, and the solids are unchanged bit for bit. The shadow no longer builds the solid of a fully transparent fill or stroke: its puff still places the eye, and at 0 % volume it is not solved either. --- crates/three-d/src/shadow.rs | 18 +++--- crates/three-d/src/shadow/tests.rs | 32 ++++++++++ crates/three-d/src/solid.rs | 4 +- crates/three-d/src/solid/inflate.rs | 93 ++++++++++++++++++++++++----- crates/three-d/src/solid/tests.rs | 63 +++++++++++++++++++ docs/target-app-parity.md | 1 + docs/three-d.md | 4 ++ 7 files changed, 191 insertions(+), 24 deletions(-) diff --git a/crates/three-d/src/shadow.rs b/crates/three-d/src/shadow.rs index d7cd01a869..9c2139bfc2 100644 --- a/crates/three-d/src/shadow.rs +++ b/crates/three-d/src/shadow.rs @@ -172,8 +172,8 @@ impl ShadowCaster { /// Add Inflate's solids for several shapes of one object ([`crate::inflate_together`]), each /// shape whose entry in `casts` is true: the eye is placed for every shape, as for the solids - /// drawn, but the faces of the others are not kept (their range is empty). A shape that does - /// not inflate is left out, as its solid is; the result says which, in order. + /// drawn, but the solids of the others are not built (their range is empty). A casting shape + /// that does not inflate is left out, as its solid is; the result says which, in order. pub fn inflate( &mut self, shapes: &[(&PathData, FillRule)], @@ -182,11 +182,15 @@ impl ShadowCaster { options: Inflate, view: View, ) -> Vec, String>> { - inflate_faces(shapes, bounds, options, view, true) - .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()) } + let mut solids = inflate_faces(shapes, bounds, options, view, true); + (0..shapes.len()) + .map(|i| { + if !casts.get(i).copied().unwrap_or(true) { + solids.pass(); + return Ok(self.faces.len()..self.faces.len()); + } + let (_, faces, projection) = solids.next().unwrap_or_else(|| Ok((vec![], vec![], None)))?; + self.add(faces, projection) }) .collect() } diff --git a/crates/three-d/src/shadow/tests.rs b/crates/three-d/src/shadow/tests.rs index 3fb3767177..00aedc3a53 100644 --- a/crates/three-d/src/shadow/tests.rs +++ b/crates/three-d/src/shadow/tests.rs @@ -185,3 +185,35 @@ fn every_effect_casts_a_shadow() { // The surface turns about the square's left edge, reaching 100 to either side of it. assert!(b.x1 > 100.0 && b.x0 < 100.0 + 1e-6, "{b:?}"); } + +/// An Inflate shape that casts nothing still places the eye with its puff, but its solid is not +/// built (nor, with no volume, its puff solved); the shapes that cast keep the very faces and +/// projection they have when every shape casts. +#[test] +fn an_inflate_shape_that_casts_nothing_builds_no_solid() { + let (a, b) = (square(100.0, 100.0, 100.0), square(150.0, 150.0, 30.0)); + let shapes = [(&a, FillRule::NonZero), (&b, FillRule::NonZero)]; + let bounds = Rect::new(100.0, 100.0, 200.0, 200.0); + let view = View { perspective: 60.0, rotation_y: 30.0, ..front() }; + let built = || crate::solid::INFLATED.with(std::cell::Cell::get); + let puffed = || crate::solid::PUFFED.with(std::cell::Cell::get); + for o in [Inflate::default(), Inflate { volume: 0.0, depth: 20.0, ..Inflate::default() }] { + let mut all = ShadowCaster::new(); + let every = all.inflate(&shapes, &[true, true], bounds, o, view); + let (start, solved) = (built(), puffed()); + let mut some = ShadowCaster::new(); + let kept = some.inflate(&shapes, &[false, true], bounds, o, view); + assert_eq!(built(), start + 1, "{o:?}: only the casting shape's solid is built"); + if o.volume == 0.0 { + assert_eq!(puffed(), solved + 1, "{o:?}: only the casting shape's puff is solved"); + } + let (Ok(first), Ok(second)) = (&every[0], &every[1]) else { panic!("{every:?}") }; + assert!(!first.is_empty() && !second.is_empty()); + assert_eq!(kept, vec![Ok(0..0), Ok(0..second.len())]); + assert_eq!(format!("{:?}", some.faces), format!("{:?}", &all.faces[second.clone()])); + assert_eq!(some.projection, all.projection); + let mut none = ShadowCaster::new(); + assert_eq!(none.inflate(&shapes, &[false, false], bounds, o, view), vec![Ok(0..0), Ok(0..0)]); + assert!(none.is_empty() && none.projection.is_none()); + } +} diff --git a/crates/three-d/src/solid.rs b/crates/three-d/src/solid.rs index 49b0738d9e..652f46a3f1 100644 --- a/crates/three-d/src/solid.rs +++ b/crates/three-d/src/solid.rs @@ -27,9 +27,9 @@ pub use build::{extrude, plane}; pub use face_map::{FaceMap, extrude_face_map, plane_face_map}; #[cfg(test)] use inflate::GRID_WORK; -#[cfg(test)] -pub(crate) use inflate::INFLATED; pub(crate) use inflate::inflate_faces; +#[cfg(test)] +pub(crate) use inflate::{INFLATED, PUFFED, SOLVE_FIRST}; pub use inflate::{inflate, inflate_each, inflate_together}; #[cfg(test)] use membrane::{Edges, GRID_BUDGET, MAX_EDGE_ENTRIES, MAX_GRID_WORK, Membrane, cell_triangles}; diff --git a/crates/three-d/src/solid/inflate.rs b/crates/three-d/src/solid/inflate.rs index d3773a92f9..2500af48c3 100644 --- a/crates/three-d/src/solid/inflate.rs +++ b/crates/three-d/src/solid/inflate.rs @@ -23,6 +23,8 @@ struct Puff { /// The rings of `path` and its membrane on the grid of `bounds`; `None` for a shape with no area. fn puff(path: &PathData, rule: FillRule, bounds: Rect, o: &Inflate) -> Result, String> { + #[cfg(test)] + PUFFED.with(|n| n.set(n.get() + 1)); // The sides take one face for each edge of the outline in each slice; the grid's faces are // counted as they are built. let size = bounds.width().max(bounds.height()); @@ -66,7 +68,7 @@ pub fn inflate_together(shapes: &[(&PathData, FillRule)], bounds: Rect, options: } /// [`inflate_together`], each solid built only as the iterator reaches it: a caller that stops early -/// builds none of the solids after. +/// builds none of the solids after (and with no volume, solves none of their puffs). pub fn inflate_each( shapes: &[(&PathData, FillRule)], bounds: Rect, @@ -77,28 +79,89 @@ pub fn inflate_each( } /// [`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. -pub(crate) fn inflate_faces( - shapes: &[(&PathData, FillRule)], - bounds: Rect, - options: Inflate, - view: View, - mesh: bool, -) -> impl Iterator> + use<> { +/// Every shape's membrane is solved at once (the eye depends on the highest puff), except with no +/// volume: nothing puffs then, and each shape's membrane is solved when its solid is taken. 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) -> Solids { let o = options.sanitized(); + let later = o.volume == 0.0; + #[cfg(test)] + let later = later && !SOLVE_FIRST.with(std::cell::Cell::get); + if later { + let shapes: Vec<(PathData, FillRule)> = shapes.iter().map(|(path, rule)| ((*path).clone(), *rule)).collect(); + return Solids { pending: Pending::Later(shapes.into_iter()), bounds, o, view, reach: 0.0, mesh }; + } 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)), - Err(e) => Err(e), - }) + Solids { pending: Pending::Solved(puffs.into_iter()), bounds, o, view, reach, mesh } +} + +/// The shapes' puffs still to take: every one solved when the shapes were laid out, or each +/// shape's path and fill rule, its puff solved when its solid is taken. +enum Pending { + Solved(std::vec::IntoIter, String>>), + Later(std::vec::IntoIter<(PathData, FillRule)>), +} + +/// The solids of [`inflate_faces`], one result for each shape in order, each built as it is taken. +pub(crate) struct Solids { + pending: Pending, + bounds: Rect, + o: Inflate, + view: View, + /// The highest puff of the shapes drawn together. + reach: f64, + mesh: bool, +} + +impl Solids { + /// Pass over the next shape without building its solid, or solving its puff if that waits. + pub(crate) fn pass(&mut self) { + match &mut self.pending { + Pending::Solved(puffs) => { + puffs.next(); + } + Pending::Later(shapes) => { + shapes.next(); + } + } + } +} + +impl Iterator for Solids { + type Item = Result; + + fn next(&mut self) -> Option { + let p = match &mut self.pending { + Pending::Solved(puffs) => puffs.next()?, + Pending::Later(shapes) => { + let (path, rule) = shapes.next()?; + check_bounds(self.bounds).and_then(|()| puff(&path, rule, self.bounds, &self.o)) + } + }; + Some(match p { + Ok(Some(p)) => inflated(p, self.bounds, self.o, self.view, self.reach, self.mesh), + Ok(None) => Ok((vec![], vec![], None)), + Err(e) => Err(e), + }) + } + + fn size_hint(&self) -> (usize, Option) { + match &self.pending { + Pending::Solved(puffs) => puffs.size_hint(), + Pending::Later(shapes) => shapes.size_hint(), + } + } } #[cfg(test)] thread_local! { + /// How many puffs this thread has solved, for the tests. + pub(crate) static PUFFED: std::cell::Cell = const { std::cell::Cell::new(0) }; + /// Solve every shape's puff before the first solid is taken even with no volume, as with + /// volume, for the tests that compare the two. + pub(crate) static SOLVE_FIRST: std::cell::Cell = const { std::cell::Cell::new(false) }; /// How many inflated solids this thread has built, for the tests. pub(crate) static INFLATED: std::cell::Cell = const { std::cell::Cell::new(0) }; /// The grid work of the last solid this thread inflated in full, for the tests. diff --git a/crates/three-d/src/solid/tests.rs b/crates/three-d/src/solid/tests.rs index e89a29afea..2a6d4786e9 100644 --- a/crates/three-d/src/solid/tests.rs +++ b/crates/three-d/src/solid/tests.rs @@ -1030,6 +1030,69 @@ fn a_flat_twisted_inflate_matches_the_extrusion() { } } +/// With no volume nothing puffs, so each shape's membrane is solved only when its solid is taken: +/// a caller that stops early (past the object's face limit) solves none after. With volume, the +/// eye waits on the highest puff, so every membrane is solved first. +#[test] +fn a_flat_inflate_solves_each_puff_as_its_solid_is_taken() { + let shapes: Vec = (0..3).map(|i| shape(&square(120.0 * i as f64, 0.0, 100.0)).0).collect(); + let shapes: Vec<(&PathData, FillRule)> = shapes.iter().map(|p| (p, FillRule::NonZero)).collect(); + let b = Rect::new(0.0, 0.0, 340.0, 100.0); + let puffed = || PUFFED.with(std::cell::Cell::get); + let start = puffed(); + let mut each = inflate_each(&shapes, b, Inflate { depth: 20.0, volume: 0.0, ..Inflate::default() }, front()); + assert_eq!(puffed(), start, "no puff is solved before the first solid is taken"); + assert!(each.next().is_some_and(|f| f.is_ok_and(|f| !f.is_empty()))); + assert_eq!(puffed(), start + 1, "the later puffs wait until their solids are taken"); + drop(each); + assert_eq!(puffed(), start + 1); + let start = puffed(); + let each = inflate_each(&shapes, b, Inflate { depth: 20.0, ..Inflate::default() }, front()); + assert_eq!(puffed(), start + 3, "with volume, every puff places the eye"); + assert_eq!(each.count(), 3); +} + +/// With no volume, solving each puff as its solid is taken gives the very solids that solving +/// every puff first gives: their facets, their faces before projection and their projection, bit +/// for bit, with an empty shape and one that cannot be inflated among them. +#[test] +fn a_flat_inflate_solved_as_taken_matches_one_solved_first() { + let mut frame = square(-10.0, -10.0, 260.0); + frame.extend(square(-8.0, -8.0, 256.0)); + let mut circle = BezPath::new(); + circle.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((180.0, 60.0), 50.0), 0.01)); + let mut bad = vectorcraft_geom::SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0)], true); + bad.anchors[1].h_out = Point::new(f64::NAN, 0.0); + let paths = [shape(&square(0.0, 0.0, 100.0)).0, shape(&frame).0, PathData::new(vec![]), PathData::new(vec![bad]), shape(&circle).0]; + let shapes: Vec<(&PathData, FillRule)> = paths.iter().map(|p| (p, FillRule::EvenOdd)).collect(); + let b = Rect::new(-10.0, -10.0, 250.0, 250.0); + let turned = View { rotation_x: -20.0, rotation_y: 30.0, ..front() }; + let options = [ + Inflate { volume: 0.0, ..Inflate::default() }, + Inflate { volume: 0.0, depth: 40.0, twist: 90.0, taper: 0.5, ..Inflate::default() }, + Inflate { volume: 0.0, depth: 10.0, both_sides: true, cap: false, ..Inflate::default() }, + ]; + for view in [front(), turned, View { perspective: 60.0, ..turned }, View { perspective: 100.0, rotation_y: 150.0, ..turned }] { + for o in options { + for mesh in [false, true] { + let solids = |first: bool| { + SOLVE_FIRST.with(|s| s.set(first)); + let out: Vec> = inflate_faces(&shapes, b, o, view, mesh).collect(); + SOLVE_FIRST.with(|s| s.set(false)); + out + }; + let (taken, first) = (solids(false), solids(true)); + assert_eq!(taken.len(), 5); + // Without a mesh the solid keeps its facets, with one its faces before projection. + let built = |r: &Result| r.as_ref().is_ok_and(|(f, m, _)| if mesh { !m.is_empty() } else { !f.is_empty() }); + assert!(built(&taken[0]) && built(&taken[1]) && built(&taken[4]), "{o:?} {view:?} mesh {mesh}"); + assert!(taken[2].as_ref().is_ok_and(|(f, m, p)| f.is_empty() && m.is_empty() && p.is_none()) && taken[3].is_err()); + assert_eq!(format!("{taken:?}"), format!("{first:?}"), "{o:?} {view:?} mesh {mesh}"); + } + } + } +} + /// #1724: under the nonzero rule a contour inside another that runs the same way cuts no hole /// (the middle is filled, as the art is drawn); one running the other way does, and so does /// either under the even-odd rule. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 296bd84b2b..c57e4f3cce 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1250,6 +1250,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Extrude & Bevel, Inflate and Rotate (Effect › 3D and Materials): live effects in the `three-d` crate with depth, twist, taper, solid or hollow caps, five original bevel shapes (width, height, repeat, spacing, inside, both sides), Inflate's volume, both sides, twist, taper and cap, Rotate's turned fills and strokes, Position presets, the rotation and light gizmos, SVG/PDF vector output and Expand Appearance; one 3D effect per object; covered by geometry, effect, canvas-versus-export, command and dialog tests ([three-d.md](three-d.md)). - Extrude & Bevel and Inflate build detailed shapes: each solid takes up to 65,536 outline points and 163,840 faces (VectorCraft's own limits, was 2,048 points and 32,768 faces), counted before any point is sampled; when the faces do not fit, every curve takes fewer pieces, down to 4. A closed path of 2,000 curves extrudes with a bevel and inflates, and paths of 5,000 and 10,000 curves build at the default settings (`crates/three-d/src/solid/tests_limits.rs`). - 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`. + - Inflate at a Volume of 0 % works out each shape's puff only when its solid is built, so no puff is worked out for the shapes after the face limit stops, and the solids are the same bit for bit; with any volume every puff is worked out first, as the highest one places the eye. The shadow builds no solid for a fully transparent fill or stroke. Tests `solid::tests::a_flat_inflate_solves_each_puff_as_its_solid_is_taken`, `a_flat_inflate_solved_as_taken_matches_one_solved_first`, `shadow::tests::an_inflate_shape_that_casts_nothing_builds_no_solid`. - 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)). - 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)). diff --git a/docs/three-d.md b/docs/three-d.md index 6de07fc2fe..6375fe9ec7 100644 --- a/docs/three-d.md +++ b/docs/three-d.md @@ -169,6 +169,10 @@ The rest is our reading: once their faces pass it 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. A stroke left out for its own limits does not count. +- Inflate works out how high every shape puffs before it builds the first solid, because the highest puff sets + how far away the eye is. At a Volume of 0 % no shape puffs, so each shape's puff is worked out only when its + solid is built, and none is worked out for the solids after the limit stops. The shadow builds no solid for a + fill or stroke that is fully transparent. ## Groups From a903feb870f08fc224b7be236d2cdbaf358587fb Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 15:43:53 -0800 Subject: [PATCH 17/44] Dimension tool: no construction guides through a first point off the anchors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With Smart Guides on, the Dimension tool's second point snapped onto the construction guides through the first point (Smart Guides › Construction Guides), so a click beside a segment straight below a first point that was not on an anchor stayed on the vertical guide instead of landing on the segment. The second point now has no construction guides through a first point that is not on an anchor. After a first point picked on an anchor, the construction guides through it still apply. The second point still snaps to anchors, centers and alignment lines, and with Snap to Perpendicular or Snap to Tangent on it lands where the line from the first point meets a segment square or grazes a curve. Docs: docs/dimensions.md, docs/target-app-parity.md. --- crates/engine/tests/it/dimensions.rs | 19 +++-- crates/tools/src/dimension.rs | 116 +++++++++++++++++++++++---- crates/tools/src/guides/snap.rs | 9 ++- docs/dimensions.md | 14 ++-- docs/target-app-parity.md | 3 +- 5 files changed, 129 insertions(+), 32 deletions(-) diff --git a/crates/engine/tests/it/dimensions.rs b/crates/engine/tests/it/dimensions.rs index 56ee67be17..c0a047dda3 100644 --- a/crates/engine/tests/it/dimensions.rs +++ b/crates/engine/tests/it/dimensions.rs @@ -1721,8 +1721,9 @@ fn both_points_snap_onto_anchors_with_a_visible_indicator() { } /// Smart Guides for the Linear picks: hovering away from any line, the first point slides into -/// line with another object's corner and its guides show before the click; the second slides onto -/// the construction guide through the first. The dimension measures between the snapped points. +/// line with another object's corner and its guides show before the click; the second slides into +/// line with the other corner, with no construction guide through the first point pulling it +/// sideways. The dimension measures between the snapped points. #[test] fn linear_picks_show_and_snap_to_smart_guides() { use vectorcraft_engine::tools::Overlay; @@ -1738,18 +1739,20 @@ fn linear_picks_show_and_snap_to_smart_guides() { assert!(o.iter().any(|o| matches!(o, Overlay::Line { .. } | Overlay::Label { .. })), "its guides show before the click: {o:?}"); ev(&mut s, PointerKind::Down, 250.0, 202.5); ev(&mut s, PointerKind::Up, 250.0, 202.5); - // Straight below the first point, 1.5 pt off: the rubber band ends on the vertical through it. - ev(&mut s, PointerKind::Move, 251.5, 260.0); + // Below the first point, 1.5 pt to its right and 2.5 pt off the bottom edge's level: the + // rubber band ends on that level, where the pointer is across it. + ev(&mut s, PointerKind::Move, 251.5, 262.5); let end = overlays(&mut s).into_iter().find_map(|o| if let Overlay::Line { b, dashed: true, .. } = o { Some(b) } else { None }).unwrap(); - assert_eq!((end.x, end.y), (250.0, 260.0), "on the construction guide"); - ev(&mut s, PointerKind::Down, 251.5, 260.0); - ev(&mut s, PointerKind::Up, 251.5, 260.0); + assert_eq!((end.x, end.y), (251.5, 260.0), "in line with the bottom edge, not on a vertical through the first point"); + ev(&mut s, PointerKind::Down, 251.5, 262.5); + ev(&mut s, PointerKind::Up, 251.5, 262.5); ev(&mut s, PointerKind::Move, 220.0, 230.0); assert!(!overlays(&mut s).iter().any(|o| matches!(o, Overlay::Label { .. })), "placing: the guides are gone"); ev(&mut s, PointerKind::Down, 220.0, 230.0); ev(&mut s, PointerKind::Up, 220.0, 230.0); let id = s.doc().unwrap().selection.objects[0].0; - assert_eq!(text(&s, id), "60.00 pt"); + // Aligned from (250, 200) to (251.5, 260): √(1.5² + 60²). + assert_eq!(text(&s, id), "60.02 pt"); } /// The page content streams of exported PDF `v`, inflated. diff --git a/crates/tools/src/dimension.rs b/crates/tools/src/dimension.rs index 14f7f85881..0840d92e08 100644 --- a/crates/tools/src/dimension.rs +++ b/crates/tools/src/dimension.rs @@ -240,12 +240,18 @@ fn points_source(cx: &ToolContext, (a, ra): (Point, &Option), (b, rb): impl DimensionTool { /// Where a Linear pick at `p` lands: on the nearest anchor of any path, within reach (kept as /// a link to its path); else where Smart Guides put it (another object's anchor or centre, in - /// line with one, or for the second point on a construction guide through the first), its - /// guides showing; else on the closest point of the line under the pointer; else `p` itself. + /// line with one, or for the second point square to a segment or grazing a curve from the + /// first with Snap to Perpendicular or Snap to Tangent on, or on a construction guide through + /// a first point picked on an anchor), its guides showing; else on the closest point of the + /// line under the pointer; else `p` itself. A first point not on an anchor has no + /// construction guides through it. /// A point the guides leave where the pointer is (right on a guide line) stays there, and with /// Snap to Pixel or Snap to Grid on a point stays where they place it. fn snap_linear(&mut self, cx: &ToolContext, p: Point) -> (Point, Option) { - let leave = self.points.last().map(|(first, _)| crate::guides::Leave::segment(cx, *first, false)); + let leave = self.points.last().map(|(first, anchor)| match anchor { + Some(_) => crate::guides::Leave::segment(cx, *first, false), + None => crate::guides::Leave::contact(*first), + }); let guided = self.snap.hover(cx, p, &[], leave.as_ref()); if let Some((point, reference)) = anchor_under(cx, p) { self.snap.clear(); @@ -846,10 +852,11 @@ mod tests { } } - /// A point that Smart Guides leave where the pointer already is (the pointer right on a - /// construction guide or in line with a target) stays on the guide the overlays show. It - /// does not move to the nearest point of a segment within the pick's reach. Off the guides, - /// a point near a segment still lands on it. + /// A point that Smart Guides leave where the pointer already is (the pointer right in line + /// with a target) stays on the guide the overlays show. It does not move to the nearest point + /// of a segment within the pick's reach. Off the guides, a point near a segment still lands + /// on it, also straight below the first point: the second point has no construction guide + /// through a first point that is not on an anchor. #[test] fn a_point_on_a_guide_stays_on_the_guide() { let (d, _) = doc_with_rect(); @@ -867,17 +874,22 @@ mod tests { // Off the guides, beside the edge: the closest point of the edge. let (on_edge, reference) = t.snap_linear(&c, Point::new(206.0, 170.5)); assert!(on_edge.distance(Point::new(200.0, 170.5)) < 1e-4 && reference.is_none(), "{on_edge:?}"); - // The first point, above the rectangle, then straight down along the vertical - // construction guide through it, past the right edge. + // The first point, above the rectangle, then straight down past the right edge: no + // vertical guide through the first point holds the second, so beside the edge it lands + // on the edge, and in line with the center it stays in line. click(&mut t, 206.0, 50.0); assert_eq!(t.points, vec![(Point::new(206.0, 50.0), None)]); - for y in [150.0, 170.5, 190.0] { - assert_eq!(t.snap_linear(&c, Point::new(206.0, y)), (Point::new(206.0, y), None), "on the construction guide at y = {y}"); - assert!(!t.snap.guides().is_empty()); + for y in [170.5, 190.0] { + let (q, reference) = t.snap_linear(&c, Point::new(206.0, y)); + assert!(q.distance(Point::new(200.0, y)) < 1e-4 && reference.is_none(), "beside the edge at y = {y}: {q:?}"); } - click(&mut t, 206.0, 170.5); + assert_eq!(t.snap_linear(&c, Point::new(207.0, 300.0)), (Point::new(207.0, 300.0), None), "away from the art"); + assert!(t.snap.guides().is_empty()); + assert_eq!(t.snap_linear(&c, Point::new(206.0, 150.0)), (Point::new(206.0, 150.0), None)); + assert!(!t.snap.guides().is_empty()); + click(&mut t, 206.0, 150.0); let dim = t.pending.as_ref().expect("both points were picked"); - assert_eq!((dim.a, dim.b), (Point::new(206.0, 50.0), Point::new(206.0, 170.5))); + assert_eq!((dim.a, dim.b), (Point::new(206.0, 50.0), Point::new(206.0, 150.0))); // The second point, off the edge, is held against the rectangle's anchors. let Some(DimensionSource::Points { free_b: Some(free), .. }) = &dim.source else { panic!("{:?}", dim.source) }; assert!(matches!(free, FreePoint::Anchors { .. }), "{free:?}"); @@ -894,6 +906,82 @@ mod tests { assert_eq!(DimensionTool::default().snap_linear(&pixel, Point::new(206.0, 170.0)), (Point::new(206.0, 170.0), None)); } + /// After a first point that is not on an anchor, the second point still lands where the line + /// from the first point meets a slanted segment square or grazes a curve, with the + /// "perpendicular" or "tangent" label, at places no construction guide through the first + /// point reaches. With Snap to Perpendicular and Snap to Tangent off it stays at the pointer. + #[test] + fn the_second_point_lands_square_to_a_segment_or_grazing_a_curve() { + use vectorcraft_doc::Appearance; + use vectorcraft_geom::{Rect, shapes}; + let (mut d, _) = doc_with_rect(); + let l = d.layers[0].id; + let (line, circle) = (d.alloc_id(), d.alloc_id()); + let slanted = shapes::line(Point::new(300.0, 100.0), Point::new(400.0, 150.0)); + d.insert(Some(l), 0, Node::path(line, slanted, Appearance::default_art())).unwrap(); + let round = shapes::ellipse(Rect::new(420.0, 370.0, 480.0, 430.0)); + d.insert(Some(l), 0, Node::path(circle, round, Appearance::default_art())).unwrap(); + let (s, p) = (Selection::default(), paint()); + let c = cx(&d, &s, &p); + let first = Point::new(300.0, 250.0); + let picked = |c: &ToolContext| { + let mut t = DimensionTool::default(); + for kind in [PointerKind::Move, PointerKind::Down, PointerKind::Up] { + t.pointer(c, &PointerEvent::new(kind, first.x, first.y)); + } + assert_eq!(t.points, vec![(first, None)]); + t + }; + let labeled = |t: &DimensionTool, label: &str| t.snap.guides().iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == label)); + // The foot of the perpendicular from (300, 250) to the line is (360, 130), along no + // construction guide angle from the first point. + let mut t = picked(&c); + let (q, reference) = t.snap_linear(&c, Point::new(361.0, 132.0)); + assert!(q.distance(Point::new(360.0, 130.0)) < 1e-4 && reference.is_none(), "{q:?}"); + assert!(labeled(&t, "perpendicular"), "{:?}", t.snap.guides()); + // Where a line from the first point grazes the circle about (450, 400), radius 30. + let (center, radius) = (Point::new(450.0, 400.0), 30.0); + let away = first - center; + let angle = away.y.atan2(away.x) + (radius / away.hypot()).acos(); + let touch = center + Vec2::new(angle.cos(), angle.sin()) * radius; + let (q, _) = t.snap_linear(&c, touch + Vec2::new(0.5, 0.5)); + assert!((q.distance(center) - radius).abs() < 0.05, "{q:?} is on the circle"); + assert!(((q - first).normalize().dot((q - center).normalize())).abs() < 0.01, "{q:?} grazes the circle"); + assert!(labeled(&t, "tangent"), "{:?}", t.snap.guides()); + // With both options off, the pointer near the foot lands on the closest point of the line + // instead. + let off = ToolContext { snap_perpendicular: false, snap_tangent: false, ..cx(&d, &s, &p) }; + let mut t = picked(&off); + let (q, _) = t.snap_linear(&off, Point::new(361.0, 132.0)); + assert!(q.distance(Point::new(360.0, 130.0)) > 0.5 && !labeled(&t, "perpendicular"), "{q:?}"); + } + + /// After a first point picked on an anchor, the second point still lands on the construction + /// guides through it. + #[test] + fn the_second_point_after_an_anchor_lands_on_construction_guides() { + let (d, id) = doc_with_rect(); + let (s, p) = (Selection::default(), paint()); + let c = cx(&d, &s, &p); + let mut t = DimensionTool::default(); + for kind in [PointerKind::Move, PointerKind::Down, PointerKind::Up] { + t.pointer(&c, &PointerEvent::new(kind, 200.0, 200.0)); + } + assert_eq!(t.points, vec![(Point::new(200.0, 200.0), Some(PointRef { node: id, subpath: 0, anchor: 2 }))]); + // 1.5 units right and below the 45° guide up and to the right of the anchor. + let (q, reference) = t.snap_linear(&c, Point::new(331.0, 72.0)); + assert!(q.distance(Point::new(329.5, 70.5)) < 1e-4 && reference.is_none(), "{q:?}"); + assert!(!t.snap.guides().is_empty()); + // The same offset from a first point off the anchors stays at the pointer. + let mut t = DimensionTool::default(); + for kind in [PointerKind::Move, PointerKind::Down, PointerKind::Up] { + t.pointer(&c, &PointerEvent::new(kind, 240.0, 240.0)); + } + assert_eq!(t.points, vec![(Point::new(240.0, 240.0), None)]); + assert_eq!(t.snap_linear(&c, Point::new(371.0, 112.0)), (Point::new(371.0, 112.0), None)); + assert!(t.snap.guides().is_empty()); + } + #[test] fn placement_press_previews_without_a_prior_move() { let (d, _) = doc_with_rect(); diff --git a/crates/tools/src/guides/snap.rs b/crates/tools/src/guides/snap.rs index 006f498b6e..d7ee8ecdd8 100644 --- a/crates/tools/src/guides/snap.rs +++ b/crates/tools/src/guides/snap.rs @@ -257,10 +257,11 @@ impl Leave { self } - /// The straight segment from the anchor `at` to a point being dragged (an open path's end - /// moved with Direct Selection): the point may land square to a segment or grazing a curve, - /// but is not held to Shift's angles or the construction guides, which a moved anchor - /// does not follow. + /// The straight segment from the point `at` to a point being placed (an open path's end + /// moved with Direct Selection, or the Dimension tool's second Linear point after a first + /// point that is not on an anchor): the point may + /// land square to a segment or grazing a curve, but is not held to Shift's angles or the + /// construction guides through `at`. pub fn contact(at: Point) -> Self { Self { at, shift: None, angles: &[], contact: true } } diff --git a/docs/dimensions.md b/docs/dimensions.md index 9cba8a66dc..4c1d836182 100644 --- a/docs/dimensions.md +++ b/docs/dimensions.md @@ -15,13 +15,17 @@ The **Dimension Tool** is in the Modify section of the toolbar. Its strip offers path's length. A press on an anchor picks it when the pointer moves less than 8 pixels before the release; a longer drag drags out the dimension of the segment under the pointer. A curved segment is measured from end to end. With Smart Guides on, both points also snap into line with other objects' - anchors and centres, and the second onto the construction guides through the first; the guides + anchors and centres. With Snap to Perpendicular or Snap to Tangent on, the second point also + snaps onto a segment where the line from the first meets it square or grazes a curve. When the + first point was picked on an anchor, the second also snaps onto the construction guides through + it; any other first point has no construction guides through it. The guides show while hovering, before each click. VectorCraft's own rules decide where a point lands: an anchor within 8 pixels takes it first; a point on a guide stays on the guide where the pointer - is, except that it moves onto a segment within the Smart Guides snapping tolerance, or onto the - place where a construction guide crosses another object's alignment line within twice that - tolerance; a point that no anchor, guide, grid or pixel snapping places lands on the closest - point of a segment within 8 pixels. + is, except that it moves onto a segment within the Smart Guides snapping tolerance, or, on a + construction guide or a Snap to Glyph guide that runs at an angle, onto the place where that + guide crosses another object's alignment line within twice that tolerance; a point that no + anchor, guide, grid or pixel snapping places lands on the closest point of a segment within 8 + pixels. A linear dimension is aligned, horizontal or vertical, as the [linear dimensions help](https://helpx.adobe.com/illustrator/desktop/measure-and-align/plot-and-measure/plot-linear-dimensions-of-objects.html) describes: after the second click, clicking again places an aligned dimension, and moving the diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 296bd84b2b..e5235b880c 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -591,7 +591,8 @@ since 2026-10-01. **Drawing tools, Star inner radius held with Ctrl/Cmd:** While a star is dragged out, Ctrl/Cmd keeps the inner radius the star showed when the key went down, also when the key is pressed with the pointer still. `a_star_drag_holds_its_inner_radius_with_ctrl_and_straightens_with_alt` and `a_star_drag_holds_the_inner_radius_it_shows_when_ctrl_goes_down` cover it, with Alt's straight shoulders and a drag canceled with Escape. **Drawing tools, Dimension tool horizontal and vertical linear dimensions:** A linear dimension between two clicked points or dragged out from a segment is aligned, horizontal or vertical, chosen by where the pointer moves while it is placed, and a click again by the second point gives aligned; `dimension.create` and `dimension.update` take `orientation`, and the dimension stays linked to the anchors it measures, keeps its orientation when it or the art turns, with a line that lay outside the points kept outside them; an unlinked horizontal or vertical dimension that is reflected has its line on the reflected side. `the_pointer_chooses_aligned_horizontal_or_vertical`, `two_points_measure_horizontally_vertically_or_aligned`, `clicking_again_by_the_second_point_places_an_aligned_dimension`, `a_segment_drag_measures_horizontally_vertically_or_aligned`, `points_on_a_vertical_line_measure_aligned`, `horizontal_and_vertical_dimensions_draw_lines_along_their_axis`, `turned_horizontal_dimensions_stay_horizontal`, `a_linked_horizontal_dimension_keeps_its_gap_from_turned_art`, `a_linked_horizontal_dimension_below_or_between_its_points_follows_turned_art`, `a_reflected_horizontal_dimension_keeps_its_line_on_the_reflected_side`, `a_dimension_without_an_orientation_reads_as_aligned`, `dimension_orientation_is_checked` and `a_turned_dimension_keeps_its_line_on_its_side` cover it. **Drawing tools, Dimension tool points that are not anchors follow the art:** A clicked point on a segment stays at its place along the segment, also when the segment curves, and a point off the art is linked to anchors (or, when the anchors lie on one line, anchors and handles) of the path, so both ends of the dimension follow when the art is moved, scaled, turned or reflected, also when the point was placed by a Smart Guide and when the dimension is saved and opened again; a linked linear dimension of reflected art (other than art on one line) has its line on the reflected side; deleting the art, changing its anchor count or whether it is closed, turning an angle's arm into a curve, or making a measured circle or arc no longer circular leaves the dimension where it was, no longer linked, instead of removing it. `a_dimension_to_a_point_on_a_segment_follows_the_art_at_both_ends`, `a_dimension_to_a_point_off_the_art_moves_with_the_art`, `a_dimension_whose_art_changes_its_anchors_stays_where_it_was`, `a_dimension_whose_art_is_opened_stays_where_it_was`, `angle_and_arc_dimensions_whose_art_no_longer_fits_stay_where_they_were`, `a_dimension_that_cannot_be_drawn_again_keeps_its_art`, `a_linked_dimension_of_reflected_art_has_its_line_on_the_reflected_side`, `a_dimension_to_a_point_off_a_line_follows_the_line`, `a_point_off_an_arch_is_reflected_with_the_arch`, `a_point_beside_an_almost_straight_curve_follows_it`, `a_point_off_a_loop_with_its_anchors_in_one_place_follows_it`, `a_point_beside_a_reflected_line_moves_as_a_half_turn_would`, `a_point_on_a_segment_stays_on_it_when_the_segment_curves`, `a_linked_dimension_of_a_reflected_curve_has_its_line_on_the_reflected_side`, `a_dimension_of_art_reflected_across_its_points_changes_sides`, `a_dimension_between_paths_reflected_each_in_its_own_way_keeps_its_offset`, `free_points_survive_a_native_file`, `dimension_free_points_are_checked`, `points_off_anchors_are_held_against_the_path` and `a_point_smart_guides_place_is_held_against_the_path` cover it. -**Drawing tools, Dimension tool points on a Smart Guide stay on the guide:** By VectorCraft's own rule, a clicked point that lies on a Smart Guide already, such as a point straight below the first one on the vertical construction guide through it, or one in line with an object's center, stays where the pointer is when no anchor is within 8 pixels and no segment is within the Smart Guides snapping tolerance, instead of moving onto a segment farther than that tolerance and within 8 pixels; with Snap to Grid or Snap to Pixel on, a point stays on the grid or the whole pixel those options give. `a_point_on_a_guide_stays_on_the_guide` covers it. +**Drawing tools, Dimension tool points on a Smart Guide stay on the guide:** By VectorCraft's own rule, a clicked point that lies on a Smart Guide already, such as one in line with an object's center, stays where the pointer is when no anchor is within 8 pixels and no segment is within the Smart Guides snapping tolerance, instead of moving onto a segment farther than that tolerance and within 8 pixels; with Snap to Grid or Snap to Pixel on, a point stays on the grid or the whole pixel those options give. `a_point_on_a_guide_stays_on_the_guide` covers it. +**Drawing tools, Dimension tool second point without construction guides through a first point off the anchors:** By VectorCraft's own rule, with Smart Guides on, the second Linear point does not snap to the construction guides through a first point that is not on an anchor, so a click beside a segment straight below such a first point lands on the segment; after a first point picked on an anchor, the construction guides through it still apply. The second point still snaps to anchors, centers and alignment lines, and with Snap to Perpendicular or Snap to Tangent on, it lands where the line from the first point meets a segment square or grazes a curve. `a_point_on_a_guide_stays_on_the_guide`, `the_second_point_after_an_anchor_lands_on_construction_guides`, `the_second_point_lands_square_to_a_segment_or_grazing_a_curve` and `linear_picks_show_and_snap_to_smart_guides` cover it. **Drawing tools, Dimension tool dimensions selected with their art keep their lines and label:** Object › Expand, Object › Expand Appearance and Object › Path › Outline Stroke leave a dimension selected with art it measures, directly or in selected groups without fills, strokes or effects of their own, as it is and change only the other art; when the new art no longer fits its link, the dimension stays where it was, no longer linked, and undo links it again. `expand_leaves_a_dimension_selected_with_its_art_as_it_is`, `expand_appearance_leaves_a_dimension_selected_with_its_art_as_it_is`, `outline_stroke_leaves_a_dimension_selected_with_its_art_as_it_is`, `a_dimension_selected_with_art_that_has_nothing_to_expand_changes_nothing`, `a_group_effect_expanded_reaches_the_dimension_it_holds`, `expand_keeps_a_dimension_in_its_asset_and_its_variable_binding`, `a_dimension_keeps_no_link_to_the_art_expand_replaced`, `a_dimension_outlasts_the_art_outline_stroke_or_expand_appearance_replaced`, `a_dimension_outlasts_the_stroke_expand_outlined` and `expand_appearance_in_safe_mode_leaves_a_dimension_selected_with_its_art` cover it. **Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views:** `live_trace_result_bounds_exclude_the_retained_bitmap_at_any_angle` checks every TraceView, geometric and visual bounds and rotated result bounds. `live_trace_selection_highlight_excludes_the_retained_source_rectangle` checks result highlights and the existing ordinary-group policy. Shared editing-child traversal also preserves Shaper handling. **Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards:** Shared existing preset definitions are reused; no external preset assets are added. Presets preserve each selected artboard's orientation, and orientation choices apply even when the active board already matches. Fit operations draft bounds until OK, affect only the active artboard and leave artwork in place. Regression coverage checks draft/Cancel, selected-set application, active-only fitting and Undo. From 660acb12ff8c07e5f3ba28192cf0d1e2e41bccb2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 21:09:12 -0800 Subject: [PATCH 18/44] Type: COLR radial gradients with a negative radius draw from their circle of radius 0 A radial gradient whose color line runs past 0 or 1 can get a circle with a negative radius once its stops are normalized to 0..1. Circles of a negative radius are not drawn: only the side of the circle of radius 0 (the tip of the cone) where the radius is positive is painted (https://learn.microsoft.com/en-us/typography/opentype/spec/colr, Radial gradients). Before: - As vector art, a padded gradient with a stop before 0 and its centers apart was focused on the start circle's center instead of the tip, and one with its centers together got stop offsets below 0. - Drawn into an image (a glyph with a sweep gradient, a Porter-Duff composite or a repeating gradient), any such gradient filled the glyph with its first stop's color. Both now recast the circles so neither radius is negative. With pad, the stops on the side that is not drawn become one stop at the tip with the color there; stops at the tip stay, so a sharp change of color there keeps the color on the drawn side. With repeat and reflect, the circles move by whole periods of the color line to the drawn side. When no circle is drawn, nothing is painted. --- crates/text/src/colr.rs | 107 ++++++++++++++++++++++++++++++-- crates/text/src/tests/colr.rs | 111 +++++++++++++++++++++++++++++++++- 2 files changed, 211 insertions(+), 7 deletions(-) diff --git a/crates/text/src/colr.rs b/crates/text/src/colr.rs index a3c6bf6e80..8e7a203ac0 100644 --- a/crates/text/src/colr.rs +++ b/crates/text/src/colr.rs @@ -306,13 +306,13 @@ impl<'a> Tree<'a> { self.unsupported.get_or_insert("a repeating or reflecting gradient"); return None; } - let (c0, c1, r0, r1) = (point(*c0), point(*c1), f64::from(*r0), f64::from(*r1)); + let (c0, r0, c1, r1, mut stops) = + non_negative_radii(point(*c0), f64::from(*r0), point(*c1), f64::from(*r1), stops(color_stops, self), *extend)?; let concentric = (c0 - c1).hypot() < 1e-6; if (r0 > 1e-6 && !concentric) || r1 <= r0 || (c0 - c1).hypot() >= r1 { self.unsupported.get_or_insert("a radial gradient from a circle"); return None; } - let mut stops = stops(color_stops, self); if stops.iter().any(|s| !(0.0..=1.0).contains(&s.offset)) { self.unsupported.get_or_insert("a radial gradient with stops past its circles"); return None; @@ -357,6 +357,87 @@ fn normalized(mut stops: Vec) -> Option<(Vec, f32, f Some((stops, first, if span > 0.0 { last } else { first + 1.0 })) } +/// A radial gradient's circles (`c0`, `r0`) and (`c1`, `r1`) and color line `stops` (0 and 1 at +/// the circles) recast so that neither circle has a negative radius, as normalizing a color line +/// to 0..1 can give. Circles of a negative radius are not drawn, only those on the side of the +/// circle of radius 0 (the tip of the cone) where the radius is positive. With pad, the stops on +/// the other side become one stop at the tip with the color there; with repeat and reflect, the +/// circles move along by whole periods of the color line to the drawn side +/// (https://learn.microsoft.com/en-us/typography/opentype/spec/colr, Radial gradients). `None` +/// when no circle is drawn. +fn non_negative_radii( + c0: Point, + r0: f64, + c1: Point, + r1: f64, + mut stops: Vec, + extend: Extend, +) -> Option<(Point, f64, Point, f64, Vec)> { + if (r0 >= 0.0 && r1 >= 0.0) || stops.is_empty() { + return Some((c0, r0, c1, r1, stops)); + } + if r0 == r1 { + return None; + } + stops.sort_by(|a, b| a.offset.total_cmp(&b.offset)); + let tip = r0 / (r0 - r1); + let grows = r1 > r0; + let (from, to, stops) = match extend { + Extend::Repeat | Extend::Reflect => { + // A reflected color line repeats every 2: once forward, then back. + let (period, stops) = if extend == Extend::Reflect { + let forward = stops.iter().map(|s| GradientStop { offset: s.offset / 2.0, ..s.clone() }); + let back = stops.iter().rev().map(|s| GradientStop { offset: 1.0 - s.offset / 2.0, ..s.clone() }); + (2.0, forward.chain(back).collect()) + } else { + (1.0, stops) + }; + let from = if grows { (tip / period).ceil() * period } else { (tip / period).floor() * period - period }; + (from, from + period, stops) + } + _ => { + // The color line runs from 0 to 1, and further where its stops do. + let first = stops.first().map_or(0.0, |s| f64::from(s.offset).min(0.0)); + let last = stops.last().map_or(1.0, |s| f64::from(s.offset).max(1.0)); + let (from, to) = match (grows, tip) { + (true, t) if t < last => (t, last), + (true, t) => (t, t + 1.0), + (false, t) if t > first => (first, t), + (false, t) => (t - 1.0, t), + }; + let line = Gradient { stops, ..Gradient::default() }; + // The color at `w`, as a stop at `offset` of the new line. + let at = |w: f64, offset: f32| { + let (color, opacity) = line.sample(w as f32); + GradientStop { opacity, ..GradientStop::new(offset, color) } + }; + // Stops at the tip stay on the drawn side of its stop, so a sharp change of color there + // keeps the color on that side. + let mut kept: Vec = line + .stops + .iter() + .filter(|s| (from - 1e-6..=to + 1e-6).contains(&f64::from(s.offset))) + .map(|s| GradientStop { offset: ((f64::from(s.offset) - from) / (to - from)).clamp(0.0, 1.0) as f32, ..s.clone() }) + .collect(); + if grows { + kept.insert(0, at(tip, 0.0)); + if kept.last().is_none_or(|s| s.offset < 1.0) { + kept.push(at(to, 1.0)); + } + } else { + kept.push(at(tip, 1.0)); + if kept.first().is_none_or(|s| s.offset > 0.0) { + kept.insert(0, at(from, 0.0)); + } + } + (from, to, kept) + } + }; + let circle = |w: f64| (c0.lerp(c1, w), (r0 + w * (r1 - r0)).max(0.0)); + let ((c0, r0), (c1, r1)) = (circle(from), circle(to)); + Some((c0, r0, c1, r1, stops)) +} + impl ColorPainter for Tree<'_> { fn push_transform(&mut self, transform: Transform) { let t = self.transform() * affine(&transform); @@ -549,10 +630,24 @@ impl ColorPainter for Pixels<'_, '_> { Brush::LinearGradient { p0, p1, color_stops, extend: e } => { peniko::Gradient::new_linear(point(p0), point(p1)).with_stops(stops(color_stops).as_slice()).with_extend(extend(e)).into() } - Brush::RadialGradient { c0, r0, c1, r1, color_stops, extend: e } => peniko::Gradient::new_two_point_radial(point(c0), r0, point(c1), r1) - .with_stops(stops(color_stops).as_slice()) - .with_extend(extend(e)) - .into(), + Brush::RadialGradient { c0, r0, c1, r1, color_stops, extend: e } => { + let line = color_stops + .iter() + .map(|s| { + let (c, a) = colour(&self.palette, self.foreground, s.palette_index, s.alpha).unwrap_or((Color::BLACK, 0.0)); + GradientStop { opacity: a, ..GradientStop::new(s.offset, c) } + }) + .collect(); + let Some((c0, r0, c1, r1, line)) = non_negative_radii(point(c0), f64::from(r0), point(c1), f64::from(r1), line, e) else { return }; + let stops: Vec = line + .iter() + .map(|s| { + let [r, g, b, a] = s.color.to_rgba8(s.opacity); + peniko::ColorStop::from((s.offset, peniko::Color::from_rgba8(r, g, b, a))) + }) + .collect(); + peniko::Gradient::new_two_point_radial(c0, r0 as f32, c1, r1 as f32).with_stops(stops.as_slice()).with_extend(extend(e)).into() + } Brush::SweepGradient { c0, start_angle, end_angle, color_stops, extend: e } => { peniko::Gradient::new_sweep(point(c0), start_angle.to_radians(), end_angle.to_radians()) .with_stops(stops(color_stops).as_slice()) diff --git a/crates/text/src/tests/colr.rs b/crates/text/src/tests/colr.rs index 212882ce7a..1516596f26 100644 --- a/crates/text/src/tests/colr.rs +++ b/crates/text/src/tests/colr.rs @@ -25,7 +25,7 @@ use vectorcraft_doc::color::{BlendMode, Color, GradientKind, Paint}; use vectorcraft_doc::{Node, NodeKind}; use crate::colr::{ColourContent, colour_glyph}; -use crate::test_fonts::{COLR_GREEN, COLR_RED}; +use crate::test_fonts::{COLR_BLUE, COLR_GREEN, COLR_RED}; const FOREGROUND: Color = Color::Rgb { r: 0.25, g: 0.5, b: 0.75 }; @@ -362,6 +362,115 @@ 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 padded radial gradient whose color line starts before 0 starts at the circle of radius 0: +/// circles of a negative radius are not drawn, so the stops before that circle become one stop +/// there with the color between them +/// (https://learn.microsoft.com/en-us/typography/opentype/spec/colr, Radial gradients). With its +/// centers apart, the gradient is focused on the tip of its cone; with its centers together, it +/// is centered there with no stop below 0. +#[test] +fn a_radial_gradient_with_a_stop_before_0_starts_at_its_circle_of_radius_0() { + use vectorcraft_doc::color::{Gradient, GradientStop}; + use write_fonts::tables::colr::{ColorLine, ColorStop, Extend}; + use write_fonts::types::{F2Dot14, FWord, UfWord}; + let one = F2Dot14::from_f32(1.0); + let half = F2Dot14::from_f32(0.5); + let line = ColorLine::new(Extend::Pad, 2, vec![ColorStop::new(F2Dot14::from_f32(-0.5), 0, half), ColorStop::new(one, 1, one)]); + let w = FWord::new; + // Half-opaque red at -0.5 and blue at 1: at 0, a third of the way from red to blue, in color + // and opacity. + let red = GradientStop { opacity: 0.5, ..GradientStop::new(0.0, rgb(COLR_RED)) }; + let tip = Gradient { stops: vec![red, GradientStop::new(1.0, rgb(COLR_BLUE))], ..Gradient::default() }.sample(1.0 / 3.0); + assert!((tip.1 - 2.0 / 3.0).abs() < 1e-6, "{tip:?}"); + for (family, c1_x, focal) in [("Colrtest Tip", 250, Some(300.0)), ("Colrtest Dot", 300, None)] { + let font = v1_font(family, COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(300), w(300), UfWord::new(0), w(c1_x), w(300), UfWord::new(400)), o) + }); + let root = art(&font, COLOUR_CHAR); + let Paint::Gradient(g) = fill(&children(&root)[0]) else { panic!("{family}: a gradient") }; + let offsets: Vec = g.gradient.stops.iter().map(|s| s.offset).collect(); + assert_eq!(offsets, [0.0, 1.0], "{family}"); + let first = &g.gradient.stops[0]; + assert_eq!((first.color, first.opacity), tip, "{family}: the color at the circle of radius 0"); + assert_eq!(g.gradient.stops[1].color, rgb(COLR_BLUE), "{family}"); + let geom = g.geom.unwrap(); + assert!((geom.start.x - f64::from(c1_x)).abs() < 1e-3 && (geom.start.y + 300.0).abs() < 1e-3, "{family}: {geom:?}"); + assert!(((geom.end - geom.start).hypot() - 400.0).abs() < 1e-3, "{family}: {geom:?}"); + assert_eq!(geom.focal.map(|f| ((f.x * 1e3).round() / 1e3, (f.y * 1e3).round() / 1e3)), focal.map(|x| (x, -300.0)), "{family}"); + } +} + +/// A sharp change of color at the circle of radius 0 of a gradient whose color line starts +/// before 0 keeps the color beyond it: red up to 0, then green fading to blue. +#[test] +fn a_sharp_change_of_color_at_the_circle_of_radius_0_keeps_the_color_beyond_it() { + use write_fonts::tables::colr::{ColorLine, ColorStop, Extend}; + use write_fonts::types::{F2Dot14, FWord, UfWord}; + let (one, zero) = (F2Dot14::from_f32(1.0), F2Dot14::from_f32(0.0)); + let stops = vec![ + ColorStop::new(F2Dot14::from_f32(-1.0), 0, one), + ColorStop::new(zero, 0, one), + ColorStop::new(zero, 2, one), + ColorStop::new(one, 1, one), + ]; + let line = ColorLine::new(Extend::Pad, 4, stops); + let w = FWord::new; + let font = v1_font("Colrtest Edge", COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(300), w(300), UfWord::new(0), w(250), w(300), UfWord::new(400)), o) + }); + let root = art(&font, COLOUR_CHAR); + let Paint::Gradient(g) = fill(&children(&root)[0]) else { panic!("a gradient") }; + let stops = &g.gradient.stops; + let colors: Vec = stops.iter().map(|s| s.color).collect(); + assert_eq!(colors, [rgb(COLR_RED), rgb(COLR_RED), rgb(COLR_GREEN), rgb(COLR_BLUE)], "{stops:?}"); + assert!(stops[..3].iter().all(|s| s.offset.abs() < 1e-5) && stops[3].offset == 1.0, "{stops:?}"); +} + +/// A glyph drawn into an image draws a radial gradient with a circle of a negative radius from the +/// circle of radius 0, as vector art does: with pad, the color at the tip of the cone and the +/// colors on its drawn side, not one solid color. A composite that keeps only its backdrop makes +/// the glyph an image. Red runs to blue; the circles are around the `O`'s middle and larger than it. +#[test] +fn an_image_of_a_radial_gradient_with_a_stop_past_its_circle_of_radius_0_draws_from_that_circle() { + use write_fonts::tables::colr::{ColorLine, ColorStop, CompositeMode, Extend}; + use write_fonts::types::{F2Dot14, FWord, UfWord}; + let one = F2Dot14::from_f32(1.0); + let w = FWord::new; + type Check = fn([u8; 3]) -> bool; + // Red at -0.5 (a start circle of radius -250 once normalized): the colors run from a third of + // the way to blue at the tip to blue. Blue at 1.5 (an end circle of radius -250): they run + // from red at the outer circle, which lies outside the `O`, to two thirds of the way to blue + // at the tip. Either way every opaque pixel holds some blue. + let cases: [(&str, f32, f32, u16, u16, Check); 2] = + [("Colrtest Tip Image", -0.5, 1.0, 0, 500, |[r, _, b]| b > 20 && r < 240), ("Colrtest Rim Image", 0.0, 1.5, 500, 0, |[_, _, b]| b > 20)]; + for (family, first, last, r0, r1, check) in cases { + let line = + ColorLine::new(Extend::Pad, 2, vec![ColorStop::new(F2Dot14::from_f32(first), 0, one), ColorStop::new(F2Dot14::from_f32(last), 1, one)]); + let font = v1_font(family, COLOUR_CHAR, |o| { + let radial = ColrPaint::radial_gradient(line.clone(), w(290), w(330), UfWord::new(r0), w(290), w(330), UfWord::new(r1)); + ColrPaint::composite(ColrPaint::glyph(solid(2), o), CompositeMode::Dest, ColrPaint::glyph(radial, o)) + }); + let Some(ColourContent::Image(image)) = content(&font, COLOUR_CHAR) else { panic!("{family}: an image") }; + let pixmap = vello_cpu::Pixmap::from_png(std::io::Cursor::new(image.blob.bytes.as_slice())).unwrap(); + let pixels: Vec<[u8; 3]> = pixmap.take_unpremultiplied().into_iter().filter(|p| p.a == 255).map(|p| [p.r, p.g, p.b]).collect(); + assert!(pixels.len() > 100, "{family}: the `O` is drawn"); + let wrong: Vec<_> = pixels.iter().filter(|p| !check(**p)).take(5).collect(); + assert!(wrong.is_empty(), "{family}: {wrong:?}"); + } + // Repeat and reflect draw their bands, not one color. + for (family, extend) in [("Colrtest Repeat Image", Extend::Repeat), ("Colrtest Reflect Image", Extend::Reflect)] { + let line = ColorLine::new(extend, 2, vec![ColorStop::new(F2Dot14::from_f32(-0.5), 0, one), ColorStop::new(one, 1, one)]); + let font = v1_font(family, COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(290), w(330), UfWord::new(0), w(290), w(330), UfWord::new(100)), o) + }); + let Some(ColourContent::Image(image)) = content(&font, COLOUR_CHAR) else { panic!("{family}: an image") }; + let pixmap = vello_cpu::Pixmap::from_png(std::io::Cursor::new(image.blob.bytes.as_slice())).unwrap(); + let colors: std::collections::HashSet<[u8; 3]> = + pixmap.take_unpremultiplied().into_iter().filter(|p| p.a == 255).map(|p| [p.r, p.g, p.b]).collect(); + assert!(colors.len() > 10, "{family}: {colors:?}"); + } +} + /// 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() { From a98b8007616177d32197fe9a666942e5e7428045 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 22:31:48 -0800 Subject: [PATCH 19/44] Export: live effects in symbol art apply in PDF, EPS, print and DXF PDF, print, EPS and DXF wrote a symbol instance by placing its symbol's art as stored, so live effects inside that art were missing: a square moved 100 pt by a Transform effect stayed in place in the file while the canvas drew it moved. bake_document_placing_symbols replaces each instance of a symbol whose art (or the art of a symbol nested in it) needs baking by that art placed as the instance places it, then baked. The art is placed before its effects apply, as on the canvas, so an instance that scales its art does not scale what its effects do. A dynamic instance's own paint changes apply before baking. Nesting stops at 32 levels and the placed art at 16,000,000 objects per export; past them an instance stays an instance. DXF places 9-slice cells unclipped, as its scene does. The symbols' own art, SVG and the .ai editing data are unchanged, and HPGL already baked each placed instance. --- crates/cad/src/lib.rs | 3 +- crates/cad/src/tests.rs | 23 +++++ crates/effects/src/bake.rs | 139 ++++++++++++++++++++++++++++++- crates/effects/src/bake/tests.rs | 107 ++++++++++++++++++++++++ crates/effects/src/lib.rs | 4 +- crates/effects/src/reshape.rs | 2 +- crates/eps/src/lib.rs | 2 +- crates/eps/src/tests.rs | 17 ++++ crates/pdf/src/export/entry.rs | 5 +- crates/pdf/src/print.rs | 2 +- crates/pdf/src/tests.rs | 20 +++++ crates/pdf/src/tests/print.rs | 23 +++++ docs/target-app-parity.md | 1 + 13 files changed, 337 insertions(+), 11 deletions(-) diff --git a/crates/cad/src/lib.rs b/crates/cad/src/lib.rs index 66a6a36baa..d459f95b40 100644 --- a/crates/cad/src/lib.rs +++ b/crates/cad/src/lib.rs @@ -428,7 +428,8 @@ pub fn export(doc: &Document, opts: &DxfOptions) -> Result { if ![r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()) { return Err("the export region is not finite".into()); } - let baked = vectorcraft_effects::bake_document(doc); + // Symbols' art is placed without clipping, as the scene places it. + let baked = vectorcraft_effects::bake_document_placing_symbols(doc, false); let doc = baked.as_ref().unwrap_or(doc); Ok(scene::Scene::new(doc, opts).run()) } diff --git a/crates/cad/src/tests.rs b/crates/cad/src/tests.rs index 96c037a4a7..fe1e909743 100644 --- a/crates/cad/src/tests.rs +++ b/crates/cad/src/tests.rs @@ -1395,6 +1395,29 @@ fn crop_keeps_an_instance_whose_art_reaches_the_region() { assert!(!kinds(&export(&d, &o).unwrap()).is_empty()); } +#[test] +fn symbol_art_effects_are_written_where_they_draw() { + // A line and the symbol's line below it, each moved 100 pt right by a Transform effect, start + // at the same x. + let moved = |mut n: Node| { + n.appearance.effects.push(vectorcraft_doc::Effect { + id: "distort.transform".into(), + params: serde_json::json!({"moveH": 100.0}), + visible: true, + }); + n + }; + let mut d = doc_with(|d| { + let line = path(d, shapes::line((10.0, 10.0).into(), (20.0, 10.0).into()), Paint::None, Paint::solid(Color::BLACK), 1.0); + vec![moved(line), Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Moved".into(), xf: Affine::IDENTITY })] + }); + let art = path(&mut d, shapes::line((10.0, 40.0).into(), (20.0, 40.0).into()), Paint::None, Paint::solid(Color::BLACK), 1.0); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Moved".into(), art: Arc::new(moved(art)) }); + let starts: Vec = entities(&export(&d, &opts(&d)).unwrap()).iter().map(|e| all(e, 10).into_iter().fold(f64::INFINITY, f64::min)).collect(); + assert_eq!(starts.len(), 2, "{starts:?}"); + assert!((starts[0] - starts[1]).abs() < 1e-6, "{starts:?}"); +} + #[test] fn symbols_fanning_out_into_empty_symbols_stop_at_the_symbol_budget() { // Five levels of 100 instances of the level below, ending in an empty symbol: 10^10 empty diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 7e42147443..d0cf98af26 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -17,7 +17,8 @@ //! object of its own, a stroke a path stroked by it alone (brushed strokes their brush art, made by //! the caller). -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; use std::sync::Arc; use vectorcraft_doc::color::Paint; @@ -26,7 +27,7 @@ use vectorcraft_geom::{Affine, FillRule, PathData, Rect, Vec2}; use crate::distort::{transform_move, transform_offsets}; use crate::group::{has_own_paint, paints}; -use crate::reshape::{as_art, geometry_effects, glyph_outlines, outline_nested_text, reshape_leaves}; +use crate::reshape::{as_art, dressed, 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, @@ -113,13 +114,105 @@ thread_local! { /// The objects of symbol art that [`bake_document`] has baked for instances inside the art of /// other instances, counted against [`crate::SYMBOL_ART_MAX_NODES`]. static NESTED_NODES: std::cell::Cell = const { std::cell::Cell::new(0) }; + /// While [`bake_document_placing_symbols`] bakes: the symbols whose instances become their + /// placed art ([`placed_symbol_art`]). + static PLACING: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; + /// The objects of symbol art that [`placed_symbol_art`] has placed for one export, counted + /// against [`vectorcraft_doc::MAX_SYMBOL_NODES`]. + static PLACED_NODES: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// The symbols whose instances [`bake_document_placing_symbols`] replaces by their placed art, and +/// how that art is placed through 9-slice guides. +struct Placing { + /// The names of the symbols whose art draws something that baking changes, directly or through + /// the instances of other such symbols in it ([`drawn_symbols`]). + symbols: HashSet, + /// Whether 9-slice cells are clipped ([`Document::place_symbol_art`]) or not + /// ([`Document::place_symbol_art_unclipped`]). + clip_slices: bool, +} + +/// The names of the symbols of `d` whose art needs baking ([`needs_bake`]), and of those whose art +/// holds an instance of one of them at any depth. +fn drawn_symbols(d: &Document) -> HashSet { + let mut out: HashSet = d.symbols.iter().filter(|s| needs_bake(&s.art)).map(|s| s.name.clone()).collect(); + if out.is_empty() { + return out; + } + let uses: Vec<(&str, Vec)> = d + .symbols + .iter() + .filter(|s| !out.contains(&s.name)) + .map(|s| { + let mut names = vec![]; + s.art.walk(&mut |c| { + if let NodeKind::SymbolInstance { symbol, .. } = &c.kind { + names.push(symbol.clone()); + } + }); + (s.name.as_str(), names) + }) + .collect(); + loop { + let before = out.len(); + for (name, names) in &uses { + if !out.contains(*name) && names.iter().any(|x| out.contains(x)) { + out.insert((*name).to_string()); + } + } + if out.len() == before { + return out; + } + } +} + +/// Does `n`'s subtree hold an instance of a symbol that [`bake_document_placing_symbols`] replaces +/// by its placed art? False outside it. +fn holds_placed_symbol(n: &Node) -> bool { + fn walk(n: &Node, symbols: &HashSet) -> bool { + matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbols.contains(symbol)) + || n.children().is_some_and(|ch| ch.iter().any(|c| walk(c, symbols))) + } + PLACING.with(|p| p.borrow().as_ref().is_some_and(|p| walk(n, &p.symbols))) +} + +/// Symbol instance `n`, while [`bake_document_placing_symbols`] bakes and its symbol is one it +/// places, as the art it draws: the symbol's art with the instance's own appearance changes +/// ([`vectorcraft_doc::symbol_overrides::dynamic_art`]), placed by the instance as the exporters +/// place it, in a group with the instance's id, name, visibility, transparency, opacity mask and +/// raster effects. The art is placed before its effects are baked, as the canvas draws it, so an +/// instance that scales its art does not scale what its effects do. `None` for other objects, +/// for instances nested [`vectorcraft_doc::depth::MAX_DEPTH`] deep in such art, and once the art +/// placed for one export would pass [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects; those +/// instances stay as they are. +fn placed_symbol_art(d: &Document, n: &Node) -> Option { + let NodeKind::SymbolInstance { symbol, xf } = &n.kind else { return None }; + let clip = PLACING.with(|p| p.borrow().as_ref().filter(|p| p.symbols.contains(symbol)).map(|p| p.clip_slices))?; + if INSTANCE_DEPTH.with(|c| c.get()) >= vectorcraft_doc::depth::MAX_DEPTH { + return None; + } + let sym = d.symbols.iter().find(|s| s.name == *symbol)?; + let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n); + let most = match d.symbol_slice_map(symbol, *xf) { + Some(m) if clip => m.most_placed(&art, d), + Some(m) => m.most_placed_unclipped(&art, d), + None => art.count_with_masks() as u64, + }; + let spent = PLACED_NODES.with(|c| c.get()).saturating_add(most); + if spent > vectorcraft_doc::MAX_SYMBOL_NODES { + return None; + } + PLACED_NODES.with(|c| c.set(spent)); + let placed = if clip { d.place_symbol_art(symbol, &art, *xf) } else { d.place_symbol_art_unclipped(symbol, &art, *xf) }?; + Some(dressed(Node::group(n.id, vec![Arc::new(placed)]), n)) } /// `n` with its geometry effects evaluated (`None` = unchanged). fn bake_node(d: &mut Document, n: &Node) -> Option { // A cancelled export leaves the rest of the document as it is: the caller drops the result. // A ruler guide draws nothing and stays as it is, last in its layer. - if n.is_ruler_guide() || !needs_bake(n) || vectorcraft_doc::cancel::is_cancelled() { + if n.is_ruler_guide() || !(needs_bake(n) || holds_placed_symbol(n)) || vectorcraft_doc::cancel::is_cancelled() { return None; } // Colour adjustments: the object recoloured (inside it too), then baked as it is. @@ -176,6 +269,14 @@ fn bake_node(d: &mut Document, n: &Node) -> Option { } return Some(baked.unwrap_or(m)); } + // Symbol instances whose art draws live effects, when the caller asks for them: the art + // placed, then baked. + if let Some(m) = placed_symbol_art(d, n) { + INSTANCE_DEPTH.with(|c| c.set(c.get() + 1)); + let baked = bake_node(d, &m); + INSTANCE_DEPTH.with(|c| c.set(c.get() - 1)); + return Some(baked.unwrap_or(m)); + } // A path whose Transform effect copies it with a gradient, or scales its strokes or moves its // patterns: each copy an object of its own, so its gradient fits it rather than spanning // every copy and its strokes and patterns can differ from the others'. @@ -693,8 +794,38 @@ fn bake_appearance_pieces(doc: Option<&Document>, n: &mut Node, outline_strokes: /// resources of its placed documents' art added ([`Document::with_placed_art`]); `None` when /// there is nothing to do (export it as is). pub fn bake_document(doc: &Document) -> Option { + bake(doc, None) +} + +/// [`bake_document`], and every symbol instance whose symbol's art draws something baking changes +/// (live geometry effects, color adjustments and the rest [`needs_bake`] counts, in its own art or +/// in the art of the symbols it places) replaced by that art as the instance places it, baked +/// ([`placed_symbol_art`]), so the art draws as on the canvas wherever the instance is. For +/// exporters that write each instance's art in its place (PDF, print, EPS, DXF), rather than +/// writing each symbol once. `clip_slices`: whether 9-slice cells are clipped +/// ([`Document::place_symbol_art`]) or not ([`Document::place_symbol_art_unclipped`]), as the +/// exporter places other instances. The symbols' own art is left as it is. +pub fn bake_document_placing_symbols(doc: &Document, clip_slices: bool) -> Option { + bake(doc, Some(clip_slices)) +} + +/// Restores what [`bake`] changes in the thread's baking state when it ends. +struct Restore(Option>, u64); + +impl Drop for Restore { + fn drop(&mut self) { + PLACING.with(|p| *p.borrow_mut() = self.0.take()); + PLACED_NODES.with(|c| c.set(self.1)); + } +} + +/// [`bake_document`], or with `placing` [`bake_document_placing_symbols`] with that `clip_slices`. +fn bake(doc: &Document, placing: Option) -> Option { let placed = doc.with_placed_art(); - if !placed.layers.iter().any(|l| needs_bake(l)) { + let placing = placing.map(|clip_slices| Placing { symbols: drawn_symbols(&placed), clip_slices }).filter(|p| !p.symbols.is_empty()); + // A bake inside another one (of art an effect makes) places symbols only when it asks to. + let _restore = Restore(PLACING.with(|p| p.replace(placing.map(Rc::new))), PLACED_NODES.with(|c| c.replace(0))); + if !placed.layers.iter().any(|l| needs_bake(l) || holds_placed_symbol(l)) { return match placed { std::borrow::Cow::Owned(d) => Some(d), std::borrow::Cow::Borrowed(_) => None, diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs index 2bcc026340..72d210f6f3 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -1025,3 +1025,110 @@ fn exporting_a_path_with_a_transform_gives_each_copy_its_own_gradient() { // Three squares of their own, not one path spanning all three. assert_eq!(gradient_pieces(d.node(NodeId(50)).unwrap()), vec![10.0; 3]); } + +/// A 10 × 10 square at (10, 40), with id `id`, that a Transform effect moves 100 pt right. +fn moved_square(id: u64) -> Node { + let mut n = Node::path(NodeId(id), shapes::rectangle(Rect::new(10.0, 40.0, 20.0, 50.0)), Appearance::default_art()); + n.appearance.effects.push(fx("distort.transform", json!({"moveH": 100.0}))); + n +} + +fn instance(id: u64, symbol: &str, xf: vectorcraft_geom::Affine) -> Node { + Node::new(NodeId(id), NodeKind::SymbolInstance { symbol: symbol.into(), xf }) +} + +fn add_symbol(d: &mut Document, name: &str, art: Node) { + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: name.into(), art: Arc::new(art) }); +} + +fn bounds(d: &Document, id: u64) -> Rect { + d.node(NodeId(id)).and_then(|n| n.geometric_bounds()).unwrap_or_else(|| panic!("node {id} has bounds")) +} + +fn near_rect(a: Rect, b: Rect) -> bool { + [(a.x0, b.x0), (a.y0, b.y0), (a.x1, b.x1), (a.y1, b.y1)].iter().all(|(x, y)| (x - y).abs() < 1e-6) +} + +/// Each instance draws its symbol's art with the art's effects applied where the instance places +/// it: an instance that doubles the art's size moves it the effect's 100 pt, as the canvas draws +/// it, not 200 pt. Plain baking leaves the instances and the symbols' art as they are. +#[test] +fn placing_symbols_bakes_their_art_where_each_instance_places_it() { + use vectorcraft_geom::Affine; + let mut d = doc_with(instance(900, "S", Affine::IDENTITY)); + let layer = d.layers[0].id; + d.insert(Some(layer), 0, instance(901, "S", Affine::translate((0.0, 100.0)) * Affine::scale(2.0))).unwrap(); + add_symbol(&mut d, "S", moved_square(800)); + assert!(bake_document(&d).is_none(), "nothing in the layers needs baking"); + let out = bake_document_placing_symbols(&d, true).unwrap(); + assert!(matches!(out.node(NodeId(900)).unwrap().kind, NodeKind::Group { .. })); + assert!(near_rect(bounds(&out, 900), Rect::new(110.0, 40.0, 120.0, 50.0)), "{:?}", bounds(&out, 900)); + assert!(near_rect(bounds(&out, 901), Rect::new(120.0, 180.0, 140.0, 200.0)), "{:?}", bounds(&out, 901)); + assert_eq!(out.symbols[0].art.appearance.effects.len(), 1, "the symbol's art keeps its effect"); +} + +/// An instance of a symbol whose art holds an instance of another symbol with effects draws that +/// art with its effects too. +#[test] +fn placing_symbols_reaches_the_art_of_nested_instances() { + use vectorcraft_geom::Affine; + let mut d = doc_with(instance(900, "Outer", Affine::IDENTITY)); + add_symbol(&mut d, "Inner", moved_square(800)); + add_symbol(&mut d, "Outer", Node::group(NodeId(801), vec![Arc::new(instance(802, "Inner", Affine::translate((0.0, 100.0))))])); + let out = bake_document_placing_symbols(&d, true).unwrap(); + assert!(near_rect(bounds(&out, 900), Rect::new(110.0, 140.0, 120.0, 150.0)), "{:?}", bounds(&out, 900)); +} + +/// A symbol whose art holds an instance of itself is placed until the depth limit, and the export +/// ends. +#[test] +fn placing_a_symbol_that_holds_itself_stops() { + use vectorcraft_geom::Affine; + let mut d = doc_with(instance(900, "Loop", Affine::IDENTITY)); + add_symbol( + &mut d, + "Loop", + Node::group(NodeId(801), vec![Arc::new(moved_square(800)), Arc::new(instance(802, "Loop", Affine::translate((0.0, 1.0))))]), + ); + let out = bake_document_placing_symbols(&d, true).unwrap(); + let mut lefts = vec![]; + out.node(NodeId(900)).unwrap().walk(&mut |n| lefts.extend(n.path_data().and_then(|p| p.bounds()).map(|b| b.x0))); + assert!(lefts.len() > 1 && lefts.iter().all(|x| (x - 110.0).abs() < 1e-6), "{lefts:?}"); + let mut depth = 0; + let mut n = out.node(NodeId(900)).unwrap(); + while let Some(next) = n.children().and_then(|ch| ch.last()) { + n = next; + depth += 1; + } + assert!(depth <= 3 * (vectorcraft_doc::depth::MAX_DEPTH + 1), "{depth}"); +} + +/// A dynamic symbol instance's own paint for an object of the art applies before the object's +/// effects are baked: a fill whose own Transform effect makes the square a group of pieces still +/// paints in the instance's red, and its sibling keeps the symbol's paint. +#[test] +fn placing_symbols_keeps_dynamic_instances_changes() { + use vectorcraft_geom::Affine; + let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0)); + let mut square = Node::path(NodeId(800), shapes::rectangle(Rect::new(10.0, 40.0, 20.0, 50.0)), Appearance::default_art()); + if let Some(AppearanceItem::Fill(f)) = square.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))) { + f.effects.push(fx("distort.transform", json!({"moveH": 100.0}))); + } + let sibling = Node::path(NodeId(801), shapes::rectangle(Rect::new(10.0, 60.0, 20.0, 70.0)), Appearance::default_art()); + let mut inst = instance(900, "D", Affine::IDENTITY); + inst.extras_mut().symbol_overrides = vec![vectorcraft_doc::SymbolOverride { path: vec![0], fill: Some(red.clone()), ..Default::default() }]; + let mut d = doc_with(inst); + add_symbol(&mut d, "D", Node::group(NodeId(802), vec![Arc::new(square), Arc::new(sibling)])); + d.symbols[0].dynamic = true; + let out = bake_document_placing_symbols(&d, true).unwrap(); + let mut fills = vec![]; + out.node(NodeId(900)).unwrap().walk(&mut |n| { + if n.path_data().is_some() + && let Some(b) = n.geometric_bounds() + { + fills.push((b.x0.round(), b.y0.round(), n.appearance.fill_paint())); + } + }); + assert!(fills.contains(&(110.0, 40.0, red)), "{fills:?}"); + assert!(fills.contains(&(10.0, 60.0, Paint::solid(Color::WHITE))), "{fills:?}"); +} diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 6316a7a273..fb11a64161 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -68,7 +68,9 @@ pub use adjust::{ ADJUSTMENTS, CHANNELS, ColorMap, ImageHook, adjust, adjust_in_document, color_map, curve_at, curve_points, has_adjustment, is_adjustment, is_channel, }; -pub use bake::{StrokeArt, bake_appearance, bake_document, bake_type_appearance, expand_art, expand_leaf, fresh_ids, needs_bake}; +pub use bake::{ + StrokeArt, bake_appearance, bake_document, bake_document_placing_symbols, bake_type_appearance, expand_art, expand_leaf, fresh_ids, needs_bake, +}; pub use clip::{WarpSeams, clip_outline, knockout_silhouette, warp_pieces_translucent}; pub use group::{ OutlineHook, PATHFINDER_EFFECTS, compound_shape_area, compound_shape_art, evaluate_container, has_container_appearance, has_pathfinder, diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index 8c829d9e0b..88df54fe5b 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -845,7 +845,7 @@ fn as_art_opts( /// `art`, the art made of `n`, with `n`'s name, visibility, transparency, opacity mask and /// remaining (raster) effects. -fn dressed(mut art: Node, n: &Node) -> Node { +pub(crate) fn dressed(mut art: Node, n: &Node) -> Node { art.name = n.name.clone(); art.visible = n.visible; art.opacity = n.opacity; diff --git a/crates/eps/src/lib.rs b/crates/eps/src/lib.rs index 49b64f4bce..2dbadf4fea 100644 --- a/crates/eps/src/lib.rs +++ b/crates/eps/src/lib.rs @@ -230,7 +230,7 @@ pub fn export(doc: &Document, opts: &EpsOptions, preview: Option<&Raster>, thumb } // Type in the document's composite fonts is drawn in them, whether or not an app has the document open. let _lent = vectorcraft_text::FontDb::global().lend_composite_fonts(&doc.composite_fonts); - let baked = vectorcraft_effects::bake_document(doc); + let baked = vectorcraft_effects::bake_document_placing_symbols(doc, true); let doc = baked.as_ref().unwrap_or(doc); let page = scene::Scene::new(doc, opts).run(); let mut setting_notes = vec![]; diff --git a/crates/eps/src/tests.rs b/crates/eps/src/tests.rs index c98cb9689a..22591bbe2b 100644 --- a/crates/eps/src/tests.rs +++ b/crates/eps/src/tests.rs @@ -1409,6 +1409,23 @@ fn an_instance_whose_brush_art_reaches_the_region_is_written() { assert!(page.contains("1 0 0 rg"), "{page}"); } +#[test] +fn symbol_art_effects_are_written_where_they_draw() { + // The symbol's square at (10, 40), which a Transform effect moves 100 pt right, is drawn from + // x = 110 and not from x = 10. + let mut d = doc_with(|d| vec![Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Moved".into(), xf: Affine::IDENTITY })]); + let mut art = path(&mut d, shapes::rectangle(Rect::new(10.0, 40.0, 20.0, 50.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0); + art.appearance.effects.push(vectorcraft_doc::Effect { + id: "distort.transform".into(), + params: serde_json::json!({"moveH": 100.0}), + visible: true, + }); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Moved".into(), art: Arc::new(art) }); + let page = page_of(&d, &opts(&d)); + let starts: Vec = page.lines().filter(|l| l.ends_with(" m")).filter_map(|l| l.split_whitespace().next()?.parse().ok()).collect(); + assert!(!starts.is_empty() && starts.iter().all(|x| (110.0..=120.0).contains(x)), "{starts:?}\n{page}"); +} + #[test] fn level_preview_and_overprint_ids_read_back() { for l in Level::ALL { diff --git a/crates/pdf/src/export/entry.rs b/crates/pdf/src/export/entry.rs index eabba52d9e..96dc97a6f8 100644 --- a/crates/pdf/src/export/entry.rs +++ b/crates/pdf/src/export/entry.rs @@ -45,8 +45,9 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha let set = &opts.settings; set.check()?; vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; - // Live geometry effects export as their result; raster effects are reported below. - let baked = vectorcraft_effects::bake_document(doc); + // Live geometry effects export as their result, in symbols' art too; raster effects are + // reported below. + let baked = vectorcraft_effects::bake_document_placing_symbols(doc, true); // Baking stops early when cancelled, leaving a half-baked copy that must not be written. vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; let doc = baked.as_ref().unwrap_or(doc); diff --git a/crates/pdf/src/print.rs b/crates/pdf/src/print.rs index 12c71756c8..57d30e327d 100644 --- a/crates/pdf/src/print.rs +++ b/crates/pdf/src/print.rs @@ -183,7 +183,7 @@ pub fn keep_layers(nodes: &mut Vec>, which: PrintLayers) { /// only the layers that print, overprints in composite output as [`PrintAdvanced::overprints`] /// says. pub fn printed_document<'a>(doc: &'a Document, set: &PrintSettings) -> Cow<'a, Document> { - let mut doc = vectorcraft_effects::bake_document(doc).map_or(Cow::Borrowed(doc), Cow::Owned); + let mut doc = vectorcraft_effects::bake_document_placing_symbols(doc, true).map_or(Cow::Borrowed(doc), Cow::Owned); let d = doc.to_mut(); keep_layers(&mut d.layers, set.print_layers); composite_overprints(d, set); diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index d63aaedb35..d888d7b4b1 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -246,6 +246,26 @@ fn an_instance_whose_brush_art_reaches_the_page_is_written() { assert!(close(l[0].geometric_bounds().unwrap(), Rect::new(-15.0, 30.0, 5.0, 70.0), 0.01), "{:?}", l[0].geometric_bounds()); } +/// The live effects in a symbol's art apply where the instance places the art, as on the canvas: +/// the symbol's square at (10, 40), which a Transform effect moves 100 pt right, is written at +/// (110, 40). +#[test] +fn symbol_art_effects_are_written_where_they_draw() { + let mut d = doc(200.0, 100.0); + let mut square = rect_node(Rect::new(10.0, 40.0, 20.0, 50.0), Color::rgb(1.0, 0.0, 0.0)); + square.appearance.effects.push(vectorcraft_doc::Effect { + id: "distort.transform".into(), + params: serde_json::json!({"moveH": 100.0}), + visible: true, + }); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Moved".into(), art: Arc::new(square) }); + add(&mut d, Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Moved".into(), xf: Affine::IDENTITY })); + let out = roundtrip(&d); + let l = leaves(&out); + assert_eq!(l.len(), 1); + assert!(close(l[0].geometric_bounds().unwrap(), Rect::new(110.0, 40.0, 120.0, 50.0), 0.01), "{:?}", l[0].geometric_bounds()); +} + /// An instance of a symbol with 9-slice guides, scaled to 200 × 150, is written as the canvas /// draws it: its 10 × 10 corner square stays 10 × 10 and its 10 × 10 center square becomes 30 × 20. #[test] diff --git a/crates/pdf/src/tests/print.rs b/crates/pdf/src/tests/print.rs index 5c5e1eb2c0..cefc255db9 100644 --- a/crates/pdf/src/tests/print.rs +++ b/crates/pdf/src/tests/print.rs @@ -451,6 +451,29 @@ fn a_symbol_instances_brush_art_keeps_its_artboard_printed() { assert!(p[0].1.iter().any(|(_, paint)| rgb8(paint) == [255, 0, 0]), "{p:?}"); } +#[test] +fn symbol_art_effects_print_where_they_draw() { + // A red square and the symbol's blue square, each moved 100 pt right by a Transform effect, + // print at the same distance from the page's left edge. + let moved = |mut n: Node| { + n.appearance.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params: json!({"moveH": 100.0}), visible: true }); + n + }; + let mut d = doc(200.0, 100.0, &[]); + let (layer, id) = (d.default_layer().unwrap(), d.alloc_id()); + let red = Node::path(id, shapes::rectangle(Rect::new(10.0, 10.0, 20.0, 20.0)), Appearance::basic(rgb(1.0, 0.0, 0.0), Paint::None, 0.0)); + d.insert(Some(layer), 0, moved(red)).unwrap(); + let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 40.0, 20.0, 50.0)), Appearance::basic(rgb(0.0, 0.0, 1.0), Paint::None, 0.0)); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Moved".into(), art: Arc::new(moved(art)) }); + let id = d.alloc_id(); + d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Moved".into(), xf: vectorcraft_geom::Affine::IDENTITY })).unwrap(); + let p = pages(&job(&d, json!({})).bytes); + assert_eq!(p.len(), 1); + let left = |c: [u8; 3]| p[0].1.iter().find(|(_, paint)| rgb8(paint) == c).map(|(r, _)| r.x0); + let (red, blue) = (left([255, 0, 0]), left([0, 0, 255])); + assert!(red.zip(blue).is_some_and(|(r, b)| (r - b).abs() < 0.05), "{p:?}"); +} + #[test] fn copies_collate_and_reverse() { let mut d = diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 296bd84b2b..06b745eec8 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1552,6 +1552,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. + Live effects inside a symbol's art (geometry effects, color adjustments and the rest that export bakes) are written where each instance places the art in PDF, print, EPS and DXF, as the canvas draws them: the art is placed first and its effects are applied after, so an instance that scales its art does not scale the distance a Transform effect moves it. Symbols nested in the art and a dynamic symbol instance's own paint changes are kept, a symbol whose art holds itself stops at the nesting limit, and the symbols' own art keeps its effects. Before, these exports wrote the art without its effects (effects `bake::placing_symbols_bakes_their_art_where_each_instance_places_it`, `placing_symbols_reaches_the_art_of_nested_instances`, `placing_a_symbol_that_holds_itself_stops`, `placing_symbols_keeps_dynamic_instances_changes`; PDF, print, EPS and DXF `symbol_art_effects_are_written_where_they_draw`, `symbol_art_effects_print_where_they_draw`). 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. From 897b4f0ecf22197a26f4bbadf0f04b041fd5f24f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 23:02:02 -0800 Subject: [PATCH 20/44] Export: placed symbol art counts its baked objects and drawn depth PDF, print, EPS and DXF exports now check the art of each placed symbol instance after its effects are baked. The objects it holds, including the copies a Transform effect makes, opacity mask art and the art of instances placed inside it (counted once), count against the export's 16,000,000-object budget. An instance whose baked art passes the budget, or would draw deeper than the 128 levels a document may draw, stays an instance. - doc: DrawnHeights measures how deep an object outside the layers draws, symbols and patterns it uses included. - effects: the bake tracks how deep each baked object sits, and a rejected candidate releases what it and the instances in it charged. --- crates/doc/src/depth.rs | 58 +++++++++++++++++++++++ crates/effects/src/bake.rs | 81 ++++++++++++++++++++++++-------- crates/effects/src/bake/tests.rs | 72 ++++++++++++++++++++++++++++ docs/target-app-parity.md | 2 +- 4 files changed, 193 insertions(+), 20 deletions(-) diff --git a/crates/doc/src/depth.rs b/crates/doc/src/depth.rs index 63b7e0ce7c..c846dfc1c6 100644 --- a/crates/doc/src/depth.rs +++ b/crates/doc/src/depth.rs @@ -251,6 +251,45 @@ fn undrawable(heights: &Heights, used: Def, depth: usize) -> bool { heights.get(&used).is_some_and(|h| h.is_none_or(|h| depth + h > MAX_DRAWN_DEPTH)) } +/// How deep the art of a document's symbols and patterns draws, to measure objects that use them +/// outside the document's layers ([`Self::deepest`]). +#[derive(Clone, Debug, Default)] +pub struct DrawnHeights { + symbols: HashMap, + patterns: HashMap, +} + +impl DrawnHeights { + /// The heights of `doc`'s symbols and patterns, as [`Document::too_deep`] counts them: the art + /// is 1 deep, and what it uses is counted under it. A symbol or pattern that uses itself, or + /// draws deeper than [`MAX_DRAWN_DEPTH`], has none. + pub fn of(doc: &Document) -> Self { + let mut out = Self::default(); + for ((pattern, name), h) in heights(doc) { + if let Some(h) = h { + let map = if pattern { &mut out.patterns } else { &mut out.symbols }; + map.insert(name.to_string(), h); + } + } + out + } + + /// The deepest `n`, placed `depth` deep, draws: its own objects and the art of their opacity + /// masks, and under the objects the art of the symbols and patterns they use. A use of a + /// symbol or pattern without a height ([`Self::of`]) counts as the object alone. `n`'s own + /// nesting may pass [`MAX_DEPTH`]. Without recursion. + pub fn deepest(&self, n: &Node, depth: usize) -> usize { + let mut out = depth; + let mut stack = vec![(n, depth)]; + while let Some((n, d)) = stack.pop() { + let used = uses(n).filter_map(|(pattern, name)| if pattern { self.patterns.get(name) } else { self.symbols.get(name) }).max(); + out = out.max(d + used.copied().unwrap_or(0)); + stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| (&**c, d + 1))); + } + out + } +} + /// Take out of `nodes` (each `depth` deep) the uses `bad` picks (given the depth of the object): /// an instance goes, a pattern paint becomes no paint. Returns whether anything changed. For trees /// within [`MAX_DEPTH`]. @@ -623,4 +662,23 @@ mod tests { d.patterns.push(crate::PatternDef::new("q", vec![painted(3, "none")])); assert!(!d.too_deep(None) && !d.limit_depth()); } + + #[test] + fn drawn_heights_measure_objects_outside_the_layers() { + // s0 holds s1 two groups deep, which holds s2, which holds an instance of nothing known: + // s2 draws 3 deep, s1 6 and s0 9. + let d = symbols(3, 2, "none"); + let heights = DrawnHeights::of(&d); + let on_layer = instance(1, "s0"); + assert_eq!(heights.deepest(&on_layer, 2), 2 + 9); + // An object's own nesting and its mask's art count past MAX_DEPTH. + let mut deep = Node::group(NodeId(5), vec![nested(MAX_DEPTH + 10)]); + deep.extras_mut().mask = Some(Box::new(OpacityMask::new((*instance(6, "s2")).clone(), true))); + assert_eq!(heights.deepest(&deep, 1), 1 + 1 + MAX_DEPTH + 10); + let shallow = Node::group(NodeId(5), vec![instance(7, "s1")]); + assert_eq!(heights.deepest(&shallow, 100), 101 + 6); + // A symbol that holds itself has no height, so its instance counts alone. + let looped = DrawnHeights::of(&symbols(2, 1, "s0")); + assert_eq!(looped.deepest(&instance(1, "s0"), 2), 2); + } } diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 9a4c940f23..6e313eb26f 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -117,9 +117,20 @@ thread_local! { /// While [`bake_document_placing_symbols`] bakes: the symbols whose instances become their /// placed art ([`placed_symbol_art`]). static PLACING: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; - /// The objects of symbol art that [`placed_symbol_art`] has placed for one export, counted - /// against [`vectorcraft_doc::MAX_SYMBOL_NODES`]. + /// The objects in the baked art of the instances that [`bake_node`] has replaced by their + /// placed art for one export ([`placed_symbol_art`]), counted against [`Placing::most_nodes`]. static PLACED_NODES: std::cell::Cell = const { std::cell::Cell::new(0) }; + /// How deep the object [`bake_node`] is baking sits in the tree it returns: a top-level layer + /// is 1 deep. + static LEVEL: std::cell::Cell = const { std::cell::Cell::new(1) }; +} + +/// `f` run for an object one level deeper ([`LEVEL`]) than the one being baked. +fn one_deeper(f: impl FnOnce() -> T) -> T { + LEVEL.with(|c| c.set(c.get() + 1)); + let out = f(); + LEVEL.with(|c| c.set(c.get().saturating_sub(1))); + out } /// The symbols whose instances [`bake_document_placing_symbols`] replaces by their placed art, and @@ -131,6 +142,27 @@ struct Placing { /// Whether 9-slice cells are clipped ([`Document::place_symbol_art`]) or not /// ([`Document::place_symbol_art_unclipped`]). clip_slices: bool, + /// Most objects the baked art of all placed instances may hold together: + /// [`vectorcraft_doc::MAX_SYMBOL_NODES`] for an export. + most_nodes: u64, + /// How deep the document's symbols and patterns draw, for the instances and pattern paints + /// left in placed art. + heights: vectorcraft_doc::depth::DrawnHeights, +} + +/// Can `baked`, the baked art of a placed instance, take the instance's place [`LEVEL`] deep, +/// when the art placed for the export so far holds `spent` objects? `Some` with the objects +/// placed then, when its objects (opacity masks' art included) fit within +/// [`Placing::most_nodes`] and nothing in it, the symbols and patterns it uses included, draws +/// deeper than [`vectorcraft_doc::depth::MAX_DRAWN_DEPTH`]. +fn placed_fits(baked: &Node, spent: u64) -> Option { + PLACING.with(|p| { + let p = p.borrow(); + let p = p.as_ref()?; + let spent = spent.saturating_add(baked.count_with_masks() as u64); + let deepest = p.heights.deepest(baked, LEVEL.with(|c| c.get())); + (spent <= p.most_nodes && deepest <= vectorcraft_doc::depth::MAX_DRAWN_DEPTH).then_some(spent) + }) } /// The names of the symbols of `d` whose art needs baking ([`needs_bake`]), and of those whose art @@ -183,12 +215,13 @@ fn holds_placed_symbol(n: &Node) -> bool { /// place it, in a group with the instance's id, name, visibility, transparency, opacity mask and /// raster effects. The art is placed before its effects are baked, as the canvas draws it, so an /// instance that scales its art does not scale what its effects do. `None` for other objects, -/// for instances nested [`vectorcraft_doc::depth::MAX_DEPTH`] deep in such art, and once the art -/// placed for one export would pass [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects; those -/// instances stay as they are. +/// for instances nested [`vectorcraft_doc::depth::MAX_DEPTH`] deep in such art, and when the art +/// as placed, before its effects are baked, would already take the art placed for the export +/// past [`Placing::most_nodes`]; those instances stay as they are. [`bake_node`] checks the baked +/// art with [`placed_fits`]. fn placed_symbol_art(d: &Document, n: &Node) -> Option { let NodeKind::SymbolInstance { symbol, xf } = &n.kind else { return None }; - let clip = PLACING.with(|p| p.borrow().as_ref().filter(|p| p.symbols.contains(symbol)).map(|p| p.clip_slices))?; + let (clip, most_nodes) = PLACING.with(|p| p.borrow().as_ref().filter(|p| p.symbols.contains(symbol)).map(|p| (p.clip_slices, p.most_nodes)))?; if INSTANCE_DEPTH.with(|c| c.get()) >= vectorcraft_doc::depth::MAX_DEPTH { return None; } @@ -199,11 +232,9 @@ fn placed_symbol_art(d: &Document, n: &Node) -> Option { Some(m) => m.most_placed_unclipped(&art, d), None => art.count_with_masks() as u64, }; - let spent = PLACED_NODES.with(|c| c.get()).saturating_add(most); - if spent > vectorcraft_doc::MAX_SYMBOL_NODES { + if PLACED_NODES.with(|c| c.get()).saturating_add(most) > most_nodes { return None; } - PLACED_NODES.with(|c| c.set(spent)); let placed = if clip { d.place_symbol_art(symbol, &art, *xf) } else { d.place_symbol_art_unclipped(symbol, &art, *xf) }?; Some(dressed(Node::group(n.id, vec![Arc::new(placed)]), n)) } @@ -272,12 +303,20 @@ fn bake_node(d: &mut Document, n: &Node) -> Option { return Some(baked.unwrap_or(m)); } // Symbol instances whose art draws live effects, when the caller asks for them: the art - // placed, then baked. + // placed, then baked. When the baked art passes the export's budget, or would draw too deep + // where the instance is, it is dropped with the art placed for the instances inside it, and + // the instance stays as it is. if let Some(m) = placed_symbol_art(d, n) { + let spent = PLACED_NODES.with(|c| c.get()); INSTANCE_DEPTH.with(|c| c.set(c.get() + 1)); let baked = bake_node(d, &m); INSTANCE_DEPTH.with(|c| c.set(c.get() - 1)); - return Some(baked.unwrap_or(m)); + let baked = baked.unwrap_or(m); + if let Some(now) = placed_fits(&baked, spent) { + PLACED_NODES.with(|c| c.set(now)); + return Some(baked); + } + PLACED_NODES.with(|c| c.set(spent)); } // A path whose Transform effect copies it with a gradient, or scales its strokes or moves its // patterns: each copy an object of its own, so its gradient fits it rather than spanning @@ -300,7 +339,7 @@ fn bake_node(d: &mut Document, n: &Node) -> Option { return bake_leaf(d, n); } let mut m = n.clone(); - let baked: Vec> = ch.iter().map(|c| bake_node(d, c).map(Arc::new).unwrap_or_else(|| c.clone())).collect(); + let baked: Vec> = ch.iter().map(|c| one_deeper(|| bake_node(d, c)).map(Arc::new).unwrap_or_else(|| c.clone())).collect(); if let Some(ch) = m.children_mut() { *ch = baked; } @@ -319,7 +358,7 @@ fn bake_pieces(d: &mut Document, mut m: Node) -> Node { .into_iter() .map(|c| { let c = Arc::unwrap_or_clone(c); - let mut c = bake_node(d, &c).unwrap_or(c); + let mut c = one_deeper(|| bake_node(d, &c)).unwrap_or(c); fresh_ids(d, &mut c, &mut seen); Arc::new(c) }) @@ -808,25 +847,29 @@ pub fn bake_document(doc: &Document) -> Option { /// ([`Document::place_symbol_art`]) or not ([`Document::place_symbol_art_unclipped`]), as the /// exporter places other instances. The symbols' own art is left as it is. pub fn bake_document_placing_symbols(doc: &Document, clip_slices: bool) -> Option { - bake(doc, Some(clip_slices)) + bake(doc, Some((clip_slices, vectorcraft_doc::MAX_SYMBOL_NODES))) } /// Restores what [`bake`] changes in the thread's baking state when it ends. -struct Restore(Option>, u64); +struct Restore(Option>, u64, usize); impl Drop for Restore { fn drop(&mut self) { PLACING.with(|p| *p.borrow_mut() = self.0.take()); PLACED_NODES.with(|c| c.set(self.1)); + LEVEL.with(|c| c.set(self.2)); } } -/// [`bake_document`], or with `placing` [`bake_document_placing_symbols`] with that `clip_slices`. -fn bake(doc: &Document, placing: Option) -> Option { +/// [`bake_document`], or with `placing` [`bake_document_placing_symbols`] with that `clip_slices` +/// and [`Placing::most_nodes`]. +fn bake(doc: &Document, placing: Option<(bool, u64)>) -> Option { let placed = doc.with_placed_art(); - let placing = placing.map(|clip_slices| Placing { symbols: drawn_symbols(&placed), clip_slices }).filter(|p| !p.symbols.is_empty()); + let placing = placing.map(|(clip_slices, most_nodes)| (drawn_symbols(&placed), clip_slices, most_nodes)).filter(|p| !p.0.is_empty()).map( + |(symbols, clip_slices, most_nodes)| Placing { symbols, clip_slices, most_nodes, heights: vectorcraft_doc::depth::DrawnHeights::of(&placed) }, + ); // A bake inside another one (of art an effect makes) places symbols only when it asks to. - let _restore = Restore(PLACING.with(|p| p.replace(placing.map(Rc::new))), PLACED_NODES.with(|c| c.replace(0))); + let _restore = Restore(PLACING.with(|p| p.replace(placing.map(Rc::new))), PLACED_NODES.with(|c| c.replace(0)), LEVEL.with(|c| c.replace(1))); if !placed.layers.iter().any(|l| needs_bake(l) || holds_placed_symbol(l)) { return match placed { std::borrow::Cow::Owned(d) => Some(d), diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs index b0ebfc4eba..f72637b6f6 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -1172,3 +1172,75 @@ fn placing_symbols_keeps_dynamic_instances_changes() { assert!(fills.contains(&(110.0, 40.0, red)), "{fills:?}"); assert!(fills.contains(&(10.0, 60.0, Paint::solid(Color::WHITE))), "{fills:?}"); } + +fn is_instance(d: &Document, id: u64) -> bool { + d.node(NodeId(id)).is_some_and(|n| matches!(n.kind, NodeKind::SymbolInstance { .. })) +} + +/// The art placed for an export is counted as baked: the copies an effect makes count, so an +/// instance whose baked art passes what is left of the budget stays an instance, and the budget +/// holds the baked art of the instances placed, exactly. +#[test] +fn placing_symbols_counts_the_baked_art() { + use vectorcraft_geom::Affine; + let mut d = doc_with(instance(900, "S", Affine::IDENTITY)); + let layer = d.layers[0].id; + d.insert(Some(layer), 0, instance(901, "S", Affine::translate((0.0, 100.0)))).unwrap(); + add_symbol(&mut d, "S", gradient_copies(800)); + let all = bake(&d, Some((true, u64::MAX))).unwrap(); + let each = all.node(NodeId(900)).map_or(0, |n| n.count_with_masks() as u64); + assert!(each > 2, "the instance's group, and the copies as objects of their own: {each}"); + assert_eq!(all.node(NodeId(901)).map(|n| n.count_with_masks() as u64), Some(each)); + let fits = bake(&d, Some((true, 2 * each))).unwrap(); + assert!(!is_instance(&fits, 900) && !is_instance(&fits, 901)); + let short = bake(&d, Some((true, 2 * each - 1))).unwrap(); + assert_eq!(usize::from(is_instance(&short, 900)) + usize::from(is_instance(&short, 901)), 1, "one instance stays as it is"); +} + +/// The baked art of an instance placed inside another's art is counted once: a budget of exactly +/// the outer instance's baked art places both. +#[test] +fn placing_symbols_counts_nested_art_once() { + use vectorcraft_geom::Affine; + let mut d = doc_with(instance(900, "Outer", Affine::IDENTITY)); + add_symbol(&mut d, "Inner", gradient_copies(800)); + add_symbol(&mut d, "Outer", Node::group(NodeId(801), vec![Arc::new(instance(802, "Inner", Affine::IDENTITY))])); + let all = bake(&d, Some((true, u64::MAX))).unwrap(); + let total = all.node(NodeId(900)).map_or(0, |n| n.count_with_masks() as u64); + let exact = bake(&d, Some((true, total))).unwrap(); + assert!(!is_instance(&exact, 900) && !is_instance(&exact, 802), "both are placed within {total}"); + let short = bake(&d, Some((true, total - 1))).unwrap(); + assert!(is_instance(&short, 900), "the outer instance stays as it is"); +} + +/// `leaf` in `levels` groups, with ids from `id` up. +fn in_groups(id: u64, levels: u64, leaf: Node) -> Node { + (0..levels).fold(leaf, |n, i| Node::group(NodeId(id + i), vec![Arc::new(n)])) +} + +/// Placed art is drawn no deeper than the document may draw: an instance whose art's effects add +/// a level past it stays an instance, and the instances around it are placed. +#[test] +fn placing_symbols_keeps_the_baked_art_within_the_drawn_depth() { + use vectorcraft_doc::depth::{DrawnHeights, MAX_DRAWN_DEPTH}; + use vectorcraft_geom::Affine; + // The instance on the layer is 2 deep; A to D each hold the next 31 objects down, and E's + // square is 2 deep in its art: 2 + 4 × 31 + 2 = MAX_DRAWN_DEPTH. + let mut d = doc_with(instance(900, "A", Affine::IDENTITY)); + for (i, (name, next)) in [("A", "B"), ("B", "C"), ("C", "D"), ("D", "E")].into_iter().enumerate() { + let base = 1000 * (i as u64 + 1); + add_symbol(&mut d, name, in_groups(base, 30, instance(base + 100, next, Affine::IDENTITY))); + } + add_symbol(&mut d, "E", in_groups(5000, 1, gradient_copies(5100))); + assert!(!d.too_deep(None)); + assert_eq!(DrawnHeights::of(&d).deepest(&d.layers[0], 1), MAX_DRAWN_DEPTH); + let out = bake_document_placing_symbols(&d, true).unwrap(); + // E's square becomes a group of copies, a level past the cap: E's instance stays. + assert!(is_instance(&out, 4100), "E's instance stays as it is"); + assert!(!is_instance(&out, 900) && !is_instance(&out, 3100), "A to D are placed"); + assert!(DrawnHeights::of(&out).deepest(&out.layers[0], 1) <= MAX_DRAWN_DEPTH); + // Without the chain above it, E's art is placed. + let mut shallow = doc_with(instance(900, "E", Affine::IDENTITY)); + add_symbol(&mut shallow, "E", in_groups(5000, 1, gradient_copies(5100))); + assert!(!is_instance(&bake_document_placing_symbols(&shallow, true).unwrap(), 900)); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c5d7314319..a5365c758c 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1561,7 +1561,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. - Live effects inside a symbol's art (geometry effects, color adjustments and the rest that export bakes) are written where each instance places the art in PDF, print, EPS and DXF, as the canvas draws them: the art is placed first and its effects are applied after, so an instance that scales its art does not scale the distance a Transform effect moves it. Symbols nested in the art and a dynamic symbol instance's own paint changes are kept, a symbol whose art holds itself stops at the nesting limit, and the symbols' own art keeps its effects. Before, these exports wrote the art without its effects (effects `bake::placing_symbols_bakes_their_art_where_each_instance_places_it`, `placing_symbols_reaches_the_art_of_nested_instances`, `placing_a_symbol_that_holds_itself_stops`, `placing_symbols_keeps_dynamic_instances_changes`; PDF, print, EPS and DXF `symbol_art_effects_are_written_where_they_draw`, `symbol_art_effects_print_where_they_draw`). + Live effects inside a symbol's art (geometry effects, color adjustments and the rest that export bakes) are written where each instance places the art in PDF, print, EPS and DXF, as the canvas draws them: the art is placed first and its effects are applied after, so an instance that scales its art does not scale the distance a Transform effect moves it. Symbols nested in the art and a dynamic symbol instance's own paint changes are kept, a symbol whose art holds itself stops at the nesting limit, and the symbols' own art keeps its effects. An instance stays an instance in these exports when the objects its art makes once baked (the copies a Transform effect makes included) would take the export's placed symbol art past 16,000,000 objects, or when its baked art would draw deeper than the 128 levels a document may draw; these are VectorCraft's own limits. Before, these exports wrote the art without its effects (effects `bake::placing_symbols_bakes_their_art_where_each_instance_places_it`, `placing_symbols_reaches_the_art_of_nested_instances`, `placing_a_symbol_that_holds_itself_stops`, `placing_symbols_keeps_dynamic_instances_changes`, `placing_symbols_counts_the_baked_art`, `placing_symbols_counts_nested_art_once`, `placing_symbols_keeps_the_baked_art_within_the_drawn_depth`; doc `depth::tests::drawn_heights_measure_objects_outside_the_layers`; PDF, print, EPS and DXF `symbol_art_effects_are_written_where_they_draw`, `symbol_art_effects_print_where_they_draw`). 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. From 77070d29a7368863cae7ff556530a20a7cfbf08c Mon Sep 17 00:00:00 2001 From: ncic <1848265+ncic@users.noreply.github.com> Date: Sat, 14 Nov 2026 09:22:18 +0100 Subject: [PATCH 21/44] Pencil: the continuation cursor follows the snapped release; Precise Cursors tests keep the badges --- crates/engine/src/tests/prefs.rs | 12 +++++---- crates/tools/src/draw2/gesture.rs | 14 ++++++++-- crates/tools/src/draw2/gesture/tests.rs | 34 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index d88b8250a2..67a740d4a3 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -723,8 +723,10 @@ fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() { assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} off"); } s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap(); - for (tool, _) in own { - assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on"); + for (tool, c) in own { + // The Pencil's star is drawn on the crosshair and keeps its badge. + let want = if tool == "pencil" { c } else { Cursor::Crosshair }; + assert_eq!(cursor(&mut s, tool), want, "{tool} on"); } for tool in always { assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on"); @@ -758,8 +760,8 @@ fn use_precise_cursors_keeps_the_smooth_tools_not_allowed_pointer() { } } -/// With Alt held while drawing, the Pencil and the Paintbrush show their closing cursors, and Use Precise Cursors -/// turns those into a crosshair too. +/// With Alt held while drawing, the Pencil and the Paintbrush show their closing cursors. Use Precise Cursors keeps +/// them: they are drawn on the crosshair, and the ring says the release closes the path. #[test] fn alt_shows_the_pencil_and_paintbrush_closing_cursors() { use vectorcraft_tools::{Cursor, Mods, PointerEvent, PointerKind}; @@ -773,7 +775,7 @@ fn alt_shows_the_pencil_and_paintbrush_closing_cursors() { s.select_tool(tool, ViewInfo::default()).unwrap(); s.pointer(&PointerEvent::new(PointerKind::Down, 250.0, 250.0), ViewInfo::default()).unwrap(); s.pointer(&PointerEvent::new(PointerKind::Drag, 300.0, 300.0), ViewInfo::default()).unwrap(); - assert_eq!(s.cursor(at, alt, ViewInfo::default()), if precise { Cursor::Crosshair } else { c }, "{tool}, precise {precise}"); + assert_eq!(s.cursor(at, alt, ViewInfo::default()), c, "{tool}, precise {precise}"); s.pointer(&PointerEvent::new(PointerKind::Up, 300.0, 300.0), ViewInfo::default()).unwrap(); } } diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index fc57b4d953..c3191c4363 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -98,6 +98,9 @@ pub struct GestureTool { /// A release where the pointer last dragged to would close the stroke because it ends near /// its start (Close Paths When Ends Are Within): the cursor shows the closing ring. near_start: bool, + /// A release where the pointer last dragged to would connect the continued path to another + /// selected path's open end (`finish`): the cursor shows the continuation cursor. + connects: bool, /// Pencil/Paintbrush continuing a selected open path: (path, continue from its start). extend: Option<(NodeId, bool)>, /// The other end of the path `extend` continues: a release near it closes the path. @@ -819,6 +822,7 @@ impl GestureTool { active: false, began: false, near_start: false, + connects: false, extend: None, close_to: None, connect_to: vec![], @@ -1303,6 +1307,7 @@ impl Tool for GestureTool { self.active = true; self.began = false; self.near_start = false; + self.connects = false; // Alt switches the Pencil when its toggle is on, the Blob Brush always, and the // Paintbrush when the press has a selected path. self.smoothing = ev.mods.alt @@ -1380,10 +1385,12 @@ impl Tool for GestureTool { let kept = self.points.last().copied().filter(|l| l.distance(p) < cx.tol(1.0) && !self.snaps()); let release_end = kept.unwrap_or(end); self.near_start = self.release_closes_near_start(cx, release_end); + self.connects = self.connects_at(cx, release_end).is_some(); return vec![]; } self.push(p, ev); self.near_start = self.release_closes_near_start(cx, end); + self.connects = self.connects_at(cx, end).is_some(); if !self.draws_path() || self.smoothing { return vec![]; } @@ -1444,6 +1451,7 @@ impl Tool for GestureTool { self.active = false; self.began = false; self.near_start = false; + self.connects = false; self.smoothing = false; self.straight = false; self.unconstrained = false; @@ -1479,6 +1487,7 @@ impl Tool for GestureTool { self.active = false; self.began = false; self.near_start = false; + self.connects = false; self.smoothing = false; self.straight = false; self.unconstrained = false; @@ -1509,6 +1518,7 @@ impl Tool for GestureTool { self.snap.clear(); self.active = false; self.near_start = false; + self.connects = false; self.began = false; self.smoothing = false; self.straight = false; @@ -1597,8 +1607,8 @@ impl Tool for GestureTool { return if self.find_extend(cx, p).is_some() { Cursor::PencilContinue } else { Cursor::PencilStraight }; } // A continuing Pencil stroke that a release would connect to another selected path's open - // end (`finish`) shows that it continues on. - if self.id == "pencil" && self.active && self.points.last().is_some_and(|&end| self.connects_at(cx, end).is_some()) { + // end (`finish`) shows that it continues on; `connects` tests the point the release keeps. + if self.id == "pencil" && self.active && self.connects { return Cursor::PencilContinue; } // Alt held at the release closes the path (`finish`). diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 3bee270237..2159b8af2c 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -959,6 +959,40 @@ fn a_continued_stroke_shows_where_it_connects_or_closes() { assert_eq!((shown, v["closed"].clone()), (Cursor::Pencil, json!(false)), "at 0 px"); } +/// The continuation cursor tests where the release ends, after snapping, not the pointer: here +/// Snap to Grid moves a stroke's end from 13 pt away from another selected path's end (outside Edit +/// Selected Paths' 12 px) to 9 pt away, so the release connects, and the cursor says so. +#[test] +fn the_continuation_cursor_follows_the_snapped_release() { + let (mut d, _) = doc_with_rect(); + let l = d.layers[0].id; + let add = |d: &mut vectorcraft_doc::Document, a: (f64, f64), b: (f64, f64)| { + let id = d.alloc_id(); + let els = vec![vectorcraft_geom::PathEl::MoveTo(Point::new(a.0, a.1)), vectorcraft_geom::PathEl::LineTo(Point::new(b.0, b.1))]; + let pd = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(els)); + d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, pd, vectorcraft_doc::Appearance::default_art())).unwrap(); + id + }; + // On the 9 pt grid. + let a = add(&mut d, (306.0, 306.0), (405.0, 405.0)); + let b = add(&mut d, (504.0, 504.0), (603.0, 504.0)); + let p = paint(); + let mut both = Selection::default(); + both.set([a, b]); + let mut cx = cx(&d, &both, &p); + cx.smart_guides = false; + cx.snap_to_point = false; + cx.snap_to_grid = true; + let mut t = GestureTool::new("pencil"); + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 405.0, 405.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 460.0, 450.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 517.0, 504.0)); + assert_eq!(t.cursor(&cx, Point::new(517.0, 504.0), Mods::default()), Cursor::PencilContinue); + let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 517.0, 504.0)); + let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") }; + assert_eq!(v["connect"], json!({"id": b.0, "end": "start"}), "{v}"); +} + /// #3846: Alt during a Shift line doesn't promise a close the release can't do, and with Alt /// Toggles to Smooth, Alt pressed after the press doesn't turn the line into a smoothing drag. #[test] From 2a5f2009d82af7808d1fa06a4d5ab90a68743cd1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 22:04:30 -0800 Subject: [PATCH 22/44] Graphs: Import Data reads UTF-16 text files without a byte-order mark The Graph Data window's Import Data read UTF-16 only after a byte-order mark, so a UTF-16 file without one filled the cells with NUL characters. It now reads such a file in the byte order Import Data Sets' rule (#7348) gives it: at least half of the two-byte units hold a NUL byte, and the byte order is the one that reads the line breaks. The text is then read as after a byte-order mark: an odd last byte is dropped and an unpaired surrogate reads as U+FFFD. A file the rule refuses is read as UTF-8, as before. The rule now also gives a byte order to a one-line file, which reads a line break in neither order: big-endian when more of its units start with a NUL byte than end with one, else little-endian as before. Import Data Sets still refuses a one-line file, which has no data rows; read in the new order, such a file can be refused with a different error first. --- crates/engine/src/cmd.rs | 4 +++ crates/engine/src/cmd/variablecmds.rs | 1 + crates/engine/src/cmd/variablecmds/csv.rs | 36 +++++++++++++++-------- crates/engine/src/tests/variables.rs | 10 +++++++ crates/ui-egui/src/dialogs/graph_data.rs | 32 ++++++++++++++++++-- docs/mcp.md | 5 ++-- docs/target-app-parity.md | 1 + 7 files changed, 72 insertions(+), 17 deletions(-) diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index 94d8add68b..92ec32a617 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -874,6 +874,10 @@ pub use buildcmds::live_paint_gaps; /// Control bar enable it when it changes one). pub use brushsym::reset_transformation_changes; +/// The byte order of UTF-16 text without a byte-order mark, by Import Data Sets' rule (the Graph +/// Data window reads files by it too). +pub use variablecmds::utf16_order_without_mark; + /// Command ids kept only so older scripts keep working: `(alias, the command it duplicates)`. /// They run as before but the command palette leaves them out (it lists the command once). pub const ALIASES: &[(&str, &str)] = &[("object.convertDocumentColorMode", "file.documentColorMode")]; diff --git a/crates/engine/src/cmd/variablecmds.rs b/crates/engine/src/cmd/variablecmds.rs index 9acd31e664..5f8d6605b3 100644 --- a/crates/engine/src/cmd/variablecmds.rs +++ b/crates/engine/src/cmd/variablecmds.rs @@ -26,6 +26,7 @@ use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, checked_ids mod csv; mod library; +pub use csv::utf16_order_without_mark; use csv::{MAX_GRAPH_CSV, dataset_export, dataset_import}; pub(crate) use library::{give_svg_library, svg_library}; use library::{load_library, save_library}; diff --git a/crates/engine/src/cmd/variablecmds/csv.rs b/crates/engine/src/cmd/variablecmds/csv.rs index 38dd42d922..be9db3b841 100644 --- a/crates/engine/src/cmd/variablecmds/csv.rs +++ b/crates/engine/src/cmd/variablecmds/csv.rs @@ -59,25 +59,35 @@ fn utf16(bytes: &[u8], le: bool) -> Option { char::decode_utf16(units).collect::>().ok().filter(|_| odd.is_empty()) } -/// `bytes`, which hold NUL bytes and no byte-order mark, as UTF-16, or `None`. At least half of -/// the two-byte units must hold a NUL byte, as characters from U+0001 to U+00FF do; a file with -/// fewer, such as UTF-8 or Windows-1252 text with stray NUL bytes, is refused. The byte order is -/// the one that reads a line break (`\n` or `\r`). The other order reads one only from U+0A00 or -/// U+0D00, so a file that reads one in both orders is refused. A file that reads none in either -/// order has no data rows, and is read little-endian so that the import reports that. The text -/// must be valid UTF-16 in that order and hold no NUL character. -fn utf16_without_mark(bytes: &[u8]) -> Option { - let (units, odd) = bytes.as_chunks::<2>(); - if !odd.is_empty() || units.iter().filter(|c| c.contains(&0)).count() * 2 < units.len() { +/// The byte order of `bytes`, which hold NUL bytes and no byte-order mark, read as UTF-16: +/// `Some(true)` for little-endian, or `None` when they don't read as UTF-16. At least half of the +/// two-byte units must hold a NUL byte, as characters from U+0001 to U+00FF do; a file with fewer, +/// such as UTF-8 or Windows-1252 text with stray NUL bytes, is refused. The byte order is the one +/// that reads a line break (`\n` or `\r`). The other order reads one only from U+0A00 or U+0D00, +/// so a file that reads one in both orders is refused. A file that reads none in either order (one +/// line) is big-endian when more of its units start with a NUL byte than end with one, as +/// characters from U+0001 to U+00FF do in that order, and little-endian otherwise. An odd last +/// byte is left out of the count. +pub fn utf16_order_without_mark(bytes: &[u8]) -> Option { + let units = bytes.as_chunks::<2>().0; + if units.iter().filter(|c| c.contains(&0)).count() * 2 < units.len() { return None; } let is_line_break = |u: u16| matches!(u, 0x0A | 0x0D); let lines_le = units.iter().any(|&c| is_line_break(u16::from_le_bytes(c))); let lines_be = units.iter().any(|&c| is_line_break(u16::from_be_bytes(c))); - if lines_le && lines_be { - return None; + match (lines_le, lines_be) { + (true, true) => None, + (false, false) => Some(units.iter().filter(|c| c[0] == 0).count() <= units.iter().filter(|c| c[1] == 0).count()), + (_, lines_be) => Some(!lines_be), } - utf16(bytes, !lines_be).filter(|text| !text.contains('\0')) +} + +/// `bytes`, which hold NUL bytes and no byte-order mark, as UTF-16 in the byte order +/// [`utf16_order_without_mark`] gives, or `None`. The file must have an even length, and the text +/// must be valid UTF-16 in that order and hold no NUL character. +fn utf16_without_mark(bytes: &[u8]) -> Option { + utf16(bytes, utf16_order_without_mark(bytes)?).filter(|text| !text.contains('\0')) } /// A row of a datasets CSV: the file line it starts on, its cells, and for each cell whether it was quoted. diff --git a/crates/engine/src/tests/variables.rs b/crates/engine/src/tests/variables.rs index eea8ae4986..6256088842 100644 --- a/crates/engine/src/tests/variables.rs +++ b/crates/engine/src/tests/variables.rs @@ -2201,6 +2201,16 @@ fn datasets_csv_reads_utf16_files_without_a_byte_order_mark() { let e = import(&mut s, &bytes).unwrap_err().to_string(); assert!(e.contains("CSV has a header but no data rows"), "{e}"); } + // With no line break, the side of each unit that holds the NUL byte gives the byte order, and a + // tie is little-endian. + let order = crate::cmd::utf16_order_without_mark; + assert_eq!(order(&utf16("City", false)), Some(false)); + assert_eq!(order(&utf16("City", true)), Some(true)); + assert_eq!(order(b"\0aa\0"), Some(true)); + assert_eq!(order(&[utf16("City\n", false), vec![0x0A]].concat()), Some(false), "an odd last byte is left out"); + // Read big-endian, this one-line file ends in an unpaired surrogate. + let e = import(&mut s, b"\0a\0b\xd8\0").unwrap_err().to_string(); + assert!(e.contains("CSV holds NUL bytes"), "{e}"); import(&mut s, &utf16("City\tName\n一二三丁目\tKai\n", true)).unwrap(); assert_eq!(value(&s, "Data Set 1"), text_value("一二三丁目")); // Most units hold their NUL byte where a big-endian file would (U+4E00); the line breaks diff --git a/crates/ui-egui/src/dialogs/graph_data.rs b/crates/ui-egui/src/dialogs/graph_data.rs index 16d22d4430..2dbcfad8fe 100644 --- a/crates/ui-egui/src/dialogs/graph_data.rs +++ b/crates/ui-egui/src/dialogs/graph_data.rs @@ -147,7 +147,10 @@ pub(crate) fn grid_from_text(text: &str) -> Grid { } /// A text file's bytes as text: UTF-16 with its byte-order mark (spreadsheets' "Unicode Text"), -/// else UTF-8 without one. +/// else UTF-16 without one when the bytes hold NUL bytes and Import Data Sets' rule gives them a +/// byte order ([`vectorcraft_engine::cmd::utf16_order_without_mark`]), else UTF-8 with or without +/// one. UTF-16 is read as it is after a byte-order mark: an odd last byte is left out, and an +/// unpaired surrogate reads as U+FFFD. pub(crate) fn decode(bytes: &[u8]) -> String { let utf16 = |rest: &[u8], le: bool| { let units: Vec = rest.as_chunks::<2>().0.iter().map(|p| if le { u16::from_le_bytes(*p) } else { u16::from_be_bytes(*p) }).collect(); @@ -157,7 +160,10 @@ pub(crate) fn decode(bytes: &[u8]) -> String { [0xFF, 0xFE, rest @ ..] => utf16(rest, true), [0xFE, 0xFF, rest @ ..] => utf16(rest, false), [0xEF, 0xBB, 0xBF, rest @ ..] => String::from_utf8_lossy(rest).into_owned(), - _ => String::from_utf8_lossy(bytes).into_owned(), + _ => match bytes.contains(&0).then(|| vectorcraft_engine::cmd::utf16_order_without_mark(bytes)).flatten() { + Some(le) => utf16(bytes, le), + None => String::from_utf8_lossy(bytes).into_owned(), + }, } } @@ -517,6 +523,28 @@ mod tests { assert_eq!(grid_from_text(&wide)[0].len(), MAX_COLS); } + #[test] + fn utf16_without_a_byte_order_mark_reads_by_its_line_breaks() { + let text = "\tSales é\r\nQ1\t1\n"; + let le: Vec = text.encode_utf16().flat_map(u16::to_le_bytes).collect(); + let be: Vec = text.encode_utf16().flat_map(u16::to_be_bytes).collect(); + assert_eq!(decode(&le), text, "little-endian"); + assert_eq!(decode(&be), text, "big-endian"); + assert_eq!(grid_from_text(&decode(&le)), g(&[&["", "Sales é"], &["Q1", "1"]])); + // One row has no line break: the side of each unit that holds the NUL byte gives the order. + for (bytes, order) in [(b"\0Q\x001\0\t\x002".to_vec(), "big-endian"), (b"Q\x001\0\t\x002\0".to_vec(), "little-endian")] { + assert_eq!(decode(&bytes), "Q1\t2", "one line, {order}"); + } + // A file cut short by one byte, or with an unpaired surrogate, reads as it would after a + // byte-order mark. + assert_eq!(decode(&[le.as_slice(), &[0x0A]].concat()), text, "odd length"); + assert_eq!(decode(&[le.as_slice(), &[0x00, 0xD8]].concat()), format!("{text}\u{FFFD}"), "unpaired surrogate"); + // A line break in both byte orders leaves the order unknown, and UTF-8 with a stray NUL + // byte isn't UTF-16: both read as UTF-8, as before. + assert_eq!(decode(&[0x0A, 0, 0, 0x0A]), "\n\0\0\n"); + assert_eq!(decode(b"a,b\n1,2\0"), "a,b\n1,2\0"); + } + #[test] fn a_sheet_sent_wider_and_taller_than_a_graph_is_cut_to_one() { // One long row and one long column: their product would be ten billion cells. diff --git a/docs/mcp.md b/docs/mcp.md index f756bc8c97..a68645348e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3175,8 +3175,9 @@ already walks Image Trace. Short agent entry points: byte order that reads a line break (`\n` or `\r`). It is refused when fewer of its units hold a NUL byte (UTF-8 or Windows-1252 text with stray NUL bytes, or UTF-16 text made mostly of other characters, which needs a byte-order mark), when both byte orders read a line break (the text holds U+0A00 or U+0D00), when it has an odd length, and - when it isn't valid UTF-16 in that order or holds a NUL character. A file with no line break has no data rows and is - refused for that. `csv` and `data` are read as the text they hold. + when it isn't valid UTF-16 in that order or holds a NUL character. A file with no line break is read big-endian when + more of its two-byte units start with a NUL byte than end with one, and little-endian otherwise; it has no data rows, + so the import is refused. `csv` and `data` are read as the text they hold. - Variable libraries: `variable.loadLibrary {path|xml|dataBase64}` (the Variables panel menu's Load Variable Library…) replaces the document's variables and data sets with a variable library's in one undo step. A load, like a `dataset.import`, adds no undo step when it leaves the variables and data sets as they were. The library diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 296bd84b2b..8fe37830a2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -793,6 +793,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Export for Screens numbers distinct row files whose names collide, compared without case, as `-2`, `-3`… instead of leaving them out; identical rows still write one file. It puts row folders that differ only in case into one folder (`document.exportForScreens`). - Variables: Import Data Sets… (`dataset.import`) reads tab-separated files as well as comma-separated ones. Every header cell is a variable, and the rows replace the document's data sets as Data Set 1, Data Set 2 and so on. An import error about a row's values gives the file line the row starts on. - Variables: Import Data Sets… (`dataset.import`) reads a UTF-16 file without a byte-order mark, instead of refusing it, when at least half of its two-byte units hold a NUL byte; it uses the byte order that reads its line breaks. + - Graphs: the Graph Data window's Import Data reads a UTF-16 text file without a byte-order mark by the same rule as Import Data Sets…, instead of filling the cells with NUL characters. It reads a file cut short by one byte, or with an unpaired surrogate, as it reads one after a byte-order mark. - Variables: Import Data Sets… (`dataset.import`) defines the variables a file's header names that the document lacks, unbound. `#Shown` defines a visibility variable, `@Photo` defines an image variable, and a header without a prefix defines a text variable. A header that is a variable's exact name keeps naming it, and any other `%` header (graph data files) is refused. - Variables: Load Variable Library… in the Variables panel menu (`variable.loadLibrary`) replaces the document's variables and data sets with a variable library's, read from its XML file, in one undo step: every data set is deleted, and every variable is deleted with its bindings and values, except a text variable the library also declares, which stays bound so the library's data sets change its objects (#4136); then the library's other text variables are defined, unbound, and its data sets take the text of each value's `p` elements, one paragraph each. Variables of other kinds are left out with a warning that names their kinds. - Variables: Save Variable Library… in the Variables panel menu (`variable.saveLibrary`) writes the document's text variables and data sets as a variable library's XML file, which Load Variable Library reads back with the same text variables, data set names and text values. Variables of other kinds are left out with a warning that names their kinds. From 5695823937004fb99c1c0d01dddd2525d2dbb22c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 02:06:18 -0800 Subject: [PATCH 23/44] Export: placed symbol art keeps its instance's stain When PDF, print, EPS and DXF export place a symbol's art to bake its effects, the art now carries the stain the instance's fill gives it, as it did before the art was placed. The art is stained first and its effects apply to the stained colors. --- crates/effects/src/bake.rs | 6 +++--- crates/effects/src/bake/tests.rs | 36 ++++++++++++++++++++++++++++++++ crates/pdf/src/tests.rs | 25 ++++++++++++++++++++++ docs/target-app-parity.md | 2 +- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 6e313eb26f..7f7da8e27b 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -210,8 +210,8 @@ fn holds_placed_symbol(n: &Node) -> bool { } /// Symbol instance `n`, while [`bake_document_placing_symbols`] bakes and its symbol is one it -/// places, as the art it draws: the symbol's art with the instance's own appearance changes -/// ([`vectorcraft_doc::symbol_overrides::dynamic_art`]), placed by the instance as the exporters +/// places, as the art it draws: the symbol's art with the instance's own appearance changes and +/// its stain ([`vectorcraft_doc::symbol_overrides::instance_art`]), placed by the instance as the exporters /// place it, in a group with the instance's id, name, visibility, transparency, opacity mask and /// raster effects. The art is placed before its effects are baked, as the canvas draws it, so an /// instance that scales its art does not scale what its effects do. `None` for other objects, @@ -226,7 +226,7 @@ fn placed_symbol_art(d: &Document, n: &Node) -> Option { return None; } let sym = d.symbols.iter().find(|s| s.name == *symbol)?; - let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n); + let art = vectorcraft_doc::symbol_overrides::instance_art(&sym.art, n); let most = match d.symbol_slice_map(symbol, *xf) { Some(m) if clip => m.most_placed(&art, d), Some(m) => m.most_placed_unclipped(&art, d), diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs index f72637b6f6..be72b4df6e 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -1173,6 +1173,42 @@ fn placing_symbols_keeps_dynamic_instances_changes() { assert!(fills.contains(&(10.0, 60.0, Paint::solid(Color::WHITE))), "{fills:?}"); } +/// An instance whose fill stains its symbol's art places the art in its stained colors, with the +/// art's effects applied after the stain: a Transform moves the stained square and an Invert +/// inverts the stained color. +#[test] +fn placing_symbols_keeps_the_instances_stain() { + use vectorcraft_geom::Affine; + let blue = Color::rgb8(30, 90, 220); + let mut inst = instance(900, "Mark", Affine::IDENTITY); + inst.appearance = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0).into(); + inst.appearance.items.retain(|i| i.is_fill()); + if let AppearanceItem::Fill(f) = &mut inst.appearance.items[0] { + f.opacity = 0.5; + } + let square = + Node::path(NodeId(800), shapes::rectangle(Rect::new(10.0, 40.0, 20.0, 50.0)), Appearance::basic(Paint::solid(blue), Paint::None, 0.0)); + let stained = vectorcraft_doc::symbol_overrides::instance_art(&square, &inst).appearance.fill_paint().color().unwrap(); + assert_ne!(stained.to_hex(), blue.to_hex()); + let [r, g, b] = stained.to_rgb(); + let inverted = Color::rgb(1.0 - r, 1.0 - g, 1.0 - b); + for (id, params, want) in [("distort.transform", json!({"moveH": 100.0}), stained), (crate::addons::color::INVERT, json!({}), inverted)] { + let mut art = square.clone(); + art.appearance.effects.push(fx(id, params)); + let mut d = doc_with(inst.clone()); + add_symbol(&mut d, "Mark", art); + let out = bake_document_placing_symbols(&d, true).unwrap(); + assert!(!is_instance(&out, 900), "{id}: the instance is placed"); + let mut fills = vec![]; + out.node(NodeId(900)).unwrap().walk(&mut |n| { + if n.path_data().is_some() { + fills.extend(n.appearance.fill_paint().color().map(|c| c.to_hex())); + } + }); + assert_eq!(fills, [want.to_hex()], "{id}"); + } +} + fn is_instance(d: &Document, id: u64) -> bool { d.node(NodeId(id)).is_some_and(|n| matches!(n.kind, NodeKind::SymbolInstance { .. })) } diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 1b415d3804..5e9796b9b4 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -356,6 +356,31 @@ fn a_stained_instance_keeps_its_stain() { } } +/// A stained instance of a symbol whose art has a live effect is written in its stained colors, +/// where the effect draws the art. +#[test] +fn a_stained_instance_keeps_its_stain_when_its_art_has_effects() { + let blue = Color::rgb8(30, 90, 220); + let square = rect_node(Rect::new(10.0, 40.0, 20.0, 50.0), blue); + let half = stained_instance("Moved", Affine::IDENTITY, 0.5, true); + let want = vectorcraft_brush::instance_art(&square, &half).appearance.fill_paint().color().unwrap().to_hex(); + assert_ne!(want, blue.to_hex(), "the stain changes the color"); + let mut art = square; + art.appearance.effects.push(vectorcraft_doc::Effect { + id: "distort.transform".into(), + params: serde_json::json!({"moveH": 100.0}), + visible: true, + }); + let mut d = doc(200.0, 100.0); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Moved".into(), art: Arc::new(art) }); + add(&mut d, half); + let out = roundtrip(&d); + let l = leaves(&out); + assert_eq!(l.len(), 1); + assert!(close(l[0].geometric_bounds().unwrap(), Rect::new(110.0, 40.0, 120.0, 50.0), 0.01), "{:?}", l[0].geometric_bounds()); + assert_eq!(leaf_fills(&out), [want]); +} + /// The placed art of all the 9-slice instances in one export holds at most `MAX_SYMBOL_NODES` /// objects, opacity-mask art included, as on the canvas: of two instances whose art placed through /// the guides holds about 9,009,000 objects each (a gradient square in all nine cells, each copy diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2c5814996f..a6266f7bb3 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1572,7 +1572,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. - Live effects inside a symbol's art (geometry effects, color adjustments and the rest that export bakes) are written where each instance places the art in PDF, print, EPS and DXF, as the canvas draws them: the art is placed first and its effects are applied after, so an instance that scales its art does not scale the distance a Transform effect moves it. Symbols nested in the art and a dynamic symbol instance's own paint changes are kept, a symbol whose art holds itself stops at the nesting limit, and the symbols' own art keeps its effects. An instance stays an instance in these exports when the objects its art makes once baked (the copies a Transform effect makes included) would take the export's placed symbol art past 16,000,000 objects, or when its baked art would draw deeper than the 128 levels a document may draw; these are VectorCraft's own limits. Before, these exports wrote the art without its effects (effects `bake::placing_symbols_bakes_their_art_where_each_instance_places_it`, `placing_symbols_reaches_the_art_of_nested_instances`, `placing_a_symbol_that_holds_itself_stops`, `placing_symbols_keeps_dynamic_instances_changes`, `placing_symbols_counts_the_baked_art`, `placing_symbols_counts_nested_art_once`, `placing_symbols_keeps_the_baked_art_within_the_drawn_depth`; doc `depth::tests::drawn_heights_measure_objects_outside_the_layers`; PDF, print, EPS and DXF `symbol_art_effects_are_written_where_they_draw`, `symbol_art_effects_print_where_they_draw`). + Live effects inside a symbol's art (geometry effects, color adjustments and the rest that export bakes) are written where each instance places the art in PDF, print, EPS and DXF, as the canvas draws them: the art is placed first and its effects are applied after, so an instance that scales its art does not scale the distance a Transform effect moves it. Symbols nested in the art, a dynamic symbol instance's own paint changes and the stain an instance's fill gives its art are kept, and the art is stained before its effects apply. A symbol whose art holds itself stops at the nesting limit, and the symbols' own art keeps its effects. An instance stays an instance in these exports when the objects its art makes once baked (the copies a Transform effect makes included) would take the export's placed symbol art past 16,000,000 objects, or when its baked art would draw deeper than the 128 levels a document may draw; these are VectorCraft's own limits. Before, these exports wrote the art without its effects (effects `bake::placing_symbols_bakes_their_art_where_each_instance_places_it`, `placing_symbols_reaches_the_art_of_nested_instances`, `placing_a_symbol_that_holds_itself_stops`, `placing_symbols_keeps_dynamic_instances_changes`, `placing_symbols_keeps_the_instances_stain`, `placing_symbols_counts_the_baked_art`, `placing_symbols_counts_nested_art_once`, `placing_symbols_keeps_the_baked_art_within_the_drawn_depth`; doc `depth::tests::drawn_heights_measure_objects_outside_the_layers`; PDF, print, EPS and DXF `symbol_art_effects_are_written_where_they_draw`, `symbol_art_effects_print_where_they_draw`; PDF `a_stained_instance_keeps_its_stain_when_its_art_has_effects`). 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. From 1c973dc14170d2d3daacf626439dc75f4a307016 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 02:36:33 -0800 Subject: [PATCH 24/44] SVG import: absolutely placed characters in right-to-left text land on their x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An absolutely placed character starts its own text chunk, anchored on its own x (SVG 1.1 text chunks). In a right-to-left line the import kerned the character before each placed one as if kerning moved the placed character right, but a right-to-left glyph takes its kerning at its left, so the correction moved the wrong glyph. In , אב on a second line put the right edge of א at 393.48 instead of 100. The placement now works in drawing order: each kerning is a cut after its character (before it when it runs right to left), each gap between two placed neighbors takes one cut, and the line's left edge is measured again every round. Corrections are divided by the character's horizontal scale, so text stretched with lengthAdjust="spacingAndGlyphs" lands on its x too. A left-to-right character drawn between two placed right-to-left ones has no kerning slot before the next one, which then keeps its place in the line; the import warns about it. --- crates/svg/src/import/text.rs | 166 +++++++++++++++++++++++------ crates/svg/tests/it/import_text.rs | 110 ++++++++++++++++++- docs/target-app-parity.md | 1 + 3 files changed, 241 insertions(+), 36 deletions(-) diff --git a/crates/svg/src/import/text.rs b/crates/svg/src/import/text.rs index 1a221a69f1..6b1ce0c063 100644 --- a/crates/svg/src/import/text.rs +++ b/crates/svg/src/import/text.rs @@ -40,7 +40,7 @@ use vectorcraft_color::{Color, Paint}; use vectorcraft_doc::{CharStyle, Dash, Justify, LineCap, LineJoin, ParaDirection, ParaStyle, StrokeLayer, TextKind, TextObject, TextRun}; use vectorcraft_geom::kurbo::ParamCurveArclen; use vectorcraft_geom::{Affine, BezPath, PathData, Point, Rect}; -use vectorcraft_text::{FontDb, TextLayout}; +use vectorcraft_text::{FontDb, PositionedGlyph, TextLayout, spacing_scale}; use super::css::{Styles, XNode}; use super::{DEFAULT_FONT_SIZE, PT_PER_IN, href, vector_effect}; @@ -1435,7 +1435,12 @@ 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, &pin, origin.x, &mut breaks); + let (justify, left, starts, unplaced) = lines(&mut cells, &x, &y, &dx, &dy, &anchor_of, &pin, origin.x, &mut breaks); + if unplaced > 0 { + warnings.push(format!( + "{label}: {unplaced} absolutely placed character(s) between characters of the other direction keep their place in the line" + )); + } obj.para.justify = justify; obj.xf = Affine::translate((finite(left), finite(origin.y))); // Lines anchored differently or at different x (paragraphs aligned or indented @@ -1568,7 +1573,8 @@ fn leading_tolerance(spacing: f64) -> f64 { /// character, alignment (as for [`justify_of`]) and anchor x. `anchor_of` gives where the chunk /// 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. +/// and its anchor x is its left edge. Also returns how many absolutely placed characters kerning +/// cannot put on their x. #[allow(clippy::too_many_arguments)] fn lines( cells: &mut [Cell], @@ -1580,7 +1586,7 @@ fn lines( pin: &dyn Fn(&mut TextObject), x0: f64, breaks: &mut Vec, -) -> (Justify, f64, Vec<(usize, f64, f64)>) { +) -> (Justify, f64, Vec<(usize, f64, f64)>, usize) { let n = cells.len(); let y0 = y[0].unwrap_or(0.0) + dy[0]; let (mut base, mut cur) = (y0, y0); @@ -1623,7 +1629,7 @@ fn lines( } if targets.is_empty() { let starts = lines.iter().map(|&(s, lx)| (s, anchor_of(s), lx)).collect(); - return (justify_of(anchor_of(0)), x0, starts); + return (justify_of(anchor_of(0)), x0, starts, 0); } // Absolutely placed characters: lay the text out left-aligned and kern each one onto its x. // SVG anchors every absolutely placed chunk on its own, so a chunk not anchored at its left @@ -1633,59 +1639,122 @@ fn lines( // Chunk starts in order: line starts and absolutely placed characters. let mut starts: Vec = lines.iter().map(|l| l.0).chain(targets.iter().map(|t| t.0)).collect(); starts.sort_unstable(); - // The left and right edges of the chunk starting at character `s`, from each character's pen and end. - let edges = |s: usize, pens: &[Option], ends: &[Option]| { + // The left and right edges of the chunk starting at character `s`, and the character drawn at its left. + let edges = |s: usize, pens: &[Option]| { let e = starts.get(starts.partition_point(|&c| c <= s)).copied().unwrap_or(n); - let left = (s..e).filter_map(|j| pens[j]).fold(f64::INFINITY, f64::min); - let right = (s..e).filter_map(|j| ends[j]).fold(f64::NEG_INFINITY, f64::max); - (left.is_finite() && right.is_finite()).then_some((left, right)) + let (mut left, mut right, mut at) = (f64::INFINITY, f64::NEG_INFINITY, s); + for (j, p) in pens.iter().enumerate().take(e).skip(s) { + let Some(p) = p else { continue }; + if p.start < left { + (left, at) = (p.start, j); + } + right = right.max(p.end); + } + (left.is_finite() && right.is_finite()).then_some((left, right, at)) }; let mut line_left: Vec = lines.iter().map(|l| l.1).collect(); + // Where each line's first chunk's left edge goes. + let mut first_left: Vec = vec![]; // Where each absolutely placed character's pen goes, from the left edge of its line's first chunk. let mut wants: Vec<(usize, f64)> = vec![]; + // Absolutely placed characters drawn right next to a placed neighbor with no kerning between + // them (a left-to-right character between right-to-left ones): they stay where the layout puts them. + let mut unplaced = 0; // Kerning changes shaping (pair kerning is off for manually kerned characters): repeat until // every character sits on its x. for round in 0..4 { let (runs, bytes) = assemble(cells, breaks); let mut text = with_runs(runs.0); pin(&mut text); - let (pens, ends) = pens(&measure(&text), &bytes); + let pens = pens(&measure(&text), &bytes, cells); // The edges of each line's first chunk, found once a round. - let first: Vec> = lines.iter().map(|&(s, _)| edges(s, &pens, &ends)).collect(); + let first: Vec> = lines.iter().map(|&(s, _)| edges(s, &pens)).collect(); if round == 0 { // The chunks as they lie before any is moved: each one's left edge on its anchor. - let left_at = |s: usize, x: f64, chunk: Option<(f64, f64)>| chunk.map_or(x, |(left, right)| x - anchor_of(s) * (right - left)); - let first_left: Vec = lines.iter().zip(&first).map(|(&(s, lx), &chunk)| left_at(s, lx, chunk)).collect(); + let left_at = |s: usize, x: f64, chunk: Option<(f64, f64, usize)>| chunk.map_or(x, |(left, right, _)| x - anchor_of(s) * (right - left)); + first_left = lines.iter().zip(&first).map(|(&(s, lx), &chunk)| left_at(s, lx, chunk)).collect(); wants = targets .iter() .map(|&(i, x)| { - let chunk = edges(i, &pens, &ends); + let chunk = edges(i, &pens); // The pen of a right-to-left chunk's first character is right of the chunk's left edge. - let pen = pens[i].zip(chunk).map_or(0.0, |(p, (left, _))| p - left); + let pen = pens[i].zip(chunk).map_or(0.0, |(p, (left, ..))| p.start - left); (i, left_at(i, x, chunk) + pen - first_left[line_of(i)]) }) .collect(); - // A line's layout starts at the line's left edge, and its first chunk's left edge is `left` from there. - line_left = first_left.iter().zip(&first).map(|(&x, chunk)| x - chunk.map_or(0.0, |(left, _)| left)).collect(); } - let (mut line, mut moved, mut worst) = (usize::MAX, 0.0, 0.0f64); - for &(i, want) in &wants { - let l = line_of(i); - if l != line { - (line, moved) = (l, 0.0); + // A line's layout starts at the line's left edge, and its first chunk's left edge is `left` + // from there (right-to-left kerning can move that chunk). + line_left = first_left.iter().zip(&first).map(|(&x, chunk)| x - chunk.map_or(0.0, |(left, ..)| left)).collect(); + // Kerning a character widens its advance to the right: the glyphs drawn right of it move + // right, and so does a right-to-left glyph's own left edge (its kerning is at its left). In + // drawing order the kerning before an absolutely placed character is a cut after the + // character before it (before that character when it runs right to left), and what is drawn + // after the cut moves. Places in drawing order are doubled (from 2) to put cuts between them. + // Each gap between two placed characters drawn one after the other takes the kerning of one + // cut in it, so placing a character moves none placed before it. Kerning is drawn at the + // character's horizontal scale, so each correction is divided by it. + let mut worst = 0.0f64; + let mut from = 0; + unplaced = 0; + while from < wants.len() { + let l = line_of(wants[from].0); + let to = from + wants[from..].partition_point(|&(i, _)| line_of(i) == l); + let on_line = &wants[from..to]; + from = to; + let Some((p0, _, at)) = first[l] else { continue }; + let Some(home) = pens[at].map(|p| 2 * p.rank + 2) else { continue }; + // A character drawn as part of the glyph before it (a mark) is kerned where that glyph is. + // The kerning stays on the character just before the placed one: a mark whose kerning + // differs from its base's is drawn as a glyph of its own, and kerning the base would put + // the space between the base and its mark. + // Each search stops at the nearest earlier character with a glyph, at the latest the + // placed character before it, so the searches of a line read each character once. + let mut cuts: Vec<(usize, usize)> = on_line + .iter() + .filter(|&&(i, _)| pens[i].is_some()) + .filter_map(|&(i, _)| (lines[l].0..i).rev().find_map(|j| pens[j]).map(|k| (2 * k.rank + if k.rtl { 1 } else { 3 }, i - 1))) + .collect(); + cuts.sort_unstable(); + let mut points: Vec<(usize, f64, f64)> = + on_line.iter().filter_map(|&(i, want)| pens[i].map(|p| (2 * p.rank + 2, p.start, want))).collect(); + points.sort_by_key(|p| p.0); + // The character kerned at the first cut between places `a` and `b`. + let cut = |a: usize, b: usize| cuts.get(cuts.partition_point(|c| c.0 <= a)).filter(|c| c.0 < b).map(|c| c.1); + let split = points.partition_point(|p| p.0 < home); + // Drawn right of the line's first chunk: kerning moves the character away from it. + let (mut prev, mut moved) = (home, 0.0); + for &(place, start, want) in &points[split..] { + if let Some(k) = cut(prev, place) { + let d = want - (start - p0 + moved); + cells[k].kern += d / spacing_scale(&cells[k].style, false); + moved += d; + worst = worst.max(d.abs()); + } else { + unplaced += 1; + } + prev = place; + } + // Drawn left of it: kerning moves the chunk (and what lies between) away from the character. + (prev, moved) = (home, 0.0); + for &(place, start, want) in points[..split].iter().rev() { + if let Some(k) = cut(place, prev) { + let d = start - p0 - moved - want; + cells[k].kern += d / spacing_scale(&cells[k].style, false); + moved += d; + worst = worst.max(d.abs()); + } else { + unplaced += 1; + } + prev = place; } - let (Some(p), Some((p0, _))) = (pens[i], first[l]) else { continue }; - let d = want - (p - p0 + moved); - cells[i - 1].kern += d; - moved += d; - worst = worst.max(d.abs()); } if worst < 0.01 { break; } } let starts = lines.iter().zip(&line_left).map(|(&(s, _), &lx)| (s, 0.0, lx)).collect(); - (Justify::Left, line_left[0], starts) + (Justify::Left, line_left[0], starts, unplaced) } /// Centred and right-aligned lines of point type `obj` where a reader draws them. `text-anchor` @@ -1794,13 +1863,40 @@ fn advance(t: &TextObject) -> f64 { measure(t).glyphs.iter().map(|g| g.advance).sum() } -/// Pen x (start) and end x of the glyph that starts at each character's byte offset. -fn pens(lay: &TextLayout, bytes: &[usize]) -> (Vec>, Vec>) { - let mut at: HashMap = HashMap::with_capacity(lay.glyphs.len()); - for g in &lay.glyphs { - at.entry(g.byte).or_insert((g.origin.x, g.origin.x + g.advance)); +/// Where the glyph that starts a character is drawn on a line of point type. +#[derive(Clone, Copy)] +struct Pen { + /// Its left edge. Manual kerning on a right-to-left glyph is at its left, and the glyph is + /// drawn right of it: its left edge is past that kerning. + start: f64, + /// The right edge of its advance. + end: f64, + /// Its place in the order its line is drawn in, from the left. + rank: usize, + rtl: bool, +} + +/// The [`Pen`] of the glyph that starts at each character's byte offset (`bytes`, parallel to `cells`). +fn pens(lay: &TextLayout, bytes: &[usize], cells: &[Cell]) -> Vec> { + // A line's glyphs are laid out in the order they are drawn. + let mut at: HashMap = HashMap::with_capacity(lay.glyphs.len()); + for (rank, g) in lay.glyphs.iter().enumerate() { + at.entry(g.byte).or_insert((rank, g)); } - bytes.iter().map(|b| at.get(b).map_or((None, None), |&(s, e)| (Some(s), Some(e)))).unzip() + bytes + .iter() + .zip(cells) + .map(|(b, c)| { + let &(rank, g) = at.get(b)?; + let mut start = g.origin.x; + if g.rtl { + let st = c.final_style(); + let kern = st.kerning.unwrap_or(0.0) / 1000.0 * st.size * spacing_scale(&st, false); + start += finite(kern); + } + Some(Pen { start, end: g.origin.x + g.advance, rank, rtl: g.rtl }) + }) + .collect() } type Key = (Option, Option); diff --git a/crates/svg/tests/it/import_text.rs b/crates/svg/tests/it/import_text.rs index 472d7517e4..b4cc379506 100644 --- a/crates/svg/tests/it/import_text.rs +++ b/crates/svg/tests/it/import_text.rs @@ -5,7 +5,7 @@ use vectorcraft_color::{GradientKind, Paint}; use vectorcraft_doc::{CharStyle, Document, Justify, Node, NodeKind, TextKind, TextObject}; use vectorcraft_geom::{Point, Rect}; use vectorcraft_svg::{import, import_with_report}; -use vectorcraft_text::{FontDb, layout}; +use vectorcraft_text::{FontDb, layout, spacing_scale}; fn svg(body: &str) -> String { format!(r#"{body}"#) @@ -719,6 +719,114 @@ fn absolutely_placed_rtl_chunks_of_several_characters_anchor_by_their_visual_edg } } +/// Document-space left and right edges of the natural advance of the characters in `bytes` (the +/// manual kerning that places the next chunk left out: it is at a glyph's right when the glyph runs +/// left to right and at its left when it runs right to left). +fn chunk_edges(t: &TextObject, bytes: std::ops::Range) -> (f64, f64) { + let lay = layout(FontDb::global(), t); + let mut starts = vec![]; + let mut at = 0; + for r in &t.runs { + starts.push((at, &r.style)); + at += r.text.len(); + } + let style = |byte: usize| starts.iter().rev().find(|(s, _)| *s <= byte).map(|(_, st)| *st).unwrap(); + let (mut left, mut right) = (f64::INFINITY, f64::NEG_INFINITY); + for g in lay.glyphs.iter().filter(|g| bytes.contains(&g.byte)) { + let st = style(g.byte); + let kern = st.kerning.unwrap_or(0.0) / 1000.0 * st.size * spacing_scale(st, false); + let x = (t.xf * g.origin).x; + let (l, r) = if g.rtl { (x + kern, x + g.advance) } else { (x, x + g.advance - kern) }; + (left, right) = (left.min(l), right.max(r)); + } + assert!(left.is_finite(), "no glyph in {bytes:?}"); + (left, right) +} + +/// SVG 1.1 §10.4 and SVG 2 §11.10.1.1 (https://www.w3.org/TR/SVG2/text.html#TextAnchoringProperties): +/// every absolutely placed character starts a chunk anchored on its own x, also in right-to-left +/// text whose characters run right to left, on any line. +#[test] +fn absolutely_placed_right_to_left_characters_sit_on_their_own_x() { + // (markup, share of each chunk's advance left of its x, each chunk's bytes and x) + let two_lines = |anchor: &str| { + format!( + r#"abאב"# + ) + }; + type Chunks = Vec<(std::ops::Range, f64)>; + let cases: Vec<(String, f64, Chunks)> = vec![ + (two_lines("start"), 1.0, vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)]), + (two_lines("middle"), 0.5, vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)]), + (two_lines("end"), 0.0, vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)]), + (r#"אב"#.into(), 1.0, vec![(0..2, 100.0), (2..4, 160.0)]), + (r#"אב"#.into(), 0.0, vec![(0..2, 100.0), (2..4, 160.0)]), + // Three lines, the characters of the last out of order and overlapping. + ( + r#"אבגדהוז"#.into(), + 1.0, + vec![(0..4, 300.0), (5..7, 60.0), (7..9, 120.0), (10..12, 200.0), (12..14, 90.0), (14..16, 205.0)], + ), + // Left-to-right characters among right-to-left ones on the same line. + (r#"אbc"#.into(), 1.0, vec![(0..2, 100.0), (2..3, 160.0), (3..4, 220.0)]), + // Chunks of several characters. + ( + r#"abאבגד"#.into(), + 1.0, + vec![(0..2, 20.0), (3..7, 100.0), (7..11, 250.0)], + ), + ( + r#"abאבגד"#.into(), + 0.5, + vec![(0..2, 20.0), (3..7, 100.0), (7..11, 250.0)], + ), + // A placed character after a letter with a combining mark. + ( + r#"אָב"#.into(), + 1.0, + vec![(4..6, 160.0)], + ), + (r#"éb"#.into(), 0.0, vec![(3..4, 160.0)]), + // Characters stretched by `lengthAdjust`, whose kerning is drawn at their scale. + ( + r#"abאב"#.into(), + 1.0, + vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)], + ), + // Left to right, the right-to-left characters still each start at their own x. + ( + r#"abאב"#.into(), + 0.0, + vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)], + ), + ( + r#"abcd"#.into(), + 0.0, + vec![(0..2, 20.0), (3..4, 100.0), (4..5, 160.0)], + ), + ]; + let unplaced = |w: &[String]| w.iter().any(|w| w.contains("absolutely placed")); + for (body, share, chunks) in cases { + let (d, w) = import_with_report(&svg(&body)).unwrap(); + assert!(!unplaced(&w), "{body}: {w:?}"); + let t = only_text(&d); + if body.contains("lengthAdjust") { + assert!(t.runs.iter().any(|r| r.style.h_scale < 99.0), "{body}: {:?}", t.runs); + } + for (bytes, x) in chunks { + let (left, right) = chunk_edges(t, bytes.clone()); + let want = x - share * (right - left); + assert!(close(left, want, 0.05), "{body}: the chunk {bytes:?} spans {left}..{right}, so it does not start at {want}"); + } + } + // A left-to-right character between right-to-left ones is drawn right next to the one after + // it, with no kerning between them: that one keeps its place in the line, and a warning says so. + let (d, w) = import_with_report(&svg(r#"אbג"#)).unwrap(); + assert!(unplaced(&w), "{w:?}"); + let (left, right) = chunk_edges(only_text(&d), 2..3); + assert!(close(right, 160.0, 0.05), "b spans {left}..{right}"); +} + #[test] fn a_small_line_gap_in_tiny_type_keeps_its_leading() { // 0.0085 against an auto leading of 0.006 differs by less than 0.01 but is not a whole multiple of it. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7e36c7f318..fd11033247 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1658,6 +1658,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 `