mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge remote-tracking branch 'origin/main' into feat/indic-composers
# Conflicts: # docs/gaps.md # docs/target-app-parity.md
This commit is contained in:
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Tech Circuit brush library, G5; previously: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (G5: built-in Neon & Glow graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); G5 estimate unchanged (3–13 h); previously: Tech Circuit brush library, G5; previously: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs
|
||||
on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON
|
||||
@@ -225,6 +225,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-15 | minor | Graphic Styles: built-in Neon & Glow graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); G5 estimate unchanged (3–13 h). |
|
||||
| 2026-11-14 | minor | Tech Circuit adds three original procedural brushes (Circuit Trace, Bus Lines, Terminal Nodes); ten built-in brush libraries. G5 remaining estimate 3–13 h (estimated); parity scores and headline totals unchanged. |
|
||||
| 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. |
|
||||
| 2026-11-10 | minor | Tidy-up, no content change: the unheaded change bullets at the top of the file and a revision row inside the headline-numbers table move to this table under their commit dates. Scores and estimates unchanged. |
|
||||
|
||||
@@ -596,6 +596,9 @@ struct Walk<'a> {
|
||||
clips: Vec<Region>,
|
||||
/// The points those masks hold ([`MAX_MASK_POINTS`]).
|
||||
mask_points: usize,
|
||||
/// Set while the stroke pieces of a clipping path whose fill paints are drawn: they close, as
|
||||
/// its fill closes its outline.
|
||||
closing: bool,
|
||||
runs: Vec<Run>,
|
||||
points: usize,
|
||||
clip_tests: u64,
|
||||
@@ -624,6 +627,7 @@ impl<'a> Walk<'a> {
|
||||
tolerance,
|
||||
clips: vec![],
|
||||
mask_points: 0,
|
||||
closing: false,
|
||||
runs: vec![],
|
||||
points: 0,
|
||||
clip_tests: 0,
|
||||
@@ -827,7 +831,9 @@ impl<'a> Walk<'a> {
|
||||
let mut paint = first.clip_paint();
|
||||
// A stroke at 0% opacity isn't drawn, so the clipping path is drawn in its fill.
|
||||
let type_stroke = |s: &Node| matches!(&s.kind, NodeKind::Text(t) if t.runs.iter().any(|r| r.style.has_stroke()));
|
||||
paint.stroke = paint.stroke.filter(|s| top_stroke(s).is_some() || type_stroke(s));
|
||||
// The strokes of a clipping path whose stroke effects were applied are a group of pieces,
|
||||
// which effects around it may have put in groups of their own.
|
||||
paint.stroke = paint.stroke.filter(|s| top_stroke(s).is_some() || type_stroke(s) || strokes_in_pieces(s));
|
||||
if paint.stroke.is_none()
|
||||
&& let Some(fill) = &paint.fill
|
||||
{
|
||||
@@ -852,9 +858,16 @@ impl<'a> Walk<'a> {
|
||||
None => {}
|
||||
}
|
||||
// Drawn whole where its stroke paints: a fill closes its outline, and type's characters
|
||||
// without a stroke are drawn too.
|
||||
if paint.stroke.is_some() {
|
||||
self.node(first, false);
|
||||
// without a stroke are drawn too. Pieces of a clipping path whose fill and stroke effects
|
||||
// were applied are drawn in its stroke pieces alone, closed where its fill paints.
|
||||
if let Some(stroke) = &paint.stroke {
|
||||
if first.extras().clip_pieces {
|
||||
let closing = std::mem::replace(&mut self.closing, paint.fill.as_ref().is_some_and(fills_in_pieces));
|
||||
self.node(stroke, false);
|
||||
self.closing = closing;
|
||||
} else {
|
||||
self.node(first, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,7 +887,8 @@ impl<'a> Walk<'a> {
|
||||
|
||||
/// A path drawn in its color: the topmost stroke's that paints, else the topmost fill's that
|
||||
/// paints (visible, with a paint and some opacity). A filled path's every subpath is closed,
|
||||
/// as its fill closes it, also when its stroke gives the color.
|
||||
/// as its fill closes it, also when its stroke gives the color, and so is each stroke piece
|
||||
/// of a clipping path whose fill paints ([`Walk::closing`]).
|
||||
fn shape(&mut self, n: &Node, bp: &BezPath) {
|
||||
if bp.elements().is_empty() {
|
||||
return;
|
||||
@@ -896,7 +910,7 @@ impl<'a> Walk<'a> {
|
||||
(None, None) => return,
|
||||
};
|
||||
let rgb = self.flat(paint);
|
||||
self.draw(rgb, bp, fill.is_some());
|
||||
self.draw(rgb, bp, fill.is_some() || self.closing);
|
||||
}
|
||||
|
||||
/// `bp` drawn in `rgb`: flattened (with `close` every subpath closed), then cut and added.
|
||||
@@ -1146,6 +1160,18 @@ fn top_stroke(n: &Node) -> Option<&StrokeLayer> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a member of `n`, at any depth of its groups, has a stroke that paints ([`top_stroke`]),
|
||||
/// with it and the groups around it shown and not fully transparent.
|
||||
fn strokes_in_pieces(n: &Node) -> bool {
|
||||
n.children().is_some_and(|ch| ch.iter().filter(|c| c.visible && c.opacity > 0.0).any(|c| top_stroke(c).is_some() || strokes_in_pieces(c)))
|
||||
}
|
||||
|
||||
/// Whether a member of `n`, at any depth of its groups, has a fill that paints ([`top_fill`]),
|
||||
/// with it and the groups around it shown and not fully transparent.
|
||||
fn fills_in_pieces(n: &Node) -> bool {
|
||||
n.children().is_some_and(|ch| ch.iter().filter(|c| c.visible && c.opacity > 0.0).any(|c| top_fill(c).is_some() || fills_in_pieces(c)))
|
||||
}
|
||||
|
||||
/// The paint of the topmost fill of `n` that paints: visible, with a paint and some opacity.
|
||||
fn top_fill(n: &Node) -> Option<&Paint> {
|
||||
n.appearance.items.iter().rev().find_map(|i| match i {
|
||||
|
||||
@@ -747,6 +747,86 @@ fn a_clipping_path_is_drawn_where_it_paints() {
|
||||
});
|
||||
let t = text(&plot(&open_doc, &opts(&open_doc)));
|
||||
assert!(t.ends_with("SP2;PU282,1129;PD564,1129,564,847,282,1129;PU;SP0;"), "{t}");
|
||||
// A stroke with a geometry effect of its own is drawn once, over the art it clips, in its color.
|
||||
let moved = doc_with(|d| {
|
||||
let (blue, red) = (Paint::solid(Color::rgb8(0, 0, 255)), Paint::solid(Color::rgb8(255, 0, 0)));
|
||||
let mut clip = Node::path(d.alloc_id(), rect(20.0, 20.0, 40.0, 40.0), Appearance::basic(blue, red, 1.0));
|
||||
for item in &mut clip.appearance.items {
|
||||
if let AppearanceItem::Stroke(s) = item {
|
||||
let params = serde_json::json!({"moveH": 5});
|
||||
s.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params, visible: true });
|
||||
}
|
||||
}
|
||||
let clipped = Node::path(d.alloc_id(), line((0.0, 30.0), (200.0, 30.0)), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0));
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(clip), Arc::new(clipped)]);
|
||||
if let NodeKind::Group { clip, .. } = &mut group.kind {
|
||||
*clip = true;
|
||||
}
|
||||
vec![group]
|
||||
});
|
||||
assert_eq!(plot(&moved, &opts(&moved)).pens, [[0, 0, 0], [255, 0, 0]]);
|
||||
// So are the stroke's pieces when an effect around them put them in a group of their own.
|
||||
let grouped = |opacity: f32| {
|
||||
doc_with(|d| {
|
||||
// One path for each, carrying only that fill or stroke.
|
||||
let piece = |d: &mut Document, fill: bool| {
|
||||
let mut a = Appearance::basic(Paint::solid(Color::rgb8(0, 0, 255)), Paint::solid(Color::rgb8(255, 0, 0)), 1.0);
|
||||
a.items.retain(|i| matches!(i, AppearanceItem::Fill(_)) == fill);
|
||||
Arc::new(Node::path(d.alloc_id(), rect(20.0, 20.0, 40.0, 40.0), a))
|
||||
};
|
||||
let (fill, stroke) = (piece(d, true), piece(d, false));
|
||||
let mut copies = Node::group(d.alloc_id(), vec![stroke]);
|
||||
copies.opacity = opacity;
|
||||
let mut clip = Node::group(d.alloc_id(), vec![fill, Arc::new(copies)]);
|
||||
clip.extras_mut().clip_pieces = true;
|
||||
let black = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
|
||||
let clipped = Node::path(d.alloc_id(), line((0.0, 30.0), (200.0, 30.0)), black);
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(clip), Arc::new(clipped)]);
|
||||
if let NodeKind::Group { clip, .. } = &mut group.kind {
|
||||
*clip = true;
|
||||
}
|
||||
vec![group]
|
||||
})
|
||||
};
|
||||
let shown = grouped(1.0);
|
||||
assert_eq!(plot(&shown, &opts(&shown)).pens, [[0, 0, 0], [255, 0, 0]]);
|
||||
// In a group at 0% opacity they are not drawn, and the clipping path is drawn in its fill.
|
||||
let faded = grouped(0.0);
|
||||
assert_eq!(plot(&faded, &opts(&faded)).pens, [[0, 0, 255], [0, 0, 0]]);
|
||||
// An open clipping path with a fill is drawn closed also where its stroke's effect moved it.
|
||||
let open_moved = |fill_opacity: f32| {
|
||||
doc_with(|d| {
|
||||
let mut open = BezPath::new();
|
||||
open.move_to((20.0, 20.0));
|
||||
open.line_to((40.0, 20.0));
|
||||
open.line_to((40.0, 40.0));
|
||||
let a = Appearance::basic(Paint::solid(Color::rgb8(0, 0, 255)), Paint::solid(Color::rgb8(255, 0, 0)), 1.0);
|
||||
let mut clip = Node::path(d.alloc_id(), PathData::from_bezpath(&open), a);
|
||||
for item in &mut clip.appearance.items {
|
||||
match item {
|
||||
AppearanceItem::Fill(f) => f.opacity = fill_opacity,
|
||||
AppearanceItem::Stroke(s) => {
|
||||
let params = serde_json::json!({"moveH": 5});
|
||||
s.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params, visible: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
let black = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
|
||||
let clipped = Node::path(d.alloc_id(), line((0.0, 30.0), (200.0, 30.0)), black);
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(clip), Arc::new(clipped)]);
|
||||
if let NodeKind::Group { clip, .. } = &mut group.kind {
|
||||
*clip = true;
|
||||
}
|
||||
vec![group]
|
||||
})
|
||||
};
|
||||
let filled = open_moved(1.0);
|
||||
let t = text(&plot(&filled, &opts(&filled)));
|
||||
assert!(t.ends_with("SP2;PU353,1129;PD635,1129,635,847,353,1129;PU;SP0;"), "{t}");
|
||||
// A fill at 0% opacity does not close it.
|
||||
let unfilled = open_moved(0.0);
|
||||
let t = text(&plot(&unfilled, &opts(&unfilled)));
|
||||
assert!(t.ends_with("SP2;PU353,1129;PD635,1129,635,847;PU;SP0;"), "{t}");
|
||||
}
|
||||
|
||||
/// A symbol `art` and an instance of it at its place.
|
||||
|
||||
@@ -74,19 +74,32 @@ impl Node {
|
||||
/// the SVG and PDF writers: its fills, painted behind the clipped art, and its strokes, painted
|
||||
/// over it (unclipped). Each part is this object keeping only those appearance items (type:
|
||||
/// also only its characters' fills or strokes); `None` where it paints nothing, as right after
|
||||
/// Make Clipping Mask. Only paths, compound paths and type paint as clipping paths.
|
||||
/// Make Clipping Mask. Only paths, compound paths and type paint as clipping paths, and so does
|
||||
/// a group of the pieces a clipping path's fills and strokes draw ([`crate::NodeExtras::clip_pieces`]),
|
||||
/// even after effects around it split or copy those pieces into groups: each part is then that
|
||||
/// group keeping only the pieces of those fills or strokes.
|
||||
pub fn clip_paint(&self) -> ClipPaint {
|
||||
use crate::appearance::AppearanceItem;
|
||||
let item = |fill: bool, i: &AppearanceItem| match i {
|
||||
AppearanceItem::Fill(f) => fill && f.visible && !f.paint.is_none(),
|
||||
AppearanceItem::Stroke(s) => !fill && s.visible && !s.paint.is_none() && s.width > 0.0,
|
||||
};
|
||||
if matches!(self.kind, NodeKind::Group { clip: false, .. }) && self.extras().clip_pieces {
|
||||
let part = |fill: bool| {
|
||||
let mut paints = false;
|
||||
let mut n = pieces_of(self, fill, &|i| item(fill, i), &mut paints)?;
|
||||
n.extras_mut().clip_pieces = false;
|
||||
paints.then_some(n)
|
||||
};
|
||||
return ClipPaint { fill: part(true), stroke: part(false) };
|
||||
}
|
||||
let runs = match &self.kind {
|
||||
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => None,
|
||||
NodeKind::Text(t) => Some(&t.runs),
|
||||
_ => return ClipPaint::default(),
|
||||
};
|
||||
let paints = |fill: bool| {
|
||||
let item = |i: &AppearanceItem| match i {
|
||||
AppearanceItem::Fill(f) => fill && f.visible && !f.paint.is_none(),
|
||||
AppearanceItem::Stroke(s) => !fill && s.visible && !s.paint.is_none() && s.width > 0.0,
|
||||
};
|
||||
let item = |i: &AppearanceItem| item(fill, i);
|
||||
let run = |r: &crate::text::TextRun| if fill { !r.style.fill.is_none() } else { !r.style.stroke.is_none() && r.style.stroke_width > 0.0 };
|
||||
self.appearance.items.iter().any(item) || runs.is_some_and(|rs| rs.iter().any(run))
|
||||
};
|
||||
@@ -110,6 +123,30 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// `n`, a group of a clipping path's pieces or one of them, keeping only the pieces of its fills
|
||||
/// (`fill`) or of its strokes, at every depth of the groups an effect put them in; `None` when it
|
||||
/// holds none. `paints` is set when one of the kept fills or strokes paints.
|
||||
fn pieces_of(n: &Node, fill: bool, paints_item: &dyn Fn(&crate::appearance::AppearanceItem) -> bool, paints: &mut bool) -> Option<Node> {
|
||||
use crate::appearance::AppearanceItem;
|
||||
if let NodeKind::Group { children, clip: false } = &n.kind {
|
||||
let kept: Vec<_> = children.iter().filter_map(|c| pieces_of(c, fill, paints_item, paints).map(std::sync::Arc::new)).collect();
|
||||
if kept.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut m = n.clone();
|
||||
if let Some(ch) = m.children_mut() {
|
||||
*ch = kept;
|
||||
}
|
||||
return Some(m);
|
||||
}
|
||||
let items = &n.appearance.items;
|
||||
if !items.iter().any(|i| matches!(i, AppearanceItem::Fill(_)) == fill) {
|
||||
return None;
|
||||
}
|
||||
*paints |= items.iter().any(paints_item);
|
||||
Some(n.clone())
|
||||
}
|
||||
|
||||
/// What a clipping path paints ([`Node::clip_paint`]).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ClipPaint {
|
||||
|
||||
@@ -79,6 +79,10 @@ pub struct NodeExtras {
|
||||
/// knockout group its knockout shape is the whole area of its triangle, not only where the
|
||||
/// image is opaque ([`Node::knockout_outline`]).
|
||||
pub warp_piece: bool,
|
||||
/// A group of the pieces a clipping path's fills and strokes draw once their geometry effects
|
||||
/// are applied, one path or compound path per fill and stroke: as the clipping path of a clip group it paints
|
||||
/// those pieces as the path paints its fills and strokes ([`Node::clip_paint`]).
|
||||
pub clip_pieces: bool,
|
||||
}
|
||||
|
||||
/// No extras: what [`Node::extras`] gives for a node without any.
|
||||
@@ -108,6 +112,7 @@ pub(super) static NO_EXTRAS: NodeExtras = NodeExtras {
|
||||
pixel_aligned: false,
|
||||
color_mask: None,
|
||||
warp_piece: false,
|
||||
clip_pieces: false,
|
||||
};
|
||||
|
||||
/// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal
|
||||
|
||||
@@ -369,7 +369,7 @@ impl Node {
|
||||
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
|
||||
// A clipping path that paints (see `clip_paint`) strokes over the clip edge.
|
||||
let clip = children.first()?;
|
||||
if matches!(clip.kind, NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_)) {
|
||||
if matches!(clip.kind, NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_)) || clip.extras().clip_pieces {
|
||||
clip.visual(symbols)
|
||||
} else {
|
||||
clip.geometric(symbols)
|
||||
|
||||
@@ -24,6 +24,7 @@ fn extras_are_written_flat_in_order() {
|
||||
e.envelope_edit_margin = Some([0.5, 0.5, 0.25, 0.25]);
|
||||
e.pixel_aligned = true;
|
||||
e.warp_piece = true;
|
||||
e.clip_pieces = true;
|
||||
}
|
||||
/// The top-level keys of a JSON object, in the order written.
|
||||
struct Keys(Vec<String>);
|
||||
@@ -71,11 +72,13 @@ fn extras_are_written_flat_in_order() {
|
||||
"envelope_edit_margin",
|
||||
"pixel_aligned",
|
||||
"warp_piece",
|
||||
"clip_pieces",
|
||||
];
|
||||
assert_eq!(keys, order);
|
||||
assert_eq!(v["shape_mode"], "exclude");
|
||||
assert_eq!(v["pixel_aligned"], true);
|
||||
assert_eq!(v["warp_piece"], true);
|
||||
assert_eq!(v["clip_pieces"], true);
|
||||
assert_eq!(serde_json::from_value::<Node>(v).unwrap(), n);
|
||||
let plain = Node::path(NodeId(9), shapes::rectangle(Rect::new(0.0, 0.0, 4.0, 2.0)), Appearance::default_art());
|
||||
let back: Node = serde_json::from_value(serde_json::to_value(&plain).unwrap()).unwrap();
|
||||
|
||||
@@ -57,6 +57,7 @@ impl Serialize for Node {
|
||||
is_default(&e.pixel_aligned),
|
||||
e.color_mask.is_none(),
|
||||
is_default(&e.warp_piece),
|
||||
is_default(&e.clip_pieces),
|
||||
];
|
||||
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
|
||||
let mut skip = skips.into_iter();
|
||||
@@ -102,6 +103,7 @@ impl Serialize for Node {
|
||||
field!("pixel_aligned", &e.pixel_aligned);
|
||||
field!("color_mask", &e.color_mask);
|
||||
field!("warp_piece", &e.warp_piece);
|
||||
field!("clip_pieces", &e.clip_pieces);
|
||||
st.end()
|
||||
}
|
||||
}
|
||||
@@ -249,6 +251,8 @@ pub(super) struct NodeWire {
|
||||
color_mask: Option<Box<crate::color_mask::ColorMask>>,
|
||||
#[serde(default)]
|
||||
warp_piece: bool,
|
||||
#[serde(default)]
|
||||
clip_pieces: bool,
|
||||
}
|
||||
|
||||
impl From<NodeWire> for Node {
|
||||
@@ -279,6 +283,7 @@ impl From<NodeWire> for Node {
|
||||
pixel_aligned: w.pixel_aligned,
|
||||
color_mask: w.color_mask,
|
||||
warp_piece: w.warp_piece,
|
||||
clip_pieces: w.clip_pieces,
|
||||
};
|
||||
Node {
|
||||
id: w.id,
|
||||
|
||||
@@ -162,6 +162,7 @@ pub const STYLE_LIBRARIES: &[BuiltinStyleLibrary] = &[
|
||||
BuiltinStyleLibrary { id: "blends-transparency", name: "Blends and Transparency", make: blends_transparency },
|
||||
BuiltinStyleLibrary { id: "game-hud", name: "Game HUD", make: game_hud },
|
||||
BuiltinStyleLibrary { id: "status-effects", name: "Status Effects", make: status_effects },
|
||||
BuiltinStyleLibrary { id: "neon-glow", name: "Neon & Glow", make: neon_glow },
|
||||
];
|
||||
|
||||
/// Built-in library `id`, made once.
|
||||
@@ -619,6 +620,44 @@ fn status_effects() -> Vec<GraphicStyle> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Neon & Glow: original multi-layer procedural styles with neon tubes, laser beams, and glowing halos.
|
||||
fn neon_glow() -> Vec<GraphicStyle> {
|
||||
vec![
|
||||
style(
|
||||
"Cyan Neon",
|
||||
vec![S(StrokeLayer { opacity: 0.7, ..round("#00e5ff", 6.0) }), S(round("#ffffff", 1.5))],
|
||||
vec![fx("stylize.dropShadow", json!({"mode": "screen", "opacity": 85.0, "x": 0.0, "y": 0.0, "blur": 8.0, "color": "#00e5ff"}))],
|
||||
),
|
||||
GraphicStyle {
|
||||
blend: BlendMode::Screen,
|
||||
..style(
|
||||
"Magenta Laser",
|
||||
vec![S(StrokeLayer { blend: BlendMode::Screen, ..round("#ff007f", 5.0) }), S(round("#ffffff", 1.5))],
|
||||
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#ff007f"}))],
|
||||
)
|
||||
},
|
||||
style(
|
||||
"Amber Tube",
|
||||
vec![S(StrokeLayer { opacity: 0.8, ..round("#ff8c00", 6.0) }), S(round("#ffea00", 1.5))],
|
||||
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#ff8c00"}))],
|
||||
),
|
||||
style(
|
||||
"Electric Lime",
|
||||
vec![S(StrokeLayer { opacity: 0.75, ..round("#39ff14", 6.0) }), S(round("#ffffff", 1.5))],
|
||||
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#39ff14"}))],
|
||||
),
|
||||
style(
|
||||
"Pulse Violet",
|
||||
vec![S(StrokeLayer { opacity: 0.75, ..round("#7b2cbf", 5.0) }), S(round("#e0aaff", 1.5))],
|
||||
vec![
|
||||
fx("stylize.innerGlow", json!({"mode": "screen", "opacity": 75.0, "blur": 6.0, "color": "#c77dff", "source": "edge"})),
|
||||
fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#7b2cbf"})),
|
||||
],
|
||||
),
|
||||
style("Cobalt Phosphor", vec![S(round("#0047ab", 6.0)), S(round("#ffffff", 1.5))], vec![fx("blur.gaussian", json!({"radius": 2.0}))]),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -750,4 +789,36 @@ mod tests {
|
||||
let Paint::Gradient(gp) = g.appearance.fill_paint() else { panic!("a gradient") };
|
||||
assert!(gp.swatch.is_none() && gp.gradient.stops.iter().all(|s| s.swatch.is_none() && s.tint == 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_and_glow_styles_are_defined_with_multi_strokes_and_effects() {
|
||||
let lib = builtin_style_library("neon-glow").expect("neon-glow library exists");
|
||||
assert_eq!(lib.name, "Neon & Glow");
|
||||
assert_eq!(lib.len(), 6);
|
||||
let names: Vec<&str> = lib.styles.iter().map(|g| g.name.as_str()).collect();
|
||||
assert_eq!(names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]);
|
||||
for g in &lib.styles {
|
||||
let strokes = g.appearance.items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count();
|
||||
assert!(strokes >= 2, "{} has {} strokes, expected >= 2", g.name, strokes);
|
||||
assert!(!g.appearance.effects.is_empty(), "{} has effects", g.name);
|
||||
}
|
||||
let cyan = lib.style("Cyan Neon").unwrap();
|
||||
assert_eq!(cyan.appearance.effects[0].id, "stylize.dropShadow");
|
||||
let magenta = lib.style("Magenta Laser").unwrap();
|
||||
assert_eq!(magenta.blend, BlendMode::Screen);
|
||||
let pulse = lib.style("Pulse Violet").unwrap();
|
||||
let pulse_fx: Vec<&str> = pulse.appearance.effects.iter().map(|e| e.id.as_str()).collect();
|
||||
assert_eq!(pulse_fx, ["stylize.innerGlow", "stylize.outerGlow"]);
|
||||
let cobalt = lib.style("Cobalt Phosphor").unwrap();
|
||||
assert_eq!(cobalt.appearance.effects[0].id, "blur.gaussian");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_glow_round_trips_through_vcstyles() {
|
||||
let lib = builtin_style_library("neon-glow").unwrap();
|
||||
let text = write(&lib);
|
||||
assert!(sniff(&text));
|
||||
let read_back = read(&text, "Fallback").unwrap();
|
||||
assert_eq!(*lib, read_back);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ 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, clipping_text_outline, dressed, geometry_effects, glyph_outlines, outline_nested_text, reshape_leaves};
|
||||
use crate::reshape::{
|
||||
as_art, clipping_text_outline, dressed, geometry_effects, glyph_outlines, outline_nested_text, paint_type_clip, reshape_leaves,
|
||||
};
|
||||
use crate::{
|
||||
GeomContext, adjust_in_document, apply_geometry_carrying, crop_marks_art, evaluate_container, has_adjustment, has_crop_marks, has_geometry,
|
||||
has_instance_row_raster, has_item_geometry, has_pathfinder, is_pathfinder, merged_params, needs_outline, reshape,
|
||||
@@ -689,6 +691,9 @@ fn moved_type_group(n: &Node) -> Option<Node> {
|
||||
/// them. `None` when no type stays type.
|
||||
fn container_keeping_type(d: &Document, n: &Node) -> Option<Node> {
|
||||
fn keep(d: &Document, n: &mut Node, moves: &[serde_json::Value], clipping: bool, kept: &mut bool) {
|
||||
if !clipping {
|
||||
paint_type_clip(Some(d), n);
|
||||
}
|
||||
let clips = n.clips();
|
||||
let Some(ch) = n.children_mut() else { return };
|
||||
for (i, c) in ch.iter_mut().enumerate() {
|
||||
|
||||
@@ -182,29 +182,29 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
REVOLVE,
|
||||
"Revolve…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{angle: degrees (0..360, 360), offset: pt (0..100000, 0), edge: left|right, rotationX: degrees (0), rotationY: degrees (0), rotationZ: degrees (0), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), segments: integer (8..128, 64), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), cap: bool (true, close circular ends of open profiles; partial-sweep cut sides stay open), expandVisibleOnly: bool (true, trim covered opaque solid surfaces only on Expand Appearance; surfaces with graphics keep every face), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the swept side, see threeD.mapGraphic: symbol: name of a document symbol, face: front (the side), x: pt (0; the offset of the graphic's center from the middle of the side, around the axis: the angle times the profile's distance from the axis at that point), y: pt (0; along the profile, toward its lower end), scale: % (1..10000, 100), rotation: degrees (-360..360, 0; counterclockwise); wrapped around the side at its own size, so it reaches farther around where the profile comes nearer the axis, shaded with it and drawn only where the side's outside faces the viewer; a full turn cuts it at the back; nothing on the caps)}. Shaded surfaces use smooth vector gradients automatically; intersecting profiles use approximate painter visibility.",
|
||||
json!({"angle":360.0,"offset":0.0,"edge":"left","rotationX":0.0,"rotationY":0.0,"rotationZ":0.0,"perspectiveAngle":0.0,"segments":64,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"cap":true,"expandVisibleOnly":true,"graphics":[]}),
|
||||
"{angle: degrees (0..360, 360), offset: pt (0..100000, 0), edge: left|right, rotationX: degrees (0), rotationY: degrees (0), rotationZ: degrees (0), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), segments: integer (8..128, 64), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), cap: bool (true, close circular ends of open profiles; partial-sweep cut sides stay open), expandVisibleOnly: bool (true, trim covered opaque solid surfaces only on Expand Appearance; surfaces with graphics keep every face), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the swept side, see threeD.mapGraphic: symbol: name of a document symbol, face: front (the side), x: pt (0; the offset of the graphic's center from the middle of the side, around the axis: the angle times the profile's distance from the axis at that point), y: pt (0; along the profile, toward its lower end), scale: % (1..10000, 100), rotation: degrees (-360..360, 0; counterclockwise); wrapped around the side at its own size, so it reaches farther around where the profile comes nearer the axis, shaded with it and drawn only where the side's outside faces the viewer; a full turn cuts it at the back; nothing on the caps), render: object ({mode: shaded}; mode: shaded|wireframe, wireframe draws the edges of the faces the effect draws, each once, stroked 0.5 pt black with round joins, with no fill, shadow or graphics, whatever the opacity of the fills, strokes and group members, while the effect stays live; the stroke is VectorCraft's own)}. Shaded surfaces use smooth vector gradients automatically; intersecting profiles use approximate painter visibility.",
|
||||
json!({"angle":360.0,"offset":0.0,"edge":"left","rotationX":0.0,"rotationY":0.0,"rotationZ":0.0,"perspectiveAngle":0.0,"segments":64,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"cap":true,"expandVisibleOnly":true,"graphics":[],"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
EXTRUDE,
|
||||
"Extrude & Bevel…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{depth: pt (0..2000, 50), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: hollow), bevel: none|classic|rounded|cove|step|ogee (none), bevelWidth: pt (0..1000, 4), bevelHeight: pt (0..1000, 4), bevelRepeat: integer (1..32, 1), bevelSpace: % (0..90, 0; flat between repeats), bevelInside: bool (false: added around the shape; true: carved into it), bevelBothSides: bool (false), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the caps, see threeD.mapGraphic: symbol: name of a document symbol, face: front|back (front; the back reads left to right as seen from behind), x: pt (0; the offset of the graphic's center from the center of the object's bounds, in the face's own plane), y: pt (0), scale: % (1..10000, 100; of the symbol's own size), rotation: degrees (-360..360, 0; counterclockwise); drawn turned, foreshortened and shaded with the cap and clipped to it, on a solid cap only)}. The front face is the art, extruded backwards about the centre of its bounds; painted with the top fill (else the top stroke).",
|
||||
json!({"depth":50.0,"twist":0.0,"taper":100.0,"cap":true,"bevel":"none","bevelWidth":4.0,"bevelHeight":4.0,"bevelRepeat":1,"bevelSpace":0.0,"bevelInside":false,"bevelBothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[]}),
|
||||
"{depth: pt (0..2000, 50), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: hollow), bevel: none|classic|rounded|cove|step|ogee (none), bevelWidth: pt (0..1000, 4), bevelHeight: pt (0..1000, 4), bevelRepeat: integer (1..32, 1), bevelSpace: % (0..90, 0; flat between repeats), bevelInside: bool (false: added around the shape; true: carved into it), bevelBothSides: bool (false), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the caps, see threeD.mapGraphic: symbol: name of a document symbol, face: front|back (front; the back reads left to right as seen from behind), x: pt (0; the offset of the graphic's center from the center of the object's bounds, in the face's own plane), y: pt (0), scale: % (1..10000, 100; of the symbol's own size), rotation: degrees (-360..360, 0; counterclockwise); drawn turned, foreshortened and shaded with the cap and clipped to it, on a solid cap only), render: object ({mode: shaded}; mode: shaded|wireframe, as Revolve's)}. The front face is the art, extruded backwards about the centre of its bounds; painted with the top fill (else the top stroke).",
|
||||
json!({"depth":50.0,"twist":0.0,"taper":100.0,"cap":true,"bevel":"none","bevelWidth":4.0,"bevelHeight":4.0,"bevelRepeat":1,"bevelSpace":0.0,"bevelInside":false,"bevelBothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[],"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
INFLATE,
|
||||
"Inflate…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{depth: pt (0..2000, 0; flat depth between front and back), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: no back, a shell open behind), volume: % (0..100, 50; 50 rounds the edge like a half sphere), bothSides: bool (false: flat back), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across)}. Painted with the top fill (else the top stroke).",
|
||||
json!({"depth":0.0,"twist":0.0,"taper":100.0,"cap":true,"volume":50.0,"bothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0}),
|
||||
"{depth: pt (0..2000, 0; flat depth between front and back), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: no back, a shell open behind), volume: % (0..100, 50; 50 rounds the edge like a half sphere), bothSides: bool (false: flat back), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), render: object ({mode: shaded}; mode: shaded|wireframe, as Revolve's)}. Painted with the top fill (else the top stroke).",
|
||||
json!({"depth":0.0,"twist":0.0,"taper":100.0,"cap":true,"volume":50.0,"bothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
ROTATE,
|
||||
"Rotate…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the plane, as Extrude & Bevel's, with face front only)}. Turns the flat art in 3D about the centre of its bounds; every fill and stroke keeps its paint, shaded as one plane.",
|
||||
json!({"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[]}),
|
||||
"{rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the plane, as Extrude & Bevel's, with face front only), render: object ({mode: shaded}; mode: shaded|wireframe, as Revolve's)}. Turns the flat art in 3D about the centre of its bounds; every fill and stroke keeps its paint, shaded as one plane.",
|
||||
json!({"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[],"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
CLASSIC_EXTRUDE,
|
||||
|
||||
+26
-12
@@ -31,9 +31,10 @@ pub fn reshaped_clip_outline(clip: &Node) -> Option<Option<(BezPath, FillRule)>>
|
||||
/// become a path of their reshaped region. A path or compound path whose fills or strokes have
|
||||
/// geometry effects becomes a group of one path per fill and stroke, reshaped by the path's effects
|
||||
/// and then by that fill's or stroke's, as an export bakes it; [`clip_outline`] clips by their
|
||||
/// union. A clipping path that paints ([`Node::clip_paint`], which a group does not) keeps the
|
||||
/// geometry effects of its own fills and strokes live instead. `None` when nothing in it has a
|
||||
/// geometry effect. Effects on the clip group or around it then reshape the result, after its own.
|
||||
/// union, and the group is marked as such pieces
|
||||
/// ([`vectorcraft_doc::NodeExtras::clip_pieces`]), so a clipping path that paints still paints
|
||||
/// them ([`Node::clip_paint`]). `None` when nothing in it has a geometry effect. Effects on the
|
||||
/// clip group or around it then reshape the result, after its own.
|
||||
pub(crate) fn with_clip_effects_applied(n: &Node) -> Option<Node> {
|
||||
with_clip_effects_applied_while(n, || true)
|
||||
}
|
||||
@@ -42,18 +43,20 @@ pub(crate) fn with_clip_effects_applied(n: &Node) -> Option<Node> {
|
||||
/// or compound path in `n`, at every depth: once `take` returns `false`, that member and the
|
||||
/// members after it are not visited. The members of a group or layer that are not visited stay as
|
||||
/// they are, with their effects. A compound path with geometry effects of its own still becomes a
|
||||
/// path of its reshaped geometry. In a clipping path that does not paint, one whose fills or
|
||||
/// strokes have geometry effects still becomes a group of one path per fill and stroke. In both
|
||||
/// cases the paths in it that are not visited go
|
||||
/// into that geometry as they are, and their own effects are not applied.
|
||||
/// path of its reshaped geometry. A path or compound path whose fills or strokes have geometry
|
||||
/// effects still becomes a group of one path per fill and stroke. In both cases the paths in it
|
||||
/// that are not visited go into that geometry as they are, and their own effects are not applied.
|
||||
pub(crate) fn with_clip_effects_applied_while(n: &Node, take: fn() -> bool) -> Option<Node> {
|
||||
let paint = n.clip_paint();
|
||||
clip_effects_applied(n, paint.fill.is_none() && paint.stroke.is_none(), take)
|
||||
let mut m = clip_effects_applied(n, true, take)?;
|
||||
if matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. }) && matches!(m.kind, NodeKind::Group { clip: false, .. }) {
|
||||
m.extras_mut().clip_pieces = true;
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// [`with_clip_effects_applied_while`]; with `items` unset (a clipping path that paints, and the
|
||||
/// paths of a compound path, which draw with the compound path's fills and strokes), the geometry
|
||||
/// effects of `n`'s own fills and strokes are left out.
|
||||
/// [`with_clip_effects_applied_while`]; with `items` unset (the paths of a compound path, which
|
||||
/// draw with the compound path's fills and strokes), the geometry effects of `n`'s own fills and
|
||||
/// strokes are left out.
|
||||
fn clip_effects_applied(n: &Node, items: bool, take: fn() -> bool) -> Option<Node> {
|
||||
let mut m: Option<Node> = None;
|
||||
let members: &[std::sync::Arc<Node>] = match &n.kind {
|
||||
@@ -88,6 +91,17 @@ fn clip_effects_applied(n: &Node, items: bool, take: fn() -> bool) -> Option<Nod
|
||||
.iter()
|
||||
.zip(&p.appearance.items)
|
||||
.map(|(item, carried)| {
|
||||
// A fill or stroke whose Transform effect scales strokes or moves patterns is drawn
|
||||
// through its art, as an export bakes such a path ([`crate::reshape`]).
|
||||
let mut whole = Node::path(n.id, g.clone(), Appearance { items: vec![carried.clone()], ..Default::default() });
|
||||
if let NodeKind::Path { rule: r, .. } = &mut whole.kind {
|
||||
*r = rule;
|
||||
}
|
||||
if crate::has_transform_art(&whole)
|
||||
&& let Some(art) = crate::reshape(None, &whole, None)
|
||||
{
|
||||
return std::sync::Arc::new(art);
|
||||
}
|
||||
let (ig, mut it) = crate::bake::apply_item(item, carried, &g, &ctx);
|
||||
crate::bake::clear_item_effects(&mut it);
|
||||
let mut piece = Node::path(n.id, ig, Appearance { items: vec![it], ..Default::default() });
|
||||
|
||||
@@ -91,12 +91,13 @@ pub use revolve::{
|
||||
CLASSIC_REVOLVE, REVOLVE, has_revolve, is_revolve, revolve_art, revolve_art_in, revolve_options, validate_revolve, validate_revolve_in,
|
||||
};
|
||||
pub use solids::{
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, EXTRUDE, GRAPHICS, GraphicFrame, INFLATE, MAX_GRAPHICS, MappedGraphic, ROTATE, ROTATE_SURFACES,
|
||||
ROTATION_PRESETS, SHADING_COLORS, SOLIDS, SolidMesh, THREE_D_TYPES, classic_extrude_options, classic_shading, extrude_options, graphic_bounds,
|
||||
graphic_frame, graphic_symbols, has_solid, inflate_options, is_classic_three_d, is_three_d, mapped_graphics, missing_symbol,
|
||||
names_graphic_symbol, rename_graphic_symbol, rename_graphic_symbol_in, solid_art, solid_art_in, takes_graphics, three_d_meshes,
|
||||
three_d_meshes_in, three_d_meshes_of, three_d_meshes_of_in, three_d_params_as, three_d_wireframe_in, validate_solid, validate_solid_in,
|
||||
validate_three_d, validate_three_d_at, validate_three_d_at_in, validate_three_d_in, validate_three_d_params, view_options, with_graphic,
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, EXTRUDE, GRAPHICS, GraphicFrame, INFLATE, MAX_GRAPHICS, MappedGraphic, RENDER, RENDER_MODES, ROTATE,
|
||||
ROTATE_SURFACES, ROTATION_PRESETS, SHADING_COLORS, SOLIDS, SolidMesh, THREE_D_TYPES, classic_extrude_options, classic_shading, extrude_options,
|
||||
graphic_bounds, graphic_frame, graphic_symbols, has_solid, inflate_options, is_classic_three_d, is_three_d, mapped_graphics, missing_symbol,
|
||||
names_graphic_symbol, rename_graphic_symbol, rename_graphic_symbol_in, renders_wireframe, solid_art, solid_art_in, takes_graphics,
|
||||
three_d_meshes, three_d_meshes_in, three_d_meshes_of, three_d_meshes_of_in, three_d_params_as, three_d_wireframe_in, validate_solid,
|
||||
validate_solid_in, validate_three_d, validate_three_d_at, validate_three_d_at_in, validate_three_d_in, validate_three_d_params, view_options,
|
||||
with_graphic,
|
||||
};
|
||||
pub use solids::{
|
||||
GRAPHIC_MAX_OFFSET, GRAPHIC_SCALE_RANGE, GRAPHICS_NOT_TAKEN, appearance_graphic_symbols, graphic_frame_at, graphics_reshaped_after,
|
||||
|
||||
@@ -746,9 +746,13 @@ pub(crate) fn clipping_text_outline(t: &Node) -> Option<Node> {
|
||||
|
||||
/// Replace type anywhere inside `n` by its outlines, as art that keeps the type's look
|
||||
/// ([`as_art`]). Type in a clip group's clipping path (all of `n` when `clipping` is set) becomes
|
||||
/// its outlines with its visibility and effects ([`clipping_text_outline`]). The swatches and
|
||||
/// patterns that live-effect plug-ins painting their pieces name are looked up in `doc`.
|
||||
/// its outlines with its visibility and effects ([`clipping_text_outline`]), and its fills and
|
||||
/// strokes become art of their own ([`paint_type_clip`]). The swatches and patterns that
|
||||
/// live-effect plug-ins painting their pieces name are looked up in `doc`.
|
||||
pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping: bool) {
|
||||
if !clipping {
|
||||
paint_type_clip(doc, n);
|
||||
}
|
||||
let clips = n.clips();
|
||||
let Some(ch) = n.children_mut() else { return };
|
||||
for (i, c) in ch.iter_mut().enumerate() {
|
||||
@@ -766,6 +770,36 @@ pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping
|
||||
}
|
||||
}
|
||||
|
||||
/// When `n` is a clip group or clipping layer whose clipping path is type that paints
|
||||
/// ([`Node::clip_paint`]), make it the art the canvas draws for it: the type's outlines clip
|
||||
/// ([`clipping_text_outline`]), its fills as art ([`as_art`]) paint behind the clipped art inside
|
||||
/// the clip, and its strokes as art paint over it, outside the clip, in a group that `n` becomes
|
||||
/// (keeping its id, name and transparency) with the clip group inside. The other members stay as
|
||||
/// they are. Hidden type paints nothing.
|
||||
pub(crate) fn paint_type_clip(doc: Option<&Document>, n: &mut Node) {
|
||||
let Some((first, rest)) = n.children().and_then(|ch| ch.split_first()) else { return };
|
||||
if !n.clips() || !matches!(first.kind, NodeKind::Text(_)) || !first.visible {
|
||||
return;
|
||||
}
|
||||
let paint = first.clip_paint();
|
||||
if paint.fill.is_none() && paint.stroke.is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(clip) = clipping_text_outline(first) else { return };
|
||||
let art = |part: Option<Node>| part.and_then(|p| as_art(doc, &p, None)).map(Arc::new);
|
||||
let members: Vec<Arc<Node>> = std::iter::once(Arc::new(clip)).chain(art(paint.fill)).chain(rest.iter().cloned()).collect();
|
||||
let stroke = art(paint.stroke);
|
||||
let id = n.id;
|
||||
match (&mut n.kind, stroke) {
|
||||
(NodeKind::Group { children, .. } | NodeKind::Layer { children, .. }, None) => *children = members,
|
||||
(NodeKind::Group { children, clip } | NodeKind::Layer { children, clip, .. }, Some(stroke)) => {
|
||||
*children = vec![Arc::new(Node::new(id, NodeKind::Group { children: members, clip: true })), stroke];
|
||||
*clip = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// `n` (see [`needs_outline`]) with its object-level geometry effects and its perspective applied:
|
||||
/// its [`outline_art`] reshaped as one piece, or for an image the image clipped by its reshaped
|
||||
/// frame. Type's own fills and strokes keep their effects on its whole outline. An image's own
|
||||
|
||||
@@ -8,7 +8,7 @@ use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, N
|
||||
use vectorcraft_geom::{PathData, PathEl, Point, Rect, Vec2};
|
||||
use vectorcraft_three_d::{Edge, Revolve, SolidFaces};
|
||||
|
||||
use crate::solids::wireframe::WireFace;
|
||||
use crate::solids::wireframe::{WireFace, wireframe_params};
|
||||
use crate::solids::{
|
||||
Profile, Scope, is_container, members, object_profile, past_effect, profile, reshape_members, resolve_for, solid_paints, union_bounds,
|
||||
};
|
||||
@@ -154,7 +154,12 @@ pub(crate) fn validate_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value)
|
||||
let b = union_bounds(&shapes).ok_or(EMPTY)?;
|
||||
let options = shared_options(&shapes, options_of(id, p))?;
|
||||
let mut copies = 0;
|
||||
shapes.iter().try_for_each(|s| check_surface(&s.path, b, options, surface_item(&s.items), &mut copies))?;
|
||||
// In wireframe mode the edges are drawn with no paint copies, graphics or shadow.
|
||||
let wire = wireframe_params(id, p);
|
||||
shapes.iter().try_for_each(|s| check_surface(&s.path, b, options, surface_item(&s.items).filter(|_| !wire), &mut copies))?;
|
||||
if wire {
|
||||
return Ok(());
|
||||
}
|
||||
// Without graphics, or without the document they draw from, no member is measured for them.
|
||||
if let Some(doc) = doc.filter(|_| id == REVOLVE && !crate::solids::mapped_graphics(p).is_empty()) {
|
||||
match graphics_member(&shapes, b, options).and_then(|k| shapes.get(k)) {
|
||||
@@ -176,7 +181,11 @@ pub(crate) fn validate_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value)
|
||||
let path = geometry(&path, before, &GeomContext::of(n));
|
||||
let b = path.bounds().ok_or("Revolve requires a shape with at least two anchors")?;
|
||||
let paints = solid_paints(n);
|
||||
check_surface(&path, b, options_of(id, p), surface_item(&paints), &mut 0)?;
|
||||
let wire = wireframe_params(id, p);
|
||||
check_surface(&path, b, options_of(id, p), surface_item(&paints).filter(|_| !wire), &mut 0)?;
|
||||
if wire {
|
||||
return Ok(());
|
||||
}
|
||||
check_graphics(doc, id, &path, b, options_of(id, p), p, &mut 0)?;
|
||||
check_shadow(p, [&path], b, options_of(id, p))
|
||||
}
|
||||
@@ -261,7 +270,11 @@ pub(crate) fn validate_item_of(doc: Option<&Document>, n: &Node, id: &str, p: &V
|
||||
let options = options_of(kind, ¶ms);
|
||||
let path = geometry(&path, at.and_then(|at| effects.get(..at)).unwrap_or(effects), &ctx.item(item));
|
||||
let b = path.bounds().ok_or(EMPTY)?;
|
||||
check_surface(&path, b, options, crate::paints(item).then_some(item), &mut copies)?;
|
||||
let wire = wireframe_params(kind, ¶ms);
|
||||
check_surface(&path, b, options, (crate::paints(item) && !wire).then_some(item), &mut copies)?;
|
||||
if wire {
|
||||
continue;
|
||||
}
|
||||
// The graphics of every item add their paints' copies to the graphics' budget, as they
|
||||
// are drawn; only the edited item's graphics are refused here.
|
||||
let graphics = check_graphics(doc, kind, &path, b, options, ¶ms, &mut graphic_copies);
|
||||
@@ -324,6 +337,7 @@ pub(crate) fn validate_revolve_params(p: &Value) -> Result<(), String> {
|
||||
pub(crate) fn validate_params_of(id: &str, p: &Value) -> Result<(), String> {
|
||||
validate_revolve_params(p)?;
|
||||
crate::solids::graphics::validate(id, p)?;
|
||||
crate::solids::wireframe::validate_render(id, p)?;
|
||||
if id == CLASSIC_REVOLVE {
|
||||
classic::validate(p)?;
|
||||
}
|
||||
@@ -728,14 +742,7 @@ pub(crate) fn revolve_meshes(n: &Node, total: &mut usize, limit: usize) -> Resul
|
||||
/// or stroke's Revolve are looked up in `doc`. Each face carries the width of the stroke the canvas
|
||||
/// draws around it ([`surface`]).
|
||||
pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<crate::solids::wireframe::FacesAndShadow, String> {
|
||||
let faces = |path: &PathData, b: Rect, fx: &Effect, options: Revolve| -> Result<Vec<WireFace>, String> {
|
||||
if fx.id == CLASSIC_REVOLVE {
|
||||
let p = merged_params(&fx.id, &fx.params);
|
||||
let facets = vectorcraft_three_d::revolve_facets(path, b, options)?;
|
||||
return Ok(facets.iter().map(|f| WireFace { path: f.path(), stroke: crate::solids::classic::stroke_width(f, &p) }).collect());
|
||||
}
|
||||
Ok(vectorcraft_three_d::revolve(path, b, options)?.iter().map(|f| WireFace { path: f.path(), stroke: SEAM_WIDTH }).collect())
|
||||
};
|
||||
let faces = wire_faces;
|
||||
if is_container(n) {
|
||||
let index = n
|
||||
.appearance
|
||||
@@ -752,7 +759,9 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<cra
|
||||
for s in shapes.iter().filter(|s| surface_item(&s.items).is_some()) {
|
||||
out.extend(faces(&s.path, b, fx, options)?);
|
||||
}
|
||||
return Ok((out, group_shadow(n.id, fx, &shapes, b, options, vec![])?));
|
||||
// In wireframe mode the canvas draws no shadow.
|
||||
let shadow = if crate::solids::renders_wireframe(fx) { vec![] } else { group_shadow(n.id, fx, &shapes, b, options, vec![])? };
|
||||
return Ok((out, shadow));
|
||||
}
|
||||
let (path, _) = object_profile(n, "Revolve")?;
|
||||
let ctx = GeomContext::of(n);
|
||||
@@ -763,7 +772,7 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<cra
|
||||
let item = surface_item(&paints).ok_or("the revolved object has no fill or stroke")?;
|
||||
let b = path.bounds().ok_or("Revolve requires a shape with at least two anchors")?;
|
||||
let surface = faces(&path, b, fx, fx_options(fx))?;
|
||||
let shadow = shadow_of(n.id, fx, &path, item, b, fx_options(fx), None)?;
|
||||
let shadow = if crate::solids::renders_wireframe(fx) { None } else { shadow_of(n.id, fx, &path, item, b, fx_options(fx), None)? };
|
||||
return Ok((surface, shadow.into_iter().map(Arc::new).collect()));
|
||||
}
|
||||
let path = geometry(&path, &n.appearance.effects, &ctx);
|
||||
@@ -781,6 +790,28 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<cra
|
||||
Ok((out, vec![]))
|
||||
}
|
||||
|
||||
/// The faces Revolve or Revolve (Classic) `fx` sweeps from `path` about the bounds `b` with
|
||||
/// `options`, each with the width of the stroke the canvas draws around it.
|
||||
fn wire_faces(path: &PathData, b: Rect, fx: &Effect, options: Revolve) -> Result<Vec<WireFace>, String> {
|
||||
if fx.id == CLASSIC_REVOLVE {
|
||||
let p = merged_params(&fx.id, &fx.params);
|
||||
let facets = vectorcraft_three_d::revolve_facets(path, b, options)?;
|
||||
return Ok(facets.iter().map(|f| WireFace { path: f.path(), stroke: crate::solids::classic::stroke_width(f, &p) }).collect());
|
||||
}
|
||||
Ok(vectorcraft_three_d::revolve(path, b, options)?.iter().map(|f| WireFace { path: f.path(), stroke: SEAM_WIDTH }).collect())
|
||||
}
|
||||
|
||||
/// The art Revolve `fx` in wireframe mode draws for object `id` from the profiles `paths`, all swept
|
||||
/// about the bounds `b` with `options`: the edges of their faces ([`crate::solids::wireframe::wire_art`]).
|
||||
/// `None` when a profile can't be swept or past the edge limit, when the canvas draws the source art.
|
||||
fn wire_art<'a>(id: NodeId, paths: impl IntoIterator<Item = &'a PathData>, b: Rect, fx: &Effect, options: Revolve) -> Option<Arc<Node>> {
|
||||
let mut faces = vec![];
|
||||
for path in paths {
|
||||
faces.extend(wire_faces(path, b, fx, options).ok()?.into_iter().map(|f| f.path));
|
||||
}
|
||||
crate::solids::wireframe::wire_art(id, &faces)
|
||||
}
|
||||
|
||||
/// [`revolve_meshes`] of group or layer `n`: one mesh for each member, about one shared axis.
|
||||
fn group_meshes(n: &Node, total: &mut usize, limit: usize) -> Result<Vec<crate::SolidMesh>, String> {
|
||||
let index = n
|
||||
@@ -824,13 +855,18 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
let paints = solid_paints(n);
|
||||
let item = surface_item(&paints)?;
|
||||
let expand = expand && !n.appearance.effects.iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
let (surface, kept) = object_surface(doc, n, &path, fx, item, expand, &mut copies)?;
|
||||
if let Some(b) = path.bounds()
|
||||
&& let Some(shadow) = shadow_of(n.id, fx, &path, item, b, fx_options(fx), kept).ok()?
|
||||
{
|
||||
children.push(Arc::new(shadow));
|
||||
if crate::solids::renders_wireframe(fx) {
|
||||
// In wireframe mode the edges of the faces, with no shadow.
|
||||
children.push(wire_art(n.id, [&path], path.bounds()?, fx, fx_options(fx))?);
|
||||
} else {
|
||||
let (surface, kept) = object_surface(doc, n, &path, fx, item, expand, &mut copies)?;
|
||||
if let Some(b) = path.bounds()
|
||||
&& let Some(shadow) = shadow_of(n.id, fx, &path, item, b, fx_options(fx), kept).ok()?
|
||||
{
|
||||
children.push(Arc::new(shadow));
|
||||
}
|
||||
children.push(Arc::new(surface));
|
||||
}
|
||||
children.push(Arc::new(surface));
|
||||
// Later geometry acts on the projected art as a whole through container evaluation.
|
||||
remaining = past_effect(&n.appearance.effects, index);
|
||||
remaining.retain(|e| !is_revolve(&e.id));
|
||||
@@ -850,6 +886,13 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
let fx = item.effects().get(index)?;
|
||||
let p = geometry(&path, item.effects().get(..index)?, &ctx.item(original));
|
||||
let expand = expand && !item.effects().iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
if crate::solids::renders_wireframe(fx) {
|
||||
let mut art = wire_art(n.id, [&p], p.bounds()?, fx, fx_options(fx))?;
|
||||
Arc::make_mut(&mut art).appearance.effects =
|
||||
item.effects().iter().skip(index + 1).filter(|e| !is_revolve(&e.id)).cloned().collect();
|
||||
children.push(art);
|
||||
continue;
|
||||
}
|
||||
let (mut art, kept) = object_surface(doc, n, &p, fx, item, expand, &mut copies)?;
|
||||
art.appearance.effects = item.effects().iter().skip(index + 1).filter(|e| !is_revolve(&e.id)).cloned().collect();
|
||||
if let Some(b) = p.bounds()
|
||||
@@ -999,6 +1042,14 @@ fn group_art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
reshape_members(n, n.appearance.effects.get(..index)?, &mut shapes);
|
||||
let b = union_bounds(&shapes)?;
|
||||
let options = shared_options(&shapes, fx_options(fx)).ok()?;
|
||||
if crate::solids::renders_wireframe(fx) {
|
||||
// In wireframe mode the edges of every member's faces, with no shadow.
|
||||
let paths = shapes.iter().filter(|s| surface_item(&s.items).is_some()).map(|s| &s.path);
|
||||
let mut art = n.clone();
|
||||
art.kind = NodeKind::Group { children: vec![wire_art(n.id, paths, b, fx, options)?], clip: false };
|
||||
art.appearance = Appearance { effects: past_effect(&n.appearance.effects, index), ..Default::default() }.into();
|
||||
return Some(art);
|
||||
}
|
||||
let expand = expand && !n.appearance.effects.iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
let mut copies = Copies::default();
|
||||
let mut surfaces = vec![];
|
||||
|
||||
@@ -36,7 +36,7 @@ pub use graphics::{
|
||||
pub(crate) mod shadow;
|
||||
|
||||
pub(crate) mod wireframe;
|
||||
pub use wireframe::three_d_wireframe_in;
|
||||
pub use wireframe::{RENDER, RENDER_MODES, renders_wireframe, three_d_wireframe_in};
|
||||
|
||||
pub const EXTRUDE: &str = "threeD.extrude";
|
||||
pub const INFLATE: &str = "threeD.inflate";
|
||||
@@ -274,6 +274,7 @@ fn validate_solid_params(id: &str, p: &Value) -> Result<(), String> {
|
||||
crate::lighting::validate(p)?;
|
||||
shadow::validate(p)?;
|
||||
graphics::validate(id, p)?;
|
||||
wireframe::validate_render(id, p)?;
|
||||
if let Some(v) = p.get("bevel")
|
||||
&& !v.as_str().is_some_and(|s| s == "none" || BevelShape::parse(s).is_some())
|
||||
{
|
||||
@@ -324,7 +325,7 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
let b = union_bounds(&shapes).ok_or_else(|| format!("{label} requires a group holding a shape with at least two anchors"))?;
|
||||
check_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
return cast_shadow(id, p, &shapes).map(|_| ());
|
||||
return check_cast(id, p, &shapes);
|
||||
}
|
||||
let (path, rule) = object_profile(n, label)?;
|
||||
let anchors = || format!("{label} requires a shape with at least two anchors");
|
||||
@@ -342,7 +343,16 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
let shapes = [Profile { path, rule, items, opacity: 1.0, scopes: vec![] }];
|
||||
check_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
cast_shadow(id, p, &shapes).map(|_| ())
|
||||
check_cast(id, p, &shapes)
|
||||
}
|
||||
|
||||
/// With the shadow of 3D effect `id` with parameters `p` on, an error when the shadow of `shapes`
|
||||
/// is past the limits ([`cast_shadow`]). In wireframe mode no shadow is drawn, and none is checked.
|
||||
fn check_cast(id: &str, p: &Value, shapes: &[Profile]) -> Result<(), String> {
|
||||
if wireframe::wireframe_params(id, p) {
|
||||
return Ok(());
|
||||
}
|
||||
cast_shadow(id, p, shapes).map(|_| ())
|
||||
}
|
||||
|
||||
/// Do the solids 3D effect `id` with parameters `p` builds from the profiles of `shapes` about `b`
|
||||
@@ -1420,6 +1430,10 @@ type ShadowAndSurfaces = (Vec<Arc<Node>>, Vec<Arc<Node>>);
|
||||
/// off), cast from the faces the solids drawn kept ([`cast_built`]), to be drawn behind every
|
||||
/// surface of the object, and the surfaces.
|
||||
fn art_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile]) -> Option<ShadowAndSurfaces> {
|
||||
// In wireframe mode the effect draws the edges of the faces it builds, with no shadow.
|
||||
if wireframe::renders_wireframe(fx) {
|
||||
return Some((vec![], wireframe::wire_surfaces(n.id, fx, shapes)?));
|
||||
}
|
||||
let p = crate::merged_params(&fx.id, &fx.params);
|
||||
let cast = !is_classic_three_d(&fx.id) && shadow::options(&p).is_some();
|
||||
let (art, built) = surfaces_for(doc, n, fx, shapes, cast)?;
|
||||
@@ -1434,8 +1448,11 @@ fn art_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile]) ->
|
||||
}
|
||||
|
||||
/// The shadow [`art_for`] draws for effect `fx` on `shapes` behind the surfaces of object `id`:
|
||||
/// none with the shadow off, and an error when it is past the limits.
|
||||
/// none with the shadow off or in wireframe mode, and an error when it is past the limits.
|
||||
pub(crate) fn shadow_for(id: NodeId, fx: &Effect, shapes: &[Profile]) -> Result<Vec<Arc<Node>>, String> {
|
||||
if wireframe::renders_wireframe(fx) {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
Ok(match cast_shadow(&fx.id, &crate::merged_params(&fx.id, &fx.params), shapes)? {
|
||||
Some((caster, settings, cast)) => shadow::scoped_art(id, &caster, settings, cast, has_masks(shapes)),
|
||||
None => vec![],
|
||||
|
||||
@@ -1,22 +1,91 @@
|
||||
//! Expand as Wireframes: the edges of the faces a 3D effect draws, as open and closed paths that
|
||||
//! can be edited on their own.
|
||||
//! can be edited on their own. Also the 3D and Materials panel's Render Settings › Wireframe, which
|
||||
//! draws those edges in place of the shaded faces while the effect stays live.
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, Node, NodeId};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, LineJoin, Node, NodeId, StrokeLayer};
|
||||
use vectorcraft_geom::{Anchor, CubicBez, PathData, Point, SubPath};
|
||||
use vectorcraft_three_d::Facet;
|
||||
|
||||
use super::{
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, Profile, ROTATE, built_solids, facets, geometry, has_solid, is_container, is_plane, is_three_d, label_of,
|
||||
members, object_profile, region, reshape_members, resolve, resolve_for, solid, solid_paint_sources, solid_parts, union_bounds,
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, EXTRUDE, INFLATE, Profile, ROTATE, built_solids, facets, geometry, has_solid, is_container, is_plane,
|
||||
is_three_d, label_of, members, object_profile, region, reshape_members, resolve, resolve_for, solid, solid_paint_sources, solid_parts,
|
||||
union_bounds,
|
||||
};
|
||||
use crate::{GeomContext, is_geometry};
|
||||
|
||||
/// The most distinct edges one object's wireframe may have.
|
||||
pub(crate) const MAX_WIRE_EDGES: usize = 1_000_000;
|
||||
|
||||
/// The parameter that holds a 3D effect's render settings: an object whose `mode` is one of
|
||||
/// [`RENDER_MODES`].
|
||||
pub const RENDER: &str = "render";
|
||||
|
||||
/// The render modes of a 3D effect: `shaded` draws the lit faces, `wireframe` the edges of every
|
||||
/// face ([`renders_wireframe`]).
|
||||
pub const RENDER_MODES: [&str; 2] = ["shaded", "wireframe"];
|
||||
|
||||
/// The 3D effects that take [`RENDER`]: the 3D and Materials panel's Extrude & Bevel, Revolve,
|
||||
/// Inflate and Rotate. The 3D (Classic) effects have their own Wireframe surface.
|
||||
const TAKES_RENDER: [&str; 4] = [EXTRUDE, crate::REVOLVE, INFLATE, ROTATE];
|
||||
|
||||
/// The weight of a rendered wireframe edge, in points: the weight Expand as Wireframes strokes its
|
||||
/// edges with (VectorCraft's own).
|
||||
pub(crate) const WIRE_WEIGHT: f64 = 0.5;
|
||||
|
||||
/// Does effect `fx` draw as a wireframe: is it one of the effects that take [`RENDER`], with its
|
||||
/// `render.mode` set to `wireframe`?
|
||||
pub fn renders_wireframe(fx: &Effect) -> bool {
|
||||
wireframe_params(&fx.id, &fx.params)
|
||||
}
|
||||
|
||||
/// Do 3D effect `id`'s parameters `p` set it to draw as a wireframe ([`renders_wireframe`])? A
|
||||
/// command that sets them is checked against the art that mode draws: the faces' edges, with no
|
||||
/// shadow, graphics or paint copies.
|
||||
pub(crate) fn wireframe_params(id: &str, p: &Value) -> bool {
|
||||
TAKES_RENDER.contains(&id) && p.get(RENDER).and_then(|r| r.get("mode")).and_then(Value::as_str) == Some("wireframe")
|
||||
}
|
||||
|
||||
/// Check the render settings in 3D effect `id`'s parameters `p`: `render`, when given to one of the
|
||||
/// effects in [`TAKES_RENDER`], is an object that holds only `mode`, one of [`RENDER_MODES`] (a
|
||||
/// missing `mode` is `shaded`). The 3D (Classic) effects refuse it as a parameter they do not take.
|
||||
pub(crate) fn validate_render(id: &str, p: &Value) -> Result<(), String> {
|
||||
let Some(render) = p.get(RENDER).filter(|_| TAKES_RENDER.contains(&id)) else { return Ok(()) };
|
||||
let valid = render
|
||||
.as_object()
|
||||
.is_some_and(|fields| fields.iter().all(|(key, value)| key == "mode" && value.as_str().is_some_and(|m| RENDER_MODES.contains(&m))));
|
||||
if !valid {
|
||||
return Err("`render` must be an object holding only `mode`, set to shaded or wireframe".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The art a 3D effect in wireframe mode draws for object `id` in place of its shaded faces: a group
|
||||
/// of the edges of `faces`, each edge once ([`edges`]), stroked [`WIRE_WEIGHT`] black with round
|
||||
/// joins and no fill, as Expand as Wireframes strokes them. Like Expand as Wireframes and the
|
||||
/// Outline view, the mesh shows every edge whatever the opacity, blending mode or opacity mask of
|
||||
/// the fills, strokes and group members it is built from; the object's own transparency applies to
|
||||
/// the art as a whole. `None` past [`MAX_WIRE_EDGES`] edges, when the canvas draws the source art,
|
||||
/// as it does for a solid past its limits.
|
||||
pub(crate) fn wire_art(id: NodeId, faces: &[PathData]) -> Option<Arc<Node>> {
|
||||
let mut stroke = StrokeLayer::new(Paint::solid(Color::BLACK), WIRE_WEIGHT);
|
||||
stroke.join = LineJoin::Round;
|
||||
let appearance = Appearance { items: vec![AppearanceItem::Stroke(stroke)], ..Default::default() };
|
||||
let children = edges(faces).ok()?.into_iter().map(|path| Arc::new(Node::path(id, path, appearance.clone()))).collect();
|
||||
Some(Arc::new(Node::group(id, children)))
|
||||
}
|
||||
|
||||
/// The art 3D effect `fx` in wireframe mode draws for object `id` from `shapes`: the edges of the
|
||||
/// faces it builds ([`wire_art`]). `None` when a solid can't be built or past the edge limit, when
|
||||
/// the canvas draws the source art.
|
||||
pub(crate) fn wire_surfaces(id: NodeId, fx: &Effect, shapes: &[Profile]) -> Option<Vec<Arc<Node>>> {
|
||||
let faces: Vec<PathData> = facet_paths(fx, shapes).ok()?.into_iter().map(|f| f.path).collect();
|
||||
Some(vec![wire_art(id, &faces)?])
|
||||
}
|
||||
|
||||
/// The grid points are rounded to before edges are compared, in points: two faces that share an
|
||||
/// edge give it once.
|
||||
const GRID: f64 = 1.0 / 1024.0;
|
||||
@@ -171,7 +240,7 @@ fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result<FacesAndShadow,
|
||||
/// as it is left out of the drawn art; past the limits of the object's solids together, the result
|
||||
/// is an error, as the canvas draws the source art. Each facet carries the width of the stroke the
|
||||
/// canvas draws around it.
|
||||
fn facet_paths(fx: &Effect, shapes: &[Profile]) -> Result<Vec<WireFace>, String> {
|
||||
pub(crate) fn facet_paths(fx: &Effect, shapes: &[Profile]) -> Result<Vec<WireFace>, String> {
|
||||
let label = label_of(&fx.id);
|
||||
let b = union_bounds(shapes).ok_or_else(|| format!("{label} requires a shape with at least two anchors"))?;
|
||||
let p = crate::merged_params(&fx.id, &fx.params);
|
||||
|
||||
@@ -281,6 +281,210 @@ fn a_clipping_group_with_revolve_and_rotate_expands_the_art_revolve_cuts() {
|
||||
assert_eq!(both, alone);
|
||||
}
|
||||
|
||||
/// The leaves `art` draws: each path with its fill and stroke.
|
||||
fn leaves(art: &Node) -> Vec<(PathData, bool, Option<StrokeLayer>)> {
|
||||
let mut out = vec![];
|
||||
art.walk(&mut |c| {
|
||||
if let Some(p) = c.path_data() {
|
||||
out.push((p.clone(), c.appearance.fill().is_some(), c.appearance.stroke().cloned()));
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// The art the canvas draws for `n`: Revolve's, else the other 3D effects'.
|
||||
fn drawn(n: &Node) -> Node {
|
||||
crate::revolve_art(n).or_else(|| super::super::solid_art(n)).unwrap_or_else(|| panic!("no art for {n:?}"))
|
||||
}
|
||||
|
||||
const WIRE: [&str; 4] = [super::super::EXTRUDE, crate::REVOLVE, super::super::INFLATE, ROTATE];
|
||||
|
||||
/// Render Settings › Wireframe: each of the panel's four effects draws the edges Expand as
|
||||
/// Wireframes gives, each a 0.5 pt black stroke with round joins and no fill, in place of its
|
||||
/// shaded faces; shaded again, it draws filled faces.
|
||||
#[test]
|
||||
fn wireframe_mode_draws_the_expanded_edges_as_strokes() {
|
||||
for id in WIRE {
|
||||
let n = solid_square(id, json!({"render": {"mode": "wireframe"}}));
|
||||
let art = leaves(&drawn(&n));
|
||||
let wire = three_d_wireframe_in(None, &n).unwrap_or_else(|e| panic!("{id}: {e}"));
|
||||
assert_eq!(art.len(), wire.len(), "{id}");
|
||||
for ((path, filled, stroke), edge) in art.iter().zip(&wire) {
|
||||
assert!(!filled, "{id}: an edge has no fill");
|
||||
let stroke = stroke.as_ref().unwrap_or_else(|| panic!("{id}: an edge is stroked"));
|
||||
assert_eq!((stroke.width, stroke.join, &stroke.paint), (WIRE_WEIGHT, LineJoin::Round, &Paint::solid(Color::BLACK)), "{id}");
|
||||
assert_eq!(segments(std::slice::from_ref(path)), segments(std::slice::from_ref(edge)), "{id}");
|
||||
}
|
||||
let shaded = leaves(&drawn(&solid_square(id, json!({"render": {"mode": "shaded"}}))));
|
||||
assert!(shaded.iter().any(|(_, filled, _)| *filled), "{id}: shaded faces are filled");
|
||||
}
|
||||
// The box turned off axis shows the nine edges of its three faces.
|
||||
let n = solid_square(super::super::EXTRUDE, json!({"render": {"mode": "wireframe"}}));
|
||||
let paths: Vec<PathData> = leaves(&drawn(&n)).into_iter().map(|(p, ..)| p).collect();
|
||||
assert_eq!(segments(&paths).len(), 9);
|
||||
}
|
||||
|
||||
/// The wireframe stays live: a deeper extrusion and a turned view redraw the edges.
|
||||
#[test]
|
||||
fn wireframe_mode_follows_the_effects_settings() {
|
||||
let paths = |p: Value| -> Vec<PathData> { leaves(&drawn(&solid_square(super::super::EXTRUDE, p))).into_iter().map(|(p, ..)| p).collect() };
|
||||
let flat = bounds(&paths(json!({"render": {"mode": "wireframe"}, "depth": 10.0})));
|
||||
let deep = bounds(&paths(json!({"render": {"mode": "wireframe"}, "depth": 200.0})));
|
||||
assert!(deep.width() > flat.width() + 1.0 || deep.height() > flat.height() + 1.0, "{flat:?} {deep:?}");
|
||||
let turned = paths(json!({"render": {"mode": "wireframe"}, "rotationY": 60.0}));
|
||||
assert_ne!(segments(&turned), segments(&paths(json!({"render": {"mode": "wireframe"}}))));
|
||||
}
|
||||
|
||||
/// In wireframe mode no shadow and no graphic is drawn: only the edges.
|
||||
#[test]
|
||||
fn wireframe_mode_draws_no_shadow() {
|
||||
for id in WIRE {
|
||||
let n = solid_square(id, json!({"render": {"mode": "wireframe"}, "shadow": true}));
|
||||
let art = leaves(&drawn(&n));
|
||||
assert!(!art.is_empty() && art.iter().all(|(_, filled, stroke)| !filled && stroke.is_some()), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The mode works where the effect is on a fill and on a group: the fill's solid and every member's
|
||||
/// solid draw as edges, and the geometry effects after a fill's effect still apply to them.
|
||||
#[test]
|
||||
fn wireframe_mode_on_a_fill_and_on_a_group() {
|
||||
for id in WIRE {
|
||||
let mut n = solid_square(id, json!({}));
|
||||
let mut fx = n.appearance.effects.remove(0);
|
||||
fx.params["render"] = json!({"mode": "wireframe"});
|
||||
let fill = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))).unwrap();
|
||||
fill.effects_mut().push(fx.clone());
|
||||
fill.effects_mut().push(crate::new_effect("distort.transform", &json!({"moveH": 500.0})).unwrap());
|
||||
let art = leaves(&drawn(&n));
|
||||
assert!(!art.is_empty() && art.iter().all(|(_, filled, stroke)| !filled && stroke.is_some()), "{id} on a fill");
|
||||
|
||||
let member = |x: f64, id: u64| {
|
||||
Arc::new(Node::path(NodeId(id), square(x, 100.0, 50.0), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0)))
|
||||
};
|
||||
let mut g = Node::group(NodeId(1), vec![member(100.0, 2), member(200.0, 3)]);
|
||||
g.appearance.effects.push(fx);
|
||||
let art = leaves(&drawn(&g));
|
||||
assert!(!art.is_empty() && art.iter().all(|(_, filled, stroke)| !filled && stroke.is_some()), "{id} on a group");
|
||||
// Both members' edges are drawn.
|
||||
let paths: Vec<PathData> = art.into_iter().map(|(p, ..)| p).collect();
|
||||
assert!(bounds(&paths).width() > 100.0, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The mesh shows every edge, as Expand as Wireframes gives them, whatever the opacity of the fill
|
||||
/// and the opacity masks of the group's members; the object's own opacity stays on the art. Two
|
||||
/// members that touch, one at 50% and one masked, draw their shared edge once: seven edges.
|
||||
#[test]
|
||||
fn the_mesh_shows_every_edge_whatever_the_members_transparency() {
|
||||
let member = |x: f64, id: u64, opacity: f32| {
|
||||
let mut n = Node::path(NodeId(id), square(x, 100.0, 50.0), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))) {
|
||||
f.opacity = opacity;
|
||||
}
|
||||
n
|
||||
};
|
||||
let mut masked = member(150.0, 3, 1.0);
|
||||
masked.extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(member(0.0, 900, 1.0), true)));
|
||||
let mut g = Node::group(NodeId(1), vec![Arc::new(member(100.0, 2, 0.5)), Arc::new(masked)]);
|
||||
g.opacity = 0.75;
|
||||
g.appearance.effects.push(crate::new_effect(ROTATE, &json!({"render": {"mode": "wireframe"}})).unwrap());
|
||||
let art = drawn(&g);
|
||||
assert_eq!(art.opacity, 0.75);
|
||||
let paths: Vec<PathData> = leaves(&art).into_iter().map(|(p, ..)| p).collect();
|
||||
assert_eq!(segments(&paths).len(), 7, "{paths:?}");
|
||||
assert_eq!(segments(&paths).len(), segments(&three_d_wireframe_in(None, &g).unwrap()).len());
|
||||
let mut scoped = 0;
|
||||
for c in art.children().into_iter().flatten() {
|
||||
c.walk(&mut |c| scoped += usize::from(c.extras().mask.is_some() || c.opacity < 1.0));
|
||||
}
|
||||
assert_eq!(scoped, 0, "no part of the mesh is masked or faded");
|
||||
// A fill at 0% still shows its mesh.
|
||||
for id in WIRE {
|
||||
let mut n = solid_square(id, json!({"render": {"mode": "wireframe"}}));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))) {
|
||||
f.opacity = 0.0;
|
||||
}
|
||||
assert!(!leaves(&drawn(&n)).is_empty(), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand Appearance of a Revolve in wireframe mode gives the stroked edges, not the shaded faces.
|
||||
#[test]
|
||||
fn expanding_a_revolve_in_wireframe_mode_gives_the_edges() {
|
||||
let n = solid_square(crate::REVOLVE, json!({"render": {"mode": "wireframe"}}));
|
||||
let art = leaves(&crate::revolve::expand_revolve_art(&n).unwrap());
|
||||
assert_eq!(art.len(), three_d_wireframe_in(None, &n).unwrap().len());
|
||||
assert!(art.iter().all(|(_, filled, stroke)| !filled && stroke.as_ref().is_some_and(|s| s.width == WIRE_WEIGHT)));
|
||||
}
|
||||
|
||||
/// `render` is checked before an edit: an object holding only `mode`, `shaded` or `wireframe`, and
|
||||
/// only on the panel's four effects.
|
||||
#[test]
|
||||
fn render_settings_are_validated() {
|
||||
for id in WIRE {
|
||||
for good in [json!({}), json!({"render": {}}), json!({"render": {"mode": "shaded"}}), json!({"render": {"mode": "wireframe"}})] {
|
||||
assert_eq!(crate::validate_three_d_params(id, &good), Ok(()), "{id} {good}");
|
||||
}
|
||||
for bad in [
|
||||
json!({"render": "wireframe"}),
|
||||
json!({"render": null}),
|
||||
json!({"render": {"mode": "raytrace"}}),
|
||||
json!({"render": {"mode": true}}),
|
||||
json!({"render": {"mode": "wireframe", "quality": "high"}}),
|
||||
] {
|
||||
let e = crate::validate_three_d_params(id, &bad).unwrap_err();
|
||||
assert_eq!(e, "`render` must be an object holding only `mode`, set to shaded or wireframe", "{id} {bad}");
|
||||
}
|
||||
// The command check on the object refuses it too.
|
||||
let n = solid_square(id, json!({}));
|
||||
assert!(crate::validate_three_d(&n, id, &json!({"render": {"mode": "x"}})).is_err(), "{id}");
|
||||
}
|
||||
for id in [super::super::CLASSIC_EXTRUDE, crate::CLASSIC_REVOLVE, super::super::CLASSIC_ROTATE] {
|
||||
let e = crate::validate_three_d_params(id, &json!({"render": {"mode": "wireframe"}})).unwrap_err();
|
||||
assert!(e.contains("`render`"), "{id}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A Revolve whose shaded surface is past the budget of paint copies (a gradient with many stops) is
|
||||
/// refused shaded, but in wireframe mode, which draws only the black edges, it is accepted and drawn:
|
||||
/// on the object and on a fill.
|
||||
#[test]
|
||||
fn wireframe_mode_is_checked_against_the_edges_it_draws() {
|
||||
use vectorcraft_doc::color::{Gradient, GradientKind, GradientPaint, GradientStop};
|
||||
let n = solid_square(crate::REVOLVE, json!({}));
|
||||
let path = n.path_data().unwrap().clone();
|
||||
let faces = vectorcraft_three_d::revolve(&path, path.bounds().unwrap(), crate::revolve::revolve_options(&json!({}))).unwrap().len();
|
||||
let stops = crate::revolve::MAX_PAINT_COPIES / (faces * 4) / std::mem::size_of::<GradientStop>() + 1;
|
||||
let stops = (0..stops).map(|i| GradientStop::new(i as f32 / stops as f32, Color::BLACK)).collect();
|
||||
let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
|
||||
let mut n = n;
|
||||
n.appearance = Appearance::basic(gradient, Paint::None, 0.0).into();
|
||||
let shaded = json!({});
|
||||
let wire = json!({"render": {"mode": "wireframe"}});
|
||||
let refused = crate::validate_revolve(&n, &shaded).unwrap_err();
|
||||
assert!(refused.contains("simplify the gradient"), "{refused}");
|
||||
assert_eq!(crate::validate_revolve(&n, &wire), Ok(()));
|
||||
n.appearance.effects.push(crate::new_effect(crate::REVOLVE, &wire).unwrap());
|
||||
assert!(leaves(&drawn(&n)).iter().all(|(_, filled, stroke)| !filled && stroke.is_some()));
|
||||
// On the fill.
|
||||
let fx = n.appearance.effects.remove(0);
|
||||
let at = n.appearance.items.iter().position(|i| matches!(i, AppearanceItem::Fill(_))).unwrap();
|
||||
assert!(crate::revolve::validate_item_revolve(&n, &shaded, at, 0).is_err());
|
||||
assert_eq!(crate::revolve::validate_item_revolve(&n, &wire, at, 0), Ok(()));
|
||||
n.appearance.items[at].effects_mut().push(fx);
|
||||
assert!(leaves(&drawn(&n)).iter().all(|(_, filled, stroke)| !filled && stroke.is_some()));
|
||||
}
|
||||
|
||||
/// A 3D (Classic) effect ignores a `render` it does not take in a loaded file: it draws as before.
|
||||
#[test]
|
||||
fn a_classic_effect_is_not_drawn_as_a_render_wireframe() {
|
||||
let mut n = solid_square(super::super::CLASSIC_EXTRUDE, json!({}));
|
||||
n.appearance.effects[0].params["render"] = json!({"mode": "wireframe"});
|
||||
assert!(!renders_wireframe(&n.appearance.effects[0]));
|
||||
assert!(leaves(&drawn(&n)).iter().any(|(_, filled, _)| *filled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_object_without_a_visible_3d_effect_is_an_error() {
|
||||
let mut n = solid_square(super::super::EXTRUDE, json!({}));
|
||||
|
||||
@@ -386,9 +386,111 @@ fn a_fills_geometry_effect_shapes_the_clip_before_the_effects_around_it() {
|
||||
let baked = bake_document(&d).unwrap();
|
||||
let exported = baked.layers.iter().find_map(|l| first_clip(l)).map(|c| clip_outline(&c).unwrap().0.bounding_box()).unwrap();
|
||||
assert!(close(exported), "exported: {exported:?}");
|
||||
// A clipping path that paints its fill keeps it, a path with the fill's effect live.
|
||||
// A clipping path that paints its fill clips alike and paints the fill it clips by.
|
||||
let painted = first_clip(&crate::evaluate_container(None, &scaled(clip_with(Paint::solid(Color::BLACK)))).unwrap()).unwrap();
|
||||
assert!(painted.clip_paint().fill.is_some(), "{painted:?}");
|
||||
assert!(close(clip_outline(&painted).unwrap().0.bounding_box()), "painted: {painted:?}");
|
||||
let fill = painted.clip_paint().fill.unwrap();
|
||||
assert!(close(fill.geometric_bounds().unwrap()), "painted fill: {fill:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_painted_clipping_paths_fill_and_stroke_effects_apply_before_the_effects_around_it() {
|
||||
// A clipping path with a black fill grown by Offset Path 10 and a 2 pt blue stroke moved 30 pt
|
||||
// right by a Transform, in a group that its Transform scales to 50 %: the fill paints the grown
|
||||
// square at half size, 60 pt across, and the stroke the square at half size, 15 pt right of
|
||||
// it, on the canvas and in an export alike.
|
||||
let mut clip = clipping_path(3, vec![]);
|
||||
clip.appearance = Appearance::basic(Paint::solid(Color::BLACK), Paint::solid(Color::rgb(0.0, 0.0, 1.0)), 2.0).into();
|
||||
for item in &mut clip.appearance.items {
|
||||
match item {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) => f.effects.push(fx("path.offsetPath", json!({"offset": 10}))),
|
||||
vectorcraft_doc::AppearanceItem::Stroke(s) => s.effects.push(fx("distort.transform", json!({"moveH": 30}))),
|
||||
}
|
||||
}
|
||||
let mut outer = Node::group(NodeId(1), vec![Arc::new(clip_group(2, clip.clone()))]);
|
||||
outer.appearance.effects.push(fx("distort.transform", json!({"scaleH": 50, "scaleV": 50})));
|
||||
fn first_clip(n: &Node) -> Option<Arc<Node>> {
|
||||
match &n.kind {
|
||||
NodeKind::Group { children, clip: true } => children.first().cloned(),
|
||||
_ => n.children()?.iter().find_map(|c| first_clip(c)),
|
||||
}
|
||||
}
|
||||
let check = |what: &str, c: &Node| {
|
||||
let paint = c.clip_paint();
|
||||
let fill = paint.fill.as_ref().and_then(Node::geometric_bounds).unwrap();
|
||||
assert!((fill.width() - 60.0).abs() < 0.5 && (fill.height() - 60.0).abs() < 0.5, "{what} fill: {fill:?}");
|
||||
let stroke = paint.stroke.as_ref().and_then(Node::geometric_bounds).unwrap();
|
||||
assert!((stroke.width() - 50.0).abs() < 0.5 && (stroke.x0 - fill.x0 - 20.0).abs() < 0.5, "{what} stroke: {stroke:?} fill: {fill:?}");
|
||||
};
|
||||
let evaluated = crate::evaluate_container(None, &outer).unwrap();
|
||||
let canvas = first_clip(&evaluated).unwrap();
|
||||
check("canvas", &canvas);
|
||||
// The clip group's box takes in the stroke's width, as for a path that paints its stroke.
|
||||
let stroke = canvas.clip_paint().stroke.as_ref().and_then(Node::geometric_bounds).unwrap();
|
||||
let visual = evaluated.children().and_then(|ch| ch.first()).and_then(|g| g.visual_bounds()).unwrap();
|
||||
assert!(visual.x1 > stroke.x1 + 0.25, "{visual:?} {stroke:?}");
|
||||
let mut d = Document::new(400.0, 400.0);
|
||||
let layer = d.layers[0].id;
|
||||
d.insert(Some(layer), 0, outer).unwrap();
|
||||
check("exported", &baked_clip(&d));
|
||||
// Without effects around it, the export paints the fill and the stroke as the canvas does.
|
||||
let mut d = Document::new(400.0, 400.0);
|
||||
let layer = d.layers[0].id;
|
||||
d.insert(Some(layer), 0, clip_group(2, clip.clone())).unwrap();
|
||||
let paint = baked_clip(&d).clip_paint();
|
||||
let fill = paint.fill.as_ref().and_then(Node::geometric_bounds);
|
||||
let stroke = paint.stroke.as_ref().and_then(Node::geometric_bounds);
|
||||
assert_eq!(fill.map(|f| f.width()), Some(120.0), "{paint:?}");
|
||||
assert_eq!(stroke.map(|s| s.x0), Some(80.0), "{paint:?}");
|
||||
// A piece that an effect around it splits into several paths (a compound path), or puts in a
|
||||
// group with its copies, still paints.
|
||||
let NodeKind::Group { children, .. } = &canvas.kind else { panic!("not the pieces: {canvas:?}") };
|
||||
let mut split = canvas.as_ref().clone();
|
||||
let mut compound = children[0].as_ref().clone();
|
||||
compound.kind = NodeKind::Compound { children: vec![children[0].clone(), children[0].clone()], rule: vectorcraft_geom::FillRule::NonZero };
|
||||
let copies = Node::group(NodeId(9), vec![children[1].clone(), children[1].clone()]);
|
||||
if let Some(ch) = split.children_mut() {
|
||||
ch[0] = Arc::new(compound);
|
||||
ch[1] = Arc::new(copies);
|
||||
}
|
||||
let paint = split.clip_paint();
|
||||
assert!(paint.fill.is_some() && paint.stroke.is_some(), "{split:?}");
|
||||
let stroke = paint.stroke.unwrap();
|
||||
assert_eq!(stroke.children().map(|ch| ch.len()), Some(1), "only the stroke's group: {stroke:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_painted_clipping_paths_stroke_keeps_the_width_its_transform_scales_it_to() {
|
||||
// A compound clipping path with a 2 pt stroke whose Transform scales strokes to 200 % without
|
||||
// transforming the object: the export paints the stroke at 4 pt.
|
||||
let square = Node::path(NodeId(4), shapes::rectangle(Rect::new(50.0, 50.0, 150.0, 150.0)), Appearance::default());
|
||||
let mut clip = Node::new(NodeId(3), NodeKind::Compound { children: vec![Arc::new(square)], rule: vectorcraft_geom::FillRule::NonZero });
|
||||
clip.appearance = Appearance::basic(Paint::solid(Color::BLACK), Paint::solid(Color::rgb(0.0, 0.0, 1.0)), 2.0).into();
|
||||
let scale = json!({"scaleH": 200, "scaleV": 200, "objects": false, "strokes": true});
|
||||
for item in &mut clip.appearance.items {
|
||||
if let vectorcraft_doc::AppearanceItem::Stroke(s) = item {
|
||||
s.effects.push(fx("distort.transform", scale.clone()));
|
||||
}
|
||||
}
|
||||
fn widths(n: &Node, out: &mut Vec<f64>) {
|
||||
for item in &n.appearance.items {
|
||||
if let vectorcraft_doc::AppearanceItem::Stroke(s) = item {
|
||||
out.push(s.width);
|
||||
}
|
||||
}
|
||||
n.children().into_iter().flatten().for_each(|c| widths(c, out));
|
||||
}
|
||||
let mut d = Document::new(400.0, 400.0);
|
||||
let layer = d.layers[0].id;
|
||||
d.insert(Some(layer), 0, clip_group(2, clip)).unwrap();
|
||||
let baked = baked_clip(&d);
|
||||
let stroke = baked.clip_paint().stroke.unwrap();
|
||||
let mut found = vec![];
|
||||
widths(&stroke, &mut found);
|
||||
assert!(!found.is_empty() && found.iter().all(|w| (w - 4.0).abs() < 1e-6), "{found:?} {stroke:?}");
|
||||
// The stroke is still drawn where the path lies, as the Transform does not move the object.
|
||||
let bounds = stroke.geometric_bounds().unwrap();
|
||||
assert!((bounds.x0 - 50.0).abs() < 0.5 && (bounds.width() - 100.0).abs() < 0.5, "{bounds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(crate) mod graph;
|
||||
mod halftone;
|
||||
pub mod help;
|
||||
pub mod importmatches;
|
||||
mod info;
|
||||
pub(crate) mod inline;
|
||||
mod interactivity;
|
||||
mod intertwine;
|
||||
@@ -344,6 +345,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(docmenu::specs());
|
||||
v.extend(variablecmds::specs());
|
||||
v.extend(docinfo::specs());
|
||||
v.extend(info::specs());
|
||||
v.extend(panelcmds::specs());
|
||||
v.extend(artboardalign::specs());
|
||||
v.extend(buildcmds::specs());
|
||||
@@ -897,6 +899,9 @@ pub use brushsym::reset_transformation_changes;
|
||||
/// Data window reads files by it too).
|
||||
pub use variablecmds::utf16_order_without_mark;
|
||||
|
||||
/// The Variables panel's sort (`variable.sort`) and its rows in that order.
|
||||
pub use variablecmds::{VariableSort, VariableSortKey, variable_order};
|
||||
|
||||
/// 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")];
|
||||
|
||||
@@ -393,8 +393,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
return Err(err(format!("can't open `{}`: {}", file_name(name), crate::cmd::place::dcs::LINK_ONLY)));
|
||||
}
|
||||
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
|
||||
// A `.ai` keeps the artboard its header gives; an EPS comes in on its page.
|
||||
let import = if matches!(format.id, "ai" | "ait") { vectorcraft_eps::import_ai } else { vectorcraft_eps::import_names };
|
||||
restore_or_import(editing, || {
|
||||
let r = vectorcraft_eps::import_names(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
|
||||
let r = import(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
|
||||
.map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?;
|
||||
Ok((r.document, r.warnings))
|
||||
})?
|
||||
|
||||
@@ -966,7 +966,7 @@ fn an_ai_file_opens_its_variable_library() {
|
||||
{"name": "v_file", "kind": "image", "bindings": 0},
|
||||
{"name": "v_graph", "kind": "graph", "bindings": 0},
|
||||
{"name": "v_unbound", "kind": "unknown", "bindings": 0},
|
||||
], "locked": true})
|
||||
], "locked": true, "sort": {"by": "none", "descending": false}})
|
||||
);
|
||||
let r = s.execute("dataset.list", &json!({})).unwrap();
|
||||
assert_eq!(r["active"], "ds_two", "{r}");
|
||||
|
||||
@@ -277,3 +277,17 @@ fn ps_and_epsf_files_open_and_place() {
|
||||
let e = writable_format(None, Some("out.ps")).unwrap_err();
|
||||
assert_eq!(e, "PostScript files can be opened but not written (see document.formats)");
|
||||
}
|
||||
|
||||
/// A program in the legacy `.ai` format with a template box and no art size (version 3) opens as a
|
||||
/// `.ai` or `.ait` on a US Letter artboard around the box's center, and as an EPS on its bounding
|
||||
/// box.
|
||||
#[test]
|
||||
fn a_legacy_ai_program_opens_on_its_artboard_and_an_eps_on_its_page() {
|
||||
let program = b"%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 30 30\n%AI3_TemplateBox: 100 100 100 100\n\
|
||||
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def /k {setcmykcolor} def\n\
|
||||
%%EndProlog\n%%BeginSetup\n%%EndSetup\n0 0 0 1 k\n10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
|
||||
for (name, size) in [("old.ai", (612.0, 792.0)), ("old.ait", (612.0, 792.0)), ("old.eps", (20.0, 20.0))] {
|
||||
let r = load(name, program).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(r.doc.artboards[0].rect.size(), vectorcraft_geom::Size::new(size.0, size.1), "{name}: {:?}", r.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,10 +386,32 @@ fn replacing_a_clipped_psd_keeps_its_scale() {
|
||||
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{r}");
|
||||
let (anchors, _, _) = clip_group(&s.doc().unwrap().doc, NodeId(r["ids"][0].as_u64().unwrap()));
|
||||
assert_eq!(anchors, [[200.0, 150.0], [300.0, 250.0], [100.0, 250.0]]);
|
||||
// Other art replacing the clip group takes its centre only, as before.
|
||||
// A plain image replacing the clip group keeps its scale too.
|
||||
let plain = vectorcraft_format::base64_encode(&red_psd(&[]));
|
||||
let r = s.execute("file.place", &json!({"name": "plain.psd", "dataBase64": plain, "replace": true})).unwrap();
|
||||
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(100.0), Some(50.0)), "{r}");
|
||||
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{r}");
|
||||
}
|
||||
|
||||
/// A plain image replacing a clipped PSD file takes the clipped image's scale and rotation.
|
||||
#[test]
|
||||
fn a_plain_image_replacing_a_clipped_psd_keeps_its_rotation() {
|
||||
let data = vectorcraft_format::base64_encode(&red_psd(&triangle_resources()));
|
||||
let plain = vectorcraft_format::base64_encode(&red_psd(&[]));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let r = s.execute("file.place", &json!({"name": "cut.psd", "dataBase64": data, "at": [200, 200]})).unwrap();
|
||||
let group = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
s.execute("object.scale", &json!({"sx": 200, "sy": 150})).unwrap();
|
||||
s.execute("object.rotate", &json!({"angle": 30})).unwrap();
|
||||
let xf = |s: &Session, id: NodeId| match s.doc().unwrap().doc.node(id).map(|n| &n.kind) {
|
||||
Some(NodeKind::Image(im)) => im.xf.as_coeffs(),
|
||||
other => panic!("not an image: {other:?}"),
|
||||
};
|
||||
let (_, _, image) = clip_group(&s.doc().unwrap().doc, group);
|
||||
let before = xf(&s, image);
|
||||
let r = s.execute("file.place", &json!({"name": "plain.psd", "dataBase64": plain, "replace": true})).unwrap();
|
||||
let after = xf(&s, NodeId(r["ids"][0].as_u64().unwrap()));
|
||||
assert!(after.iter().zip(before).all(|(a, b)| (a - b).abs() < 1e-9), "{after:?} != {before:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -40,7 +40,9 @@ pub use commands::specs;
|
||||
pub(crate) use data::{apply_csv, parse_csv};
|
||||
#[cfg(test)]
|
||||
use data::{fmt_value, to_csv};
|
||||
pub use edit::{GraphTypeSubject, graph_type_subject};
|
||||
pub(crate) use edit::{graphs_holding, keep_placed};
|
||||
pub use generate::calculated_axes;
|
||||
#[cfg(test)]
|
||||
use generate::generate;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -2,7 +2,7 @@ use vectorcraft_doc::graph::graph_art_without_type as art_bounds;
|
||||
|
||||
use super::axis::{MAX_TICK_MARKS, count_given, one_line};
|
||||
use super::data::apply_data;
|
||||
use super::generate::{children_bounds, generate};
|
||||
use super::generate::{calculated_axes, children_bounds, generate};
|
||||
use super::series::{picked_series, reselect_series};
|
||||
use super::{
|
||||
Affine, Document, EngineError, GraphKind, GraphSpec, Node, NodeId, Rect, Result, Session, TickLength, Value, ValueAxisSide, bad, f64_req, json,
|
||||
@@ -169,6 +169,28 @@ pub(super) fn spec_of(s: &Session, id: NodeId) -> Result<GraphSpec> {
|
||||
s.doc()?.doc.node(id).and_then(|n| n.extras().graph.as_deref().cloned()).ok_or(EngineError::NoNode(id))
|
||||
}
|
||||
|
||||
/// What `graph.setType` without `id` or `seriesIndexes` acts on, for the Graph Type dialog.
|
||||
pub struct GraphTypeSubject {
|
||||
/// The selected graph, or the graph that holds the selection.
|
||||
pub id: NodeId,
|
||||
/// The series picked in it with Group Selection, in order; empty when the type goes to the whole graph.
|
||||
pub series: Vec<usize>,
|
||||
/// The least value, the greatest and the number of divisions each value axis is drawn with while its tick values
|
||||
/// are calculated: the left (or only) axis, then the right axis' own scale (the left one's without Separate Scales).
|
||||
pub calculated: [(f64, f64, usize); 2],
|
||||
}
|
||||
|
||||
/// The graph and series that `graph.setType {}` reads its options from, as [`GraphTypeSubject`]. An error without a
|
||||
/// selected graph, as the command gives.
|
||||
pub fn graph_type_subject(s: &Session) -> Result<GraphTypeSubject> {
|
||||
const C: &str = "graph.setType";
|
||||
let p = json!({});
|
||||
let id = target(s, &p, C)?;
|
||||
let spec = spec_of(s, id)?;
|
||||
let count = spec.rows.iter().map(Vec::len).max().unwrap_or(0).min(vectorcraft_doc::MAX_GRAPH_SERIES);
|
||||
Ok(GraphTypeSubject { id, series: picked_series(s, &p, id, count, C)?, calculated: calculated_axes(&spec) })
|
||||
}
|
||||
|
||||
fn query_type_options(spec: &GraphSpec, count: usize, picked: &[usize]) -> Value {
|
||||
// With series picked, the type and value axis they share, so the dialog's OK keeps them.
|
||||
let kinds: Vec<GraphKind> = picked.iter().map(|i| spec.series_kind(*i)).collect();
|
||||
|
||||
@@ -38,6 +38,101 @@ fn nth<T>(list: &[Option<T>], s: usize) -> Option<&T> {
|
||||
list.get(s).and_then(Option::as_ref)
|
||||
}
|
||||
|
||||
/// The data a value range is read from: each series' type, the cells (a blank is `None`), which series use the right
|
||||
/// axis' own scale, and the number of categories.
|
||||
struct Values<'a> {
|
||||
kinds: &'a [GraphKind],
|
||||
cells: &'a [Vec<Option<f64>>],
|
||||
on_right: &'a [bool],
|
||||
ncat: usize,
|
||||
}
|
||||
|
||||
impl Values<'_> {
|
||||
fn cell(&self, c: usize, s: usize) -> Option<f64> {
|
||||
self.cells.get(c).and_then(|r| r.get(s)).copied().flatten()
|
||||
}
|
||||
|
||||
fn axis(&self, s: usize) -> usize {
|
||||
usize::from(self.on_right.get(s).copied().unwrap_or(false))
|
||||
}
|
||||
|
||||
/// The value range per axis (stacked graphs stack positives and negatives separately), and a scatter's x range.
|
||||
fn range(&self, scatter: bool) -> ([f64; 2], [f64; 2], f64, f64) {
|
||||
let (mut lo, mut hi) = ([0.0f64; 2], [0.0f64; 2]);
|
||||
let (mut xlo, mut xhi) = (f64::MAX, f64::MIN);
|
||||
for c in 0..self.ncat {
|
||||
if !scatter {
|
||||
for (a, (lo, hi)) in lo.iter_mut().zip(hi.iter_mut()).enumerate() {
|
||||
self.extend(c, a, (lo, hi));
|
||||
}
|
||||
} else {
|
||||
for s in (0..self.kinds.len()).step_by(2) {
|
||||
if let (Some(y), Some(x)) = (self.cell(c, s), self.cell(c, s + 1)) {
|
||||
hi[0] = hi[0].max(y);
|
||||
lo[0] = lo[0].min(y);
|
||||
xlo = xlo.min(x);
|
||||
xhi = xhi.max(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(lo, hi, xlo, xhi)
|
||||
}
|
||||
|
||||
/// Widen the range of axis `a` by category `c`'s values.
|
||||
fn extend(&self, c: usize, a: usize, (lo, hi): (&mut f64, &mut f64)) {
|
||||
let val = |s: usize| self.cell(c, s).unwrap_or(0.0);
|
||||
let on = |s: &usize, kinds: &[GraphKind]| self.axis(*s) == a && self.kinds.get(*s).is_some_and(|k| kinds.contains(k));
|
||||
// Stacked columns (or bars) stack together, and so do areas; other series count one value each.
|
||||
for stack in [[GraphKind::StackedColumn, GraphKind::StackedBar], [GraphKind::Area, GraphKind::Area]] {
|
||||
let members = || (0..self.kinds.len()).filter(|s| on(s, &stack));
|
||||
*hi = hi.max(members().map(|s| val(s).max(0.0)).sum());
|
||||
*lo = lo.min(members().map(|s| val(s).min(0.0)).sum());
|
||||
}
|
||||
let single = |s: &usize| on(s, &[GraphKind::Column, GraphKind::Bar, GraphKind::Line]);
|
||||
for v in (0..self.kinds.len()).filter(single).filter_map(|s| self.cell(c, s)) {
|
||||
*hi = hi.max(v);
|
||||
*lo = lo.min(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The tick values `g`'s value axes are drawn with while they are calculated from the data (Graph Type › Tick Values
|
||||
/// without Override Calculated Values): the least value, the greatest and the number of divisions of the left (or
|
||||
/// only) axis, then of the right axis' own scale, which is the left one's unless Separate Scales gives the series on
|
||||
/// the right axis a scale of their own. A pie graph has no value axis and gives the axis of a graph without data. A
|
||||
/// single bound the graph has (only a minimum or only a maximum) is calculated with, as the axis is drawn.
|
||||
pub fn calculated_axes(g: &GraphSpec) -> [(f64, f64, usize); 2] {
|
||||
// Adding 0 makes a negative zero, which a nice axis around 0 can start at, a plain 0.
|
||||
let divisions = |(lo, hi, step): (f64, f64, f64)| (lo + 0.0, hi + 0.0, ((hi - lo) / step).round().clamp(1.0, 100.0) as usize);
|
||||
// The bounds the axis is calculated with: all but a pair that overrides the calculation.
|
||||
let partial = |min: Option<f64>, max: Option<f64>| match (min, max) {
|
||||
(Some(a), Some(b)) if b > a && (b - a).is_finite() => (None, None),
|
||||
bounds => bounds,
|
||||
};
|
||||
let (min, max) = partial(g.axis_min, g.axis_max);
|
||||
if g.kind == GraphKind::Radar {
|
||||
let top = g.rows.iter().flatten().copied().fold(0.0, f64::max);
|
||||
return [divisions(value_axis(g.ticks, min, max, 0.0, top)); 2];
|
||||
}
|
||||
let nser = g.rows.iter().map(Vec::len).max().unwrap_or(0).clamp(1, vectorcraft_doc::MAX_GRAPH_SERIES);
|
||||
let kinds: Vec<GraphKind> = (0..nser).map(|s| g.series_kind(s)).collect();
|
||||
let (scatter, horizontal) = (g.kind == GraphKind::Scatter, matches!(g.kind, GraphKind::Bar | GraphKind::StackedBar));
|
||||
let separate = g.separate_scales && g.value_axis == ValueAxisSide::Both && !horizontal && !scatter;
|
||||
let on_right: Vec<bool> = (0..nser).map(|s| separate && g.right_series.contains(&s)).collect();
|
||||
let cells = if g.kind == GraphKind::Pie { vec![] } else { g.cells() };
|
||||
let ncat = g.rows.len().clamp(1, vectorcraft_doc::MAX_GRAPH_CATEGORIES);
|
||||
let (lo, hi, ..) = Values { kinds: &kinds, cells: &cells, on_right: &on_right, ncat }.range(scatter);
|
||||
let left = divisions(value_axis(g.ticks, min, max, lo[0], hi[0]));
|
||||
let used = |right: bool| on_right.contains(&right);
|
||||
if !used(true) {
|
||||
return [left; 2];
|
||||
}
|
||||
let (right_min, right_max) = partial(g.right_axis_min, g.right_axis_max);
|
||||
let right = divisions(value_axis(g.right_ticks, right_min, right_max, lo[1], hi[1]));
|
||||
if used(false) { [left, right] } else { [right; 2] }
|
||||
}
|
||||
|
||||
/// One graph being generated: its data, the nodes drawn so far and what the legend and shadows still need.
|
||||
struct Plot<'a> {
|
||||
g: &'a GraphSpec,
|
||||
@@ -526,41 +621,7 @@ impl<'a> Plot<'a> {
|
||||
|
||||
/// The value range per axis (stacked graphs stack positives and negatives separately), and a scatter's x range.
|
||||
fn value_range(&self, geo: &Geo, scatter: bool) -> ([f64; 2], [f64; 2], f64, f64) {
|
||||
let (mut lo, mut hi) = ([0.0f64; 2], [0.0f64; 2]);
|
||||
let (mut xlo, mut xhi) = (f64::MAX, f64::MIN);
|
||||
for c in 0..self.ncat {
|
||||
if !scatter {
|
||||
for (a, (lo, hi)) in lo.iter_mut().zip(hi.iter_mut()).enumerate() {
|
||||
self.extend_range(geo, c, a, (lo, hi));
|
||||
}
|
||||
} else {
|
||||
for s in (0..self.nser).step_by(2) {
|
||||
if let (Some(y), Some(x)) = (self.cell(c, s), self.cell(c, s + 1)) {
|
||||
hi[0] = hi[0].max(y);
|
||||
lo[0] = lo[0].min(y);
|
||||
xlo = xlo.min(x);
|
||||
xhi = xhi.max(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(lo, hi, xlo, xhi)
|
||||
}
|
||||
|
||||
/// Widen the range of axis `a` by category `c`'s values.
|
||||
fn extend_range(&self, geo: &Geo, c: usize, a: usize, (lo, hi): (&mut f64, &mut f64)) {
|
||||
let nser = self.nser;
|
||||
// Stacked columns (or bars) stack together, and so do areas; other series count one value each.
|
||||
for stack in [[GraphKind::StackedColumn, GraphKind::StackedBar], [GraphKind::Area, GraphKind::Area]] {
|
||||
let members = || (0..nser).filter(|s| geo.axis(*s) == a && stack.contains(&self.kind(*s)));
|
||||
*hi = hi.max(members().map(|s| self.val(c, s).max(0.0)).sum());
|
||||
*lo = lo.min(members().map(|s| self.val(c, s).min(0.0)).sum());
|
||||
}
|
||||
let single = |s: &usize| geo.axis(*s) == a && matches!(self.kind(*s), GraphKind::Column | GraphKind::Bar | GraphKind::Line);
|
||||
for v in (0..nser).filter(single).filter_map(|s| self.cell(c, s)) {
|
||||
*hi = hi.max(v);
|
||||
*lo = lo.min(v);
|
||||
}
|
||||
Values { kinds: &self.kinds, cells: &self.cells, on_right: &geo.on_right, ncat: self.ncat }.range(scatter)
|
||||
}
|
||||
|
||||
/// The value axes with ticks and labels, the legend's start after them, and the axis lines.
|
||||
|
||||
@@ -4670,6 +4670,58 @@ fn group_selected_series_go_on_the_chosen_axis() {
|
||||
assert!(spec(&s, id).right_series.is_empty());
|
||||
}
|
||||
|
||||
/// The Graph Type dialog's subject: the selected graph, the series picked in it, and the tick values each value axis
|
||||
/// is drawn with while they are calculated, which are the ones the graph's value labels show.
|
||||
#[test]
|
||||
fn graph_type_subject_gives_the_picked_series_and_the_calculated_tick_values() {
|
||||
let mut s = Session::new();
|
||||
assert!(super::graph_type_subject(&s).is_err(), "no document");
|
||||
s.execute("file.new", &json!({"width": 800, "height": 800})).unwrap();
|
||||
assert!(super::graph_type_subject(&s).is_err(), "no graph selected");
|
||||
// Q1 3, 2; Q2 5, 4; Q3 -1, 6.
|
||||
let id = graph(&mut s, "column");
|
||||
let subject = super::graph_type_subject(&s).unwrap();
|
||||
assert_eq!((subject.id, subject.series), (id, vec![]));
|
||||
assert_eq!(subject.calculated, [(-2.0, 6.0, 4); 2]);
|
||||
let shown = |s: &Session| value_labels(s, id).into_iter().map(|(t, _)| t).collect::<Vec<_>>();
|
||||
assert_eq!(shown(&s), ["-2", "0", "2", "4", "6"]);
|
||||
// Overriding with them draws the same axis, and the calculated values stay the data's.
|
||||
s.execute("graph.setType", &json!({"axisMin": -2, "axisMax": 6, "ticks": 4})).unwrap();
|
||||
assert_eq!(shown(&s), ["-2", "0", "2", "4", "6"]);
|
||||
s.execute("graph.setType", &json!({"axisMin": 0, "axisMax": 100, "ticks": 0})).unwrap();
|
||||
assert_eq!(super::graph_type_subject(&s).unwrap().calculated[0], (-2.0, 6.0, 4));
|
||||
// A single bound is calculated with, as the axis is drawn.
|
||||
s.execute("graph.setType", &json!({"axisMin": -10, "axisMax": null})).unwrap();
|
||||
let (lo, hi, _) = super::graph_type_subject(&s).unwrap().calculated[0];
|
||||
let labels = shown(&s);
|
||||
assert!(lo <= -10.0, "{lo}");
|
||||
assert_eq!((labels.first().and_then(|t| t.parse::<f64>().ok()), labels.last().and_then(|t| t.parse::<f64>().ok())), (Some(lo), Some(hi)));
|
||||
s.execute("graph.setType", &json!({"axisMin": null, "axisMax": null})).unwrap();
|
||||
// Stacked columns stack their positive and their negative values: Q2 reaches 9, Q3 from -1 to 6.
|
||||
s.execute("graph.setType", &json!({"type": "stackedColumn"})).unwrap();
|
||||
assert_eq!(super::graph_type_subject(&s).unwrap().calculated[0], (-2.0, 10.0, 6));
|
||||
assert_eq!(shown(&s), ["-2", "0", "2", "4", "6", "8", "10"]);
|
||||
// A radar graph's axis starts at 0, and a pie graph has none.
|
||||
s.execute("graph.setType", &json!({"type": "radar"})).unwrap();
|
||||
assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 6.0, 3); 2]);
|
||||
s.execute("graph.setType", &json!({"type": "pie"})).unwrap();
|
||||
assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 1.0, 5); 2]);
|
||||
// Series picked with Group Selection.
|
||||
s.execute("graph.setType", &json!({"type": "column"})).unwrap();
|
||||
let picked = series(s.doc().unwrap().doc.node(id).unwrap(), 1).id;
|
||||
s.execute("select.set", &json!({"ids": [picked.0]})).unwrap();
|
||||
let subject = super::graph_type_subject(&s).unwrap();
|
||||
assert_eq!((subject.id, subject.series), (id, vec![1]));
|
||||
// Separate Scales: the right axis has the tick values of its own series.
|
||||
let mut s = Session::new();
|
||||
let id = two_scale_graph(&mut s);
|
||||
assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 1000.0, 5); 2]);
|
||||
s.execute("graph.setType", &json!({"seriesIndexes": [1], "valueAxis": "right"})).unwrap();
|
||||
s.execute("graph.setType", &json!({"valueAxis": "both", "separateScales": true})).unwrap();
|
||||
assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 10.0, 5), (0.0, 1000.0, 5)]);
|
||||
assert_eq!(spec(&s, id).right_series, [1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_graphs_keep_their_value_axis_along_the_bottom() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Window › Info (`info.readout`): the selection's size and the active tool's readout, as the Info
|
||||
//! panel shows them.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_render::effects;
|
||||
|
||||
use super::{CommandSpec, Result, Session, always, cmd};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
query "info.readout",
|
||||
"Info",
|
||||
[],
|
||||
None,
|
||||
"{} → {tool: the active tool's id, width?, height?: the sides of the selection's bounding box in pt (the box around the anchors selected with Direct Selection, when there are some), measured as prefs usePreviewBounds says; left out with nothing selected, readout?: what the Info panel shows for the tool in use: {kind: \"drag\", dx, dy, distance, angle} while the Selection, Direct Selection or Group Selection tool moves the selection or the Gradient tool draws a gradient vector on the art; {kind: \"scale\", width, height} (percent) during a Scale tool drag; {kind: \"rotate\", center: [x, y], angle: deg counter-clockwise, as object.rotate takes it} and {kind: \"reflect\", center, angle: the axis in deg counter-clockwise from horizontal, as object.reflect takes it} during a Rotate or Reflect tool drag; {kind: \"shear\", center, axis: 0 for a horizontal shear and 90 for a vertical one, angle: deg, as object.shear takes it} during a Shear tool drag; {kind: \"brush\", brush: name|null} while the Paintbrush is the tool; left out for any other tool and between drags. Lengths are in pt, a drag's angle is in deg counter-clockwise from the x axis, and center is the center of the selection's bounding box as the drag has carried it, in document coordinates}",
|
||||
always,
|
||||
readout
|
||||
)]
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// The sides of the selection's bounding box as the Info panel shows them: of the box around
|
||||
/// the anchors selected with Direct Selection when there are some, else of the bounding box
|
||||
/// rotated with rotated objects ([`Session::transform_box`]), measured as Use Preview Bounds
|
||||
/// says. None with nothing selected.
|
||||
pub fn info_size(&self) -> Option<(f64, f64)> {
|
||||
let st = self.active()?;
|
||||
let anchors = effects::preview::anchor_bounds_with(&st.selection, &st.doc, self.bounds_measure());
|
||||
let r = anchors.or_else(|| self.transform_box(&st.selection.objects).map(|b| b.rect))?;
|
||||
Some((r.width(), r.height()))
|
||||
}
|
||||
}
|
||||
|
||||
fn readout(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let mut out = json!({ "tool": s.tool_id() });
|
||||
if let Some((w, h)) = s.info_size() {
|
||||
out["width"] = json!(w);
|
||||
out["height"] = json!(h);
|
||||
}
|
||||
if let Some(r) = s.tool_readout() {
|
||||
out["readout"] = r.to_json();
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use crate::{Session, ViewInfo};
|
||||
|
||||
fn view() -> ViewInfo {
|
||||
ViewInfo { smart_guides: false, snap_to_point: false, ..Default::default() }
|
||||
}
|
||||
|
||||
/// A 100 by 60 rectangle at (50, 50), selected, with a 10 pt stroke.
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 60})).unwrap()["id"].clone();
|
||||
s.execute("select.set", &json!({ "ids": [id] })).unwrap();
|
||||
s.execute("stroke.set", &json!({"weight": 10})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn tool(s: &mut Session, id: &str) {
|
||||
s.select_tool(id, view()).unwrap();
|
||||
}
|
||||
|
||||
fn pointer(s: &mut Session, kind: PointerKind, x: f64, y: f64) {
|
||||
s.pointer(&PointerEvent::new(kind, x, y), view()).unwrap();
|
||||
}
|
||||
|
||||
fn info(s: &mut Session) -> serde_json::Value {
|
||||
s.execute("info.readout", &json!({})).unwrap()
|
||||
}
|
||||
|
||||
fn near(v: &serde_json::Value, want: f64) -> bool {
|
||||
v.as_f64().is_some_and(|v| (v - want).abs() < 1e-6)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_size_follows_use_preview_bounds() {
|
||||
let mut s = session();
|
||||
let i = info(&mut s);
|
||||
assert!(near(&i["width"], 100.0) && near(&i["height"], 60.0), "{i}");
|
||||
assert!(i.get("readout").is_none(), "{i}");
|
||||
s.execute("prefs.set", &json!({"key": "usePreviewBounds", "value": true})).unwrap();
|
||||
let i = info(&mut s);
|
||||
assert!(near(&i["width"], 110.0) && near(&i["height"], 70.0), "{i}");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let i = info(&mut s);
|
||||
assert!(i.get("width").is_none() && i.get("height").is_none(), "{i}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_move_reads_how_far_it_has_gone_until_the_release() {
|
||||
let mut s = session();
|
||||
tool(&mut s, "selection");
|
||||
pointer(&mut s, PointerKind::Down, 100.0, 80.0);
|
||||
assert!(info(&mut s).get("readout").is_none());
|
||||
pointer(&mut s, PointerKind::Drag, 130.0, 40.0);
|
||||
let i = info(&mut s);
|
||||
let r = &i["readout"];
|
||||
assert_eq!(r["kind"], "drag", "{i}");
|
||||
assert!(near(&r["dx"], 30.0) && near(&r["dy"], -40.0) && near(&r["distance"], 50.0), "{i}");
|
||||
assert!(near(&r["angle"], 53.13010235415598), "{i}");
|
||||
pointer(&mut s, PointerKind::Up, 130.0, 40.0);
|
||||
let i = info(&mut s);
|
||||
assert!(i.get("readout").is_none(), "{i}");
|
||||
assert!(near(&i["width"], 100.0), "{i}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_transform_tool_reads_its_drag() {
|
||||
for (id, check) in [
|
||||
("scale", (|r| r["kind"] == "scale" && near(&r["width"], 200.0) && near(&r["height"], 200.0)) as fn(&serde_json::Value) -> bool),
|
||||
("rotate", |r| r["kind"] == "rotate" && near(&r["center"][0], 100.0) && near(&r["center"][1], 80.0) && near(&r["angle"], -90.0)),
|
||||
("reflect", |r| r["kind"] == "reflect" && near(&r["center"][0], 100.0) && near(&r["angle"], 0.0)),
|
||||
("shear", |r| {
|
||||
r["kind"] == "shear"
|
||||
&& near(&r["center"][0], 100.0)
|
||||
&& near(&r["center"][1], 80.0)
|
||||
&& near(&r["axis"], 0.0)
|
||||
&& near(&r["angle"], 45.0)
|
||||
}),
|
||||
] {
|
||||
let mut s = session();
|
||||
tool(&mut s, id);
|
||||
// The reference point is the box's center (100, 80).
|
||||
let from = if id == "shear" { (100.0, 60.0) } else { (120.0, 100.0) };
|
||||
pointer(&mut s, PointerKind::Down, from.0, from.1);
|
||||
let to = match id {
|
||||
"scale" => (140.0, 120.0),
|
||||
"rotate" => (80.0, 100.0),
|
||||
"reflect" => (160.0, 80.0),
|
||||
_ => (120.0, 60.0),
|
||||
};
|
||||
pointer(&mut s, PointerKind::Drag, to.0, to.1);
|
||||
let i = info(&mut s);
|
||||
assert_eq!(i["tool"], id);
|
||||
assert!(check(&i["readout"]), "{id}: {i}");
|
||||
pointer(&mut s, PointerKind::Up, to.0, to.1);
|
||||
assert!(info(&mut s).get("readout").is_none(), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_paintbrush_reads_its_brush() {
|
||||
let mut s = session();
|
||||
tool(&mut s, "paintbrush");
|
||||
let i = info(&mut s);
|
||||
assert_eq!(i["readout"], json!({"kind": "brush", "brush": null}), "{i}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_parameter_is_refused() {
|
||||
let mut s = session();
|
||||
let e = s.execute("info.readout", &json!({"tool": "scale"})).unwrap_err().to_string();
|
||||
assert!(e.contains("tool"), "{e}");
|
||||
}
|
||||
}
|
||||
@@ -495,14 +495,14 @@ fn at_param(p: &Value) -> Result<Option<Point>> {
|
||||
|
||||
/// The transform that puts new art with the natural box `natural` where `old` is: an image's scale
|
||||
/// and rotation relative to its 100% size (its resolution's) and its centre are kept (with
|
||||
/// `image_xf`), also for an image alone in a clip group when the new art is an image clipped by
|
||||
/// its file's clipping path (`clipped`); other objects keep their centre.
|
||||
fn replace_xf(d: &Document, old: &Node, natural: Rect, image_xf: bool, clipped: bool) -> Affine {
|
||||
/// `image_xf`), also for an image alone in a clip group when the new art is an image, clipped by
|
||||
/// its file's clipping path or not (`new_image`); other objects keep their centre.
|
||||
fn replace_xf(d: &Document, old: &Node, natural: Rect, image_xf: bool, new_image: bool) -> Affine {
|
||||
// An image alone in a clip group (as a PSD or PSB file's clipping path places it,
|
||||
// [`fileio::file_clipped`]) is measured as the image when the new art brings its own clip
|
||||
// group; a DCS file's plates are not.
|
||||
// [`fileio::file_clipped`]) is measured as the image when the new art is an image; a DCS
|
||||
// file's plates are not.
|
||||
let image = match &old.kind {
|
||||
NodeKind::Group { children, clip: true } if clipped => match &children[..] {
|
||||
NodeKind::Group { children, clip: true } if new_image => match &children[..] {
|
||||
[path, image] if matches!(path.kind, NodeKind::Path { clipping: true, .. }) => match &image.kind {
|
||||
NodeKind::Image(im) if d.images.get(&im.key).is_none_or(|b| b.separated().is_none()) => image,
|
||||
_ => old,
|
||||
@@ -681,7 +681,7 @@ fn apply(s: &mut Session, read: PlaceRead) -> Result<Value> {
|
||||
}
|
||||
let natural = loaded.natural;
|
||||
let m = match (old_node, rect) {
|
||||
(Some(o), _) => replace_xf(&st.doc, o, natural, !is_text, matches!(loaded.art, Art::Image(_, Some(_)))),
|
||||
(Some(o), _) => replace_xf(&st.doc, o, natural, !is_text, matches!(loaded.art, Art::Image(..))),
|
||||
(None, Some(r)) => fit(natural, r),
|
||||
// Its artboard's bottom-left corner on the target's.
|
||||
(None, None) if let (Some(src), Some(to)) = (loaded.board, target) => Affine::translate((to.x0 - src.x0, to.y1 - src.y1)),
|
||||
|
||||
@@ -206,6 +206,7 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"imageTrace.makeAndExpand",
|
||||
"imageTrace.release",
|
||||
"imageTrace.setView",
|
||||
"info.readout",
|
||||
"layer.clippingMask.toggle",
|
||||
"layer.collectInNew",
|
||||
"layer.delete",
|
||||
@@ -693,6 +694,7 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"variable.makeTextDynamic",
|
||||
"variable.makeVisibilityDynamic",
|
||||
"variable.rename",
|
||||
"variable.sort",
|
||||
"variable.unbind",
|
||||
];
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Recolor Artwork",
|
||||
["Edit", "Edit Colors"],
|
||||
None,
|
||||
"{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex)], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null (in from, to and groupColors, #rrggbbaa and #rgba are refused), exclude?: false (the row's colours are kept), tint?: 100 (percent; with limitTo, art linked to the global or spot swatch the new color is takes this share of its tint)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library id or name; or \"document\" (the document's swatches): new colours snap to its nearest colour, group?: colour group to rewrite, groupColors?: [colour] (what `group` is rewritten with; default: the rows' new colours, in order, excluded rows left out), rename?: the group's new name, includeImages?: true, includePatterns?: true, globalAdjust?: {saturation?: 0, brightness?: 0, temperature?: 0, luminosity?: 0} (Global Adjust: percent from -100 to 100, 0 changes nothing; every colour a row that isn't excluded recolours, and the group's new colours, take the same change after the row's new colour: saturation scales HSB saturation by 1 + saturation/100, brightness moves HSB brightness that share of the way to full or to black, temperature adds 0.2 × temperature/100 to red and takes it from blue, luminosity moves Lab lightness that share of the way to 100 or 0 keeping a and b; with limitTo each row's new colour takes the change before it snaps to the library)} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces (model?: \"keep\" default | \"new\" (the new colour's own model, so a CMYK colour replacing RGB art gives CMYK art) | \"document\" (the document's colour mode)), except a library swatch's with limitTo: art takes the swatch itself, linked to a global or spot colour at its tints, and the swatch joins the document's swatches; art linked to a global swatch of `group` that is rewritten with its row's new color stays linked to it, at its tint, unless the row converts the color into the art's model; other recolored art, and art in a row linked to a global swatch of `group` that is rewritten with another color, keeps its row's new color and loses its swatch link) and the group, as one undo step (none when nothing changes). On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke, and on a graph label the new paint of its characters and rows for every label of its kind (docs/mcp.md) → {changed, swatches: [names added to the document's swatches], relinked?: with group, the other paints linked to its global swatches that took their new colors, note?: set when a colour kept a model other than its new colour's (see model), notChanged: {locked, hidden} (as edit.colors.invert), warning?: set when notChanged counts any object}",
|
||||
"{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex); may be empty: an empty row recolours nothing, adds no library swatch and gives `group` no colour], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null (in from, to and groupColors, #rrggbbaa and #rgba are refused), exclude?: false (the row's colours are kept), tint?: 100 (percent; with limitTo, art linked to the global or spot swatch the new color is takes this share of its tint)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library id or name; or \"document\" (the document's swatches): new colours snap to its nearest colour, group?: colour group to rewrite, groupColors?: [colour] (what `group` is rewritten with; default: the rows' new colours, in order, excluded and empty rows left out), rename?: the group's new name, includeImages?: true, includePatterns?: true, globalAdjust?: {saturation?: 0, brightness?: 0, temperature?: 0, luminosity?: 0} (Global Adjust: percent from -100 to 100, 0 changes nothing; every colour a row that isn't excluded recolours, and the group's new colours, take the same change after the row's new colour: saturation scales HSB saturation by 1 + saturation/100, brightness moves HSB brightness that share of the way to full or to black, temperature adds 0.2 × temperature/100 to red and takes it from blue, luminosity moves Lab lightness that share of the way to 100 or 0 keeping a and b; with limitTo each row's new colour takes the change before it snaps to the library)} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces (model?: \"keep\" default | \"new\" (the new colour's own model, so a CMYK colour replacing RGB art gives CMYK art) | \"document\" (the document's colour mode)), except a library swatch's with limitTo: art takes the swatch itself, linked to a global or spot colour at its tints, and the swatch joins the document's swatches; art linked to a global swatch of `group` that is rewritten with its row's new color stays linked to it, at its tint, unless the row converts the color into the art's model; other recolored art, and art in a row linked to a global swatch of `group` that is rewritten with another color, keeps its row's new color and loses its swatch link) and the group, as one undo step (none when nothing changes). On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke, and on a graph label the new paint of its characters and rows for every label of its kind (docs/mcp.md) → {changed, swatches: [names added to the document's swatches], relinked?: with group, the other paints linked to its global swatches that took their new colors, note?: set when a colour kept a model other than its new colour's (see model), notChanged: {locked, hidden} (as edit.colors.invert), warning?: set when notChanged counts any object}",
|
||||
has_doc,
|
||||
apply
|
||||
),
|
||||
@@ -1614,8 +1614,11 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(k) => super::swatchlib::library_swatches(s, k).unwrap_or_default(),
|
||||
None => vec![],
|
||||
};
|
||||
let row_swatches: Vec<Option<Swatch>> =
|
||||
rows.iter().map(|r| r.new_color().and_then(|c| lib_swatches.iter().find(|w| w.paint.color() == Some(c)).cloned())).collect();
|
||||
// A row with no current colors recolors nothing, so its swatch does not join the document.
|
||||
let row_swatches: Vec<Option<Swatch>> = rows
|
||||
.iter()
|
||||
.map(|r| r.new_color().filter(|_| !r.from.is_empty()).and_then(|c| lib_swatches.iter().find(|w| w.paint.color() == Some(c)).cloned()))
|
||||
.collect();
|
||||
// While an object of a dynamic symbol instance's art is selected, the selection is refused, as
|
||||
// `selected_roots` refuses it.
|
||||
super::edit::refuse_symbol_part(s)?;
|
||||
@@ -1657,7 +1660,8 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.ok_or_else(|| bad(C, format!("bad colour {v} in groupColors")))
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
None => rows.iter().filter_map(Row::new_color).map(|c| mapped_adjust.apply(c)).collect(),
|
||||
// A row with no current colors gives the group no color.
|
||||
None => rows.iter().filter(|r| !r.from.is_empty()).filter_map(Row::new_color).map(|c| mapped_adjust.apply(c)).collect(),
|
||||
};
|
||||
let rename = text_param(p, "rename", C)?;
|
||||
let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok();
|
||||
|
||||
@@ -330,18 +330,19 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn list(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
let xml_id = s.prefs.identify_objects_by == "xmlId";
|
||||
let st = s.doc()?;
|
||||
let variables: Vec<Value> = st
|
||||
.doc
|
||||
.variables
|
||||
.variables
|
||||
.iter()
|
||||
let vars = &st.doc.variables.variables;
|
||||
let variables: Vec<Value> = variable_order(st, xml_id)
|
||||
.into_iter()
|
||||
.filter_map(|i| vars.get(i))
|
||||
.map(|v| {
|
||||
let bindings = st.doc.variables.objects_of(&v.name).len();
|
||||
json!({"name": v.name, "kind": v.kind.label(), "bindings": bindings})
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({"variables": variables, "locked": st.doc.variables.locked}))
|
||||
let sort = json!({"by": st.variables_sort.by.id(), "descending": st.variables_sort.descending});
|
||||
Ok(json!({"variables": variables, "locked": st.doc.variables.locked, "sort": sort}))
|
||||
}
|
||||
|
||||
/// Lock Variables: without `locked` it toggles. Setting the state the document already has
|
||||
@@ -417,6 +418,120 @@ fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"variable": name}))
|
||||
}
|
||||
|
||||
/// The column the Variables panel sorts its list by (`variable.sort`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum VariableSortKey {
|
||||
/// The order the variables were defined in.
|
||||
#[default]
|
||||
Defined,
|
||||
/// The Variables column: the variable's name.
|
||||
Name,
|
||||
/// The Objects column: the name of the object the variable is bound to.
|
||||
Object,
|
||||
/// The type column: text, image, graph, visibility, then variables of unknown kind.
|
||||
Type,
|
||||
}
|
||||
|
||||
impl VariableSortKey {
|
||||
/// The values `variable.sort` takes for `by`, in the order of [`Self::id`].
|
||||
const IDS: [&'static str; 4] = ["name", "object", "type", "none"];
|
||||
|
||||
/// The value `variable.sort` takes for `by` and `variable.list` reports.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Name => "name",
|
||||
Self::Object => "object",
|
||||
Self::Type => "type",
|
||||
Self::Defined => "none",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"name" => Some(Self::Name),
|
||||
"object" => Some(Self::Object),
|
||||
"type" => Some(Self::Type),
|
||||
"none" => Some(Self::Defined),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How the Variables panel orders its list: a column, and whether the order is reversed. Panel
|
||||
/// state, not saved and not an undo step.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct VariableSort {
|
||||
pub by: VariableSortKey,
|
||||
pub descending: bool,
|
||||
}
|
||||
|
||||
/// The place of a variable's kind in the order the type column sorts by.
|
||||
fn kind_rank(kind: VariableKind) -> u8 {
|
||||
match kind {
|
||||
VariableKind::Text => 0,
|
||||
VariableKind::Image => 1,
|
||||
VariableKind::Graph => 2,
|
||||
VariableKind::Visibility => 3,
|
||||
VariableKind::Unknown => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Variables panel's rows in its sort order, as indices into the document's variables.
|
||||
/// Names compare without regard to case. Sorted by object, a variable bound to nothing comes
|
||||
/// first and one bound to several objects comes after those bound to one; `xml_id` names objects
|
||||
/// by their XML ids, as the panel does with Identify Objects By set to XML ID. Ties go by name.
|
||||
pub fn variable_order(st: &crate::DocState, xml_id: bool) -> Vec<usize> {
|
||||
let vars = &st.doc.variables.variables;
|
||||
let sort = st.variables_sort;
|
||||
let mut order: Vec<usize> = (0..vars.len()).collect();
|
||||
if sort.by != VariableSortKey::Defined {
|
||||
order.sort_by_cached_key(|&i| {
|
||||
let v = &vars[i];
|
||||
let (rank, label) = match sort.by {
|
||||
VariableSortKey::Object => match st.doc.variables.objects_of(&v.name).as_slice() {
|
||||
[] => (0, String::new()),
|
||||
[one] => (1, st.doc.node(*one).map(|n| n.identify_label(xml_id)).unwrap_or_default()),
|
||||
_ => (2, String::new()),
|
||||
},
|
||||
VariableSortKey::Type => (kind_rank(v.kind), String::new()),
|
||||
VariableSortKey::Name | VariableSortKey::Defined => (0, String::new()),
|
||||
};
|
||||
(rank, label.to_lowercase(), label, v.name.to_lowercase(), v.name.clone())
|
||||
});
|
||||
}
|
||||
if sort.descending {
|
||||
order.reverse();
|
||||
}
|
||||
order
|
||||
}
|
||||
|
||||
/// Sort the Variables panel's list. Panel state, like `variable.highlight`: not saved and not an
|
||||
/// undo step. Setting the order the panel already has changes nothing.
|
||||
fn sort(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "variable.sort";
|
||||
let key = "by";
|
||||
let by = match p.get(key) {
|
||||
None => return Err(bad(C, format!("missing `{key}`"))),
|
||||
Some(v) => v
|
||||
.as_str()
|
||||
.and_then(VariableSortKey::parse)
|
||||
.ok_or_else(|| bad(C, format!("`{key}` must be one of {}", VariableSortKey::IDS.join(", "))))?,
|
||||
};
|
||||
// A `descending` that is given must be true or false: null is refused, not read as omitted.
|
||||
let key = "descending";
|
||||
let descending = match p.get(key) {
|
||||
None => false,
|
||||
Some(v) => v.as_bool().ok_or_else(|| bad(C, format!("`{key}` must be true or false")))?,
|
||||
};
|
||||
let sort = VariableSort { by, descending };
|
||||
let st = s.doc_mut()?;
|
||||
if st.variables_sort != sort {
|
||||
st.variables_sort = sort;
|
||||
st.revision += 1;
|
||||
}
|
||||
Ok(json!({"by": by.id(), "descending": descending}))
|
||||
}
|
||||
|
||||
fn dataset_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "dataset.new";
|
||||
let name = var_name(p, C)?;
|
||||
@@ -726,7 +841,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
variables_unlocked,
|
||||
rename
|
||||
),
|
||||
cmd!(query "variable.list", "List Variables", [], None, "{} → variables with kinds and binding counts, and whether they are locked → {variables, locked}", has_doc, list),
|
||||
cmd!(
|
||||
query "variable.list",
|
||||
"List Variables",
|
||||
[],
|
||||
None,
|
||||
"{} → variables with kinds and binding counts in the Variables panel's order, whether they are locked, and the panel's sort (as variable.sort sets it) → {variables, locked, sort: {by, descending}}",
|
||||
has_doc,
|
||||
list
|
||||
),
|
||||
cmd!(
|
||||
"variable.makeTextDynamic",
|
||||
"Make Text Dynamic",
|
||||
@@ -799,6 +922,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
highlight
|
||||
),
|
||||
cmd!(
|
||||
"variable.sort",
|
||||
"Sort Variables",
|
||||
[],
|
||||
None,
|
||||
"{by: name|object|type|none, descending?: bool} sort the Variables panel's list (a click on a column header) by variable name, by the name of the bound object, or by type (text, image, graph, visibility, then unknown); `none` is the order the variables were defined in, and `descending` reverses the order. Names compare without regard to case; by object, unbound variables come first and variables bound to several objects last, and ties go by name. Panel state: not saved and not an undo step; variable.list reports it → {by, descending}",
|
||||
has_doc,
|
||||
sort
|
||||
),
|
||||
cmd!("dataset.new", "New Data Set…", ["Window", "Variables"], None, "{name, values?: {variable: value}} add a dataset", has_doc, dataset_new),
|
||||
cmd!(
|
||||
"dataset.set",
|
||||
|
||||
@@ -29,6 +29,9 @@ pub struct DocState {
|
||||
/// Bound Object act on. Panel state, not art selection: not saved, not undoable
|
||||
/// (`variable.highlight`). A name, so renaming a variable moves the highlight with it.
|
||||
pub variables_highlight: Option<String>,
|
||||
/// The order of the Variables panel's list: the column it is sorted by, and whether it is
|
||||
/// reversed. Panel state: not saved, not undoable (`variable.sort`).
|
||||
pub variables_sort: cmd::VariableSort,
|
||||
/// Isolation mode container.
|
||||
pub isolation: Option<NodeId>,
|
||||
/// Select › Start Global Edit: the objects edited together until Stop Global Edit or Escape
|
||||
@@ -316,6 +319,7 @@ impl DocState {
|
||||
active_layer,
|
||||
layer_rows: vec![],
|
||||
variables_highlight: None,
|
||||
variables_sort: Default::default(),
|
||||
isolation: None,
|
||||
global_edit: None,
|
||||
similar_outlines: Default::default(),
|
||||
|
||||
@@ -29,6 +29,7 @@ use vectorcraft_tools::{PaintDefaults, Tool};
|
||||
pub use cmd::clipboard::{Clipboard, SourceLayer};
|
||||
pub use cmd::create::constrain_click;
|
||||
pub use cmd::distortcmds::perspective_click;
|
||||
pub use cmd::graph::{GraphTypeSubject, calculated_axes as graph_calculated_axes, graph_type_subject};
|
||||
pub use cmd::params::{check_keys, check_params};
|
||||
pub use cmd::pathops::auto_simplify_level;
|
||||
pub use cmd::rasterfx::{export_pdf, flatten_raster_effects};
|
||||
|
||||
@@ -84,6 +84,63 @@ fn release_and_ungroup_keep_the_clipping_paths_paint() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixels of `doc` rendered on white that are mostly blue and mostly green.
|
||||
fn blue_and_green(doc: &Document) -> (usize, usize) {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let r = Renderer::new().render(doc, 200, 200, Affine::IDENTITY, &opts);
|
||||
let (mut blue, mut green) = (0, 0);
|
||||
for y in 0..200 {
|
||||
for x in 0..200 {
|
||||
let p = r.pixel(x, y);
|
||||
blue += usize::from(p[2] > 200 && p[0] < 60 && p[1] < 60);
|
||||
green += usize::from(p[1] > 200 && p[0] < 60 && p[2] < 60);
|
||||
}
|
||||
}
|
||||
(blue, green)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn painted_type_clipping_inside_a_group_with_geometry_effects_keeps_its_paint() {
|
||||
// The canvas and the SVG and PDF output paint the type's fill and stroke as they did before
|
||||
// the effect. Type that stays type beside the clip group takes the second Transform case
|
||||
// through baking that keeps type.
|
||||
let cases =
|
||||
[("distort.transform", json!({"moveH": 5}), false), ("distort.transform", json!({"moveH": 5}), true), ("distort.roughen", json!({}), false)];
|
||||
for (effect, params, other_type) in cases {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
rect(&mut s, 0.0, 30.0, "#ff0000");
|
||||
let t = NodeId(s.execute("text.create", &json!({"x": 10, "y": 120, "text": "Clip", "size": 72})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
let clip = NodeId(s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("paint.setFill", &json!({"color": "#0000ff", "ids": [t.0]})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"color": "#00ff00", "ids": [t.0]})).unwrap();
|
||||
s.execute("stroke.set", &json!({"weight": 2, "ids": [t.0]})).unwrap();
|
||||
let before = blue_and_green(&s.doc().unwrap().doc);
|
||||
let mut ids = vec![clip.0];
|
||||
if other_type {
|
||||
ids.push(s.execute("text.create", &json!({"x": 10, "y": 190, "text": "x", "size": 10})).unwrap()["id"].as_u64().unwrap());
|
||||
}
|
||||
let group = NodeId(s.execute("object.group", &json!({"ids": ids})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("effect.apply", &json!({"effect": effect, "params": params, "ids": [group.0], "item": null})).unwrap();
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let (blue, green) = blue_and_green(&doc);
|
||||
assert!(
|
||||
blue * 2 > before.0 && green * 2 > before.1,
|
||||
"{effect} {other_type}: the canvas paints the type's fill and stroke: {blue} {green}, {before:?} before"
|
||||
);
|
||||
let svg = vectorcraft_svg::export(&doc, &Default::default());
|
||||
let pdf = vectorcraft_pdf::export(&doc, &Default::default()).unwrap();
|
||||
for (what, out) in [("SVG", vectorcraft_svg::import(&svg).unwrap()), ("PDF", vectorcraft_pdf::import(&pdf).unwrap())] {
|
||||
let (b, g) = blue_and_green(&out);
|
||||
assert!(
|
||||
b.abs_diff(blue) * 10 < blue && g.abs_diff(green) * 5 < green,
|
||||
"{effect} {other_type}, {what}: {b} {g} against the canvas's {blue} {green}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_as_a_clipping_path_paints_its_characters() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -303,6 +303,62 @@ fn methods_and_bad_params() {
|
||||
assert!(s.execute("recolor.apply", &json!({"map": {}})).is_err(), "needs art or a group");
|
||||
}
|
||||
|
||||
/// A row with no current colors (Recolor Artwork's New Row) recolors nothing, and records no
|
||||
/// undo step on its own; beside it the other rows recolor as before, and the weights and
|
||||
/// randomize keep it. A row without `from`, or with a bad current color, is still refused.
|
||||
#[test]
|
||||
fn an_empty_row_recolors_nothing() {
|
||||
let mut s = session();
|
||||
let red = rect(&mut s, json!("#ff0000"));
|
||||
let blue = rect(&mut s, json!("#0000ff"));
|
||||
select_all(&mut s);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let empty = json!({"from": [], "to": "rgb 0 255 0"});
|
||||
let r = run(&mut s, "recolor.apply", json!({"map": [empty]}));
|
||||
assert_eq!(r["changed"], json!(0));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step");
|
||||
let map = json!([empty, {"from": ["rgb 0 0 255"], "to": "rgb 255 255 0"}]);
|
||||
let w = run(&mut s, "recolor.weigh", json!({"map": map}));
|
||||
assert_eq!(w["map"], map, "the rows as given");
|
||||
assert_eq!(w["shares"], json!([0.0, 100.0]));
|
||||
let shuffled = run(&mut s, "recolor.randomize", json!({"map": map, "seed": 1}));
|
||||
assert_eq!(shuffled["map"][0]["from"], json!([]));
|
||||
run(&mut s, "recolor.apply", json!({"map": map}));
|
||||
assert_eq!((fill(&s, red).to_hex(), fill(&s, blue).to_hex()), ("#ff0000".into(), "#ffff00".into()));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
for bad in [json!([{"to": "rgb 0 255 0"}]), json!([{"from": ["nonsense"], "to": "rgb 0 255 0"}])] {
|
||||
assert!(s.execute("recolor.apply", &json!({"map": bad})).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An empty row adds no library swatch with `limitTo`, and gives a rewritten group no color: the
|
||||
/// group keeps one color per row that holds current colors.
|
||||
#[test]
|
||||
fn an_empty_row_adds_no_swatch_and_no_group_color() {
|
||||
let mut s = session();
|
||||
rect(&mut s, json!("#ff0000"));
|
||||
rect(&mut s, json!("#0000ff"));
|
||||
select_all(&mut s);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let swatches = s.doc().unwrap().doc.swatches.len();
|
||||
let r = run(&mut s, "recolor.apply", json!({"map": [{"from": [], "to": "#ff0033"}], "limitTo": "web-safe-216"}));
|
||||
assert_eq!(r["swatches"], json!([]), "{r}");
|
||||
assert_eq!(s.doc().unwrap().doc.swatches.len(), swatches);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step");
|
||||
run(&mut s, "swatch.newGroup", json!({"name": "Pair", "colors": ["#ff0000", "#0000ff"]}));
|
||||
select_all(&mut s);
|
||||
let map = json!([
|
||||
{"from": [], "to": "rgb 0 255 0"},
|
||||
{"from": ["rgb 255 0 0"], "to": "rgb 255 0 0"},
|
||||
{"from": ["rgb 0 0 255"], "to": "rgb 0 0 255"},
|
||||
]);
|
||||
run(&mut s, "recolor.apply", json!({"map": map, "group": "Pair"}));
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let g = d.swatch_groups.iter().find(|g| g.name == "Pair").unwrap();
|
||||
let hexes: Vec<String> = g.swatches.iter().map(|w| w.paint.color().unwrap().to_hex()).collect();
|
||||
assert_eq!(hexes, ["#ff0000", "#0000ff"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn randomize_shuffles_new_colours_and_skips_excluded_rows() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -45,6 +45,7 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects()
|
||||
assert!(libs.iter().any(|l| l["id"] == "game-hud" && l["name"] == "Game HUD"), "{libs:?}");
|
||||
assert!(libs.iter().any(|l| l["id"] == "status-effects" && l["name"] == "Status Effects"), "{libs:?}");
|
||||
assert!(libs.iter().any(|l| l["id"] == "textures" && l["name"] == "Textures" && l["count"] == 8), "{libs:?}");
|
||||
assert!(libs.iter().any(|l| l["id"] == "neon-glow" && l["name"] == "Neon & Glow" && l["count"] == 6), "{libs:?}");
|
||||
for l in libs {
|
||||
let id = l["id"].as_str().unwrap();
|
||||
let g = run(&mut s, "graphicStyle.library", json!({"library": id}));
|
||||
@@ -69,6 +70,9 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects()
|
||||
let texture_names: Vec<&str> = textures["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect();
|
||||
let expected = ["Fine Grain", "Canvas Weave", "Burlap Sack", "Cracked Glaze", "Mosaic Floor", "Quilted Squares", "Leaded Glass", "Damp Paper"];
|
||||
assert_eq!(texture_names, expected);
|
||||
let neon = run(&mut s, "graphicStyle.library", json!({"library": "neon-glow"}));
|
||||
let neon_names: Vec<&str> = neon["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(neon_names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]);
|
||||
let id = rect(&mut s, 10.0, 20.0, 40.0, 30.0);
|
||||
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "game-hud", "name": "Mana Glow", "apply": true, "ids": [id.0]}));
|
||||
assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec!["Mana Glow"], Some("Mana Glow")));
|
||||
@@ -419,3 +423,53 @@ fn every_builtin_style_paints_visibly_on_a_rectangle() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_and_glow_library_adds_and_applies_multi_stroke_styles() {
|
||||
let mut s = session();
|
||||
let list = run(&mut s, "graphicStyle.libraries", json!({}));
|
||||
let libs = list["libraries"].as_array().unwrap();
|
||||
let at = libs.iter().position(|l| l["id"] == "neon-glow").expect("neon-glow listed");
|
||||
assert_eq!(libs[at]["name"], "Neon & Glow");
|
||||
assert_eq!(libs[at]["count"], 6);
|
||||
|
||||
let (_, lib) = cmd::stylelib::library(&s, "neon-glow").unwrap();
|
||||
assert_eq!(lib.len(), 6);
|
||||
|
||||
let want = [
|
||||
("Cyan Neon", 2, "stylize.dropShadow"),
|
||||
("Magenta Laser", 2, "stylize.outerGlow"),
|
||||
("Amber Tube", 2, "stylize.outerGlow"),
|
||||
("Electric Lime", 2, "stylize.outerGlow"),
|
||||
("Pulse Violet", 2, "stylize.innerGlow"),
|
||||
("Cobalt Phosphor", 2, "blur.gaussian"),
|
||||
];
|
||||
|
||||
for (i, (name, stroke_count, first_effect)) in want.iter().enumerate() {
|
||||
let id = rect(&mut s, 10.0 + 30.0 * i as f64, 15.0, 25.0, 25.0);
|
||||
let before_undo = undo_len(&s);
|
||||
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": name, "apply": true, "ids": [id.0]}));
|
||||
assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec![*name], Some(*name)));
|
||||
assert_eq!(undo_len(&s), before_undo + 1, "{name}");
|
||||
|
||||
let node = doc(&s).node(id).unwrap();
|
||||
let strokes = node.appearance.items.iter().filter(|it| matches!(it, vectorcraft_doc::AppearanceItem::Stroke(_))).count();
|
||||
assert_eq!(strokes, *stroke_count, "{name}");
|
||||
assert_eq!(node.appearance.effects[0].id, *first_effect, "{name}");
|
||||
}
|
||||
|
||||
// Adding existing style dedupes and creates no new undo step.
|
||||
let before = (undo_len(&s), doc(&s).graphic_styles.len());
|
||||
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": "Cyan Neon"}));
|
||||
assert_eq!((names(&r["added"]), names(&r["existing"])), (vec![], vec!["Cyan Neon"]));
|
||||
assert_eq!((undo_len(&s), doc(&s).graphic_styles.len()), before);
|
||||
|
||||
// Round-trip through serialize / open
|
||||
let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string();
|
||||
run(&mut s, "document.open", json!({"name": "neon.vectorcraft", "dataBase64": b64}));
|
||||
for (name, _, _) in want {
|
||||
let d = doc(&s);
|
||||
let st = d.graphic_style(name).unwrap();
|
||||
assert!(st.same_look(lib.style(name).unwrap()), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3382,3 +3382,93 @@ fn an_svg_without_readable_variable_data_opens_its_art() {
|
||||
assert!(d.variables.variables.is_empty());
|
||||
assert_eq!(vectorcraft_testkit::svg_shapes(&vectorcraft_svg::export(d, &Default::default())), 1, "the art opens");
|
||||
}
|
||||
|
||||
/// Five variables of three kinds, defined out of every order: `beta` (text, on type named Mike),
|
||||
/// `Alpha` (visibility, on a rectangle named apple), `gamma` (image, bound to nothing), `Delta`
|
||||
/// (visibility, on two rectangles) and `Echo` (text, on type named Bravo).
|
||||
fn sortable_variables() -> Session {
|
||||
let mut s = session();
|
||||
let mike = text(&mut s, "m");
|
||||
let bravo = text(&mut s, "b");
|
||||
let apple = rect(&mut s);
|
||||
let (r1, r2) = (rect(&mut s), rect(&mut s));
|
||||
for (id, name) in [(mike, "Mike"), (bravo, "Bravo"), (apple, "apple")] {
|
||||
s.execute("layer.setProps", &json!({"id": id.0, "name": name})).unwrap();
|
||||
}
|
||||
for (name, kind, ids) in [
|
||||
("beta", "text", vec![mike]),
|
||||
("Alpha", "visibility", vec![apple]),
|
||||
("gamma", "image", vec![]),
|
||||
("Delta", "visibility", vec![r1, r2]),
|
||||
("Echo", "text", vec![bravo]),
|
||||
] {
|
||||
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
|
||||
if !ids.is_empty() {
|
||||
s.execute("variable.bind", &json!({"variable": name, "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn listed_names(s: &mut Session) -> Vec<String> {
|
||||
let list = s.execute("variable.list", &json!({})).unwrap();
|
||||
list["variables"].as_array().unwrap().iter().map(|v| v["name"].as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
/// `variable.sort` orders the Variables panel's list, which `variable.list` follows, by each
|
||||
/// column: names without regard to case; by object, unbound first and several objects last; by
|
||||
/// type, text, image, graph, visibility. `descending` reverses each order, and `none` goes back
|
||||
/// to the order the variables were defined in.
|
||||
#[test]
|
||||
fn variable_sort_orders_the_list_by_each_column() {
|
||||
let mut s = sortable_variables();
|
||||
let defined = ["beta", "Alpha", "gamma", "Delta", "Echo"];
|
||||
assert_eq!(listed_names(&mut s), defined);
|
||||
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["sort"], json!({"by": "none", "descending": false}));
|
||||
for (by, order) in [
|
||||
("name", ["Alpha", "beta", "Delta", "Echo", "gamma"]),
|
||||
("object", ["gamma", "Alpha", "Echo", "beta", "Delta"]),
|
||||
("type", ["beta", "Echo", "gamma", "Alpha", "Delta"]),
|
||||
("none", defined),
|
||||
] {
|
||||
let r = s.execute("variable.sort", &json!({"by": by})).unwrap();
|
||||
assert_eq!(r, json!({"by": by, "descending": false}));
|
||||
assert_eq!(listed_names(&mut s), order, "{by}");
|
||||
s.execute("variable.sort", &json!({"by": by, "descending": true})).unwrap();
|
||||
let reversed: Vec<&str> = order.iter().rev().copied().collect();
|
||||
assert_eq!(listed_names(&mut s), reversed, "{by}, descending");
|
||||
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["sort"], json!({"by": by, "descending": true}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The sort is panel state: it is not an undo step and does not count as a change to save, and
|
||||
/// setting the order the list already has does not redraw anything. A bad `by` or `descending`
|
||||
/// is an error that leaves the order as it was.
|
||||
#[test]
|
||||
fn variable_sort_refuses_bad_params_and_is_not_an_undo_step() {
|
||||
let mut s = sortable_variables();
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
let dirty = s.doc().unwrap().is_dirty();
|
||||
s.execute("variable.sort", &json!({"by": "name", "descending": true})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps, "no undo step");
|
||||
assert_eq!(s.doc().unwrap().is_dirty(), dirty);
|
||||
let revision = s.doc().unwrap().revision;
|
||||
s.execute("variable.sort", &json!({"by": "name", "descending": true})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().revision, revision, "the same order changes nothing");
|
||||
|
||||
for (p, error) in [
|
||||
(json!({}), "missing `by`"),
|
||||
(json!({"by": null}), "`by` must be one of name, object, type, none"),
|
||||
(json!({"by": "name", "descending": null}), "`descending` must be true or false"),
|
||||
(json!({"by": "size"}), "`by` must be one of name, object, type, none"),
|
||||
(json!({"by": "Name"}), "`by` must be one of name, object, type, none"),
|
||||
(json!({"by": 1}), "`by` must be one of name, object, type, none"),
|
||||
(json!({"by": "type", "descending": "yes"}), "`descending` must be true or false"),
|
||||
] {
|
||||
let e = s.execute("variable.sort", &p).unwrap_err().to_string();
|
||||
assert!(e.contains(error), "{p}: {e}");
|
||||
}
|
||||
let sort = s.doc().unwrap().variables_sort;
|
||||
assert_eq!((sort.by, sort.descending), (crate::cmd::VariableSortKey::Name, true), "errors leave the order alone");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
|
||||
}
|
||||
|
||||
@@ -1103,6 +1103,12 @@ impl Session {
|
||||
self.pointer_held
|
||||
}
|
||||
|
||||
/// What the Info panel shows for the active tool as it is now
|
||||
/// ([`vectorcraft_tools::Tool::readout`]), in the view of the last pointer event.
|
||||
pub fn tool_readout(&mut self) -> Option<vectorcraft_tools::Readout> {
|
||||
self.with_tool_cx(self.last_view, |t, cx| t.readout(cx))
|
||||
}
|
||||
|
||||
/// Is the active tool moving, scaling or rotating the selection with a drag?
|
||||
pub fn tool_transforming(&self) -> bool {
|
||||
self.tool.transforming()
|
||||
|
||||
@@ -10455,7 +10455,8 @@ fn an_envelope_is_written_in_the_order_the_reader_reads_it() {
|
||||
}
|
||||
|
||||
/// Envelopes the reader would open in another shape, or not at all, are written as their distorted
|
||||
/// art with a warning, and open as that art: an envelope of type, one in a turned frame, a warp drawn
|
||||
/// art with a warning, and open as that art: an envelope of type with text wrap options, one of
|
||||
/// mirrored type (written as its outlines), one in a turned frame, a warp drawn
|
||||
/// without its style's net, a mesh the reader would lay out the other way round, a top object
|
||||
/// with a side of more segments than the grid has columns, a Fidelity that isn't a whole number and bounds margins kept with
|
||||
/// Distort Appearance off. Other live objects keep their own warning.
|
||||
@@ -10470,10 +10471,24 @@ fn envelopes_the_reader_cannot_open_live_are_written_as_their_art() {
|
||||
f(&mut doc.node_mut(id).unwrap().kind);
|
||||
doc
|
||||
};
|
||||
let typed = change(&|k| {
|
||||
let type_node = |id: u64| {
|
||||
let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 50.0), "Type", vectorcraft_doc::CharStyle::default());
|
||||
Node::new(vectorcraft_doc::NodeId(id), NodeKind::Text(Box::new(t)))
|
||||
};
|
||||
let wrapped_type = change(&|k| {
|
||||
if let NodeKind::Envelope { content, .. } = k {
|
||||
let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 50.0), "Type", vectorcraft_doc::CharStyle::default());
|
||||
content.push(Arc::new(Node::new(vectorcraft_doc::NodeId(9_000), NodeKind::Text(Box::new(t)))));
|
||||
let mut t = type_node(9_000);
|
||||
t.extras_mut().wrap = Some(vectorcraft_doc::TextWrap { offset: 6.0, invert: false });
|
||||
content.push(Arc::new(t));
|
||||
}
|
||||
});
|
||||
let mirrored_type = change(&|k| {
|
||||
if let NodeKind::Envelope { content, .. } = k {
|
||||
let mut t = type_node(9_001);
|
||||
if let NodeKind::Text(t) = &mut t.kind {
|
||||
t.xf = Affine::new([-1.0, 0.0, 0.0, 1.0, 50.0, 50.0]);
|
||||
}
|
||||
content.push(Arc::new(t));
|
||||
}
|
||||
});
|
||||
let turned = change(&|k| {
|
||||
@@ -10512,7 +10527,8 @@ fn envelopes_the_reader_cannot_open_live_are_written_as_their_art() {
|
||||
}
|
||||
});
|
||||
let cases = [
|
||||
("type", typed),
|
||||
("type with text wrap options", wrapped_type),
|
||||
("mirrored type, written as its outlines", mirrored_type),
|
||||
("turned", turned),
|
||||
("old warp", old_warp),
|
||||
("sideways mesh", sideways),
|
||||
@@ -10537,6 +10553,91 @@ fn transpose(points: Vec<kurbo::Point>, rows: usize, cols: usize) -> Vec<kurbo::
|
||||
(0..=cols).flat_map(|c| (0..=rows).map(move |r| (r, c))).map(|(r, c)| points[r * (cols + 1) + c]).collect()
|
||||
}
|
||||
|
||||
/// An envelope named `Bent` of `kind` ([`envelope_node`]) whose contents also hold point type
|
||||
/// "Type", 24 pt and blue, from (104, 62), with Edit Contents `editing`.
|
||||
fn typed_envelope_node(doc: &mut Document, kind: vectorcraft_doc::live::EnvelopeKind, editing: bool) -> Node {
|
||||
let mut envelope = envelope_node(doc, kind, 50.0, editing, vectorcraft_doc::live::EnvelopeOptions::NEW);
|
||||
let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(104.0, 62.0), "Type", style(24.0, Color::rgb(0.0, 0.0, 1.0)));
|
||||
let text = text_node(doc, t);
|
||||
let NodeKind::Envelope { content, .. } = &mut envelope.kind else { unreachable_envelope() };
|
||||
content.push(Arc::new(text));
|
||||
envelope
|
||||
}
|
||||
|
||||
/// A 200 × 100 document of `envelope` alone.
|
||||
fn alone(envelope: Node) -> Document {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
doc.insert(Some(layer), usize::MAX, envelope).unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
/// `doc` written, then read back through its editing copy beside a page that draws as `doc` does,
|
||||
/// its live objects as their art: the objects on its first layer, the writer's warnings and the
|
||||
/// reader's notes.
|
||||
fn read_with_page(doc: &Document) -> (Vec<Arc<Node>>, Vec<String>, Vec<String>) {
|
||||
let (p, warnings) = private_data(doc, &opts()).unwrap();
|
||||
let mut page = doc.clone();
|
||||
let ids: Vec<vectorcraft_doc::NodeId> = page.layers[0].children().into_iter().flatten().map(|n| n.id).collect();
|
||||
for id in ids {
|
||||
if let Some(n) = page.node_mut(id) {
|
||||
*n = vectorcraft_effects::expand_live_deep(Some(doc), n);
|
||||
}
|
||||
}
|
||||
let (read, notes, layers) = crate::layered_ai(&p.data, page, vec![], false, false, 1.0);
|
||||
assert!(layers, "{notes:?}");
|
||||
(read.layers[0].children().cloned().unwrap_or_default(), warnings, notes)
|
||||
}
|
||||
|
||||
/// The type in `nodes` and their groups, with whether it shows.
|
||||
fn type_in(nodes: &[Arc<Node>]) -> Vec<(String, bool)> {
|
||||
let mut out = vec![];
|
||||
for n in nodes {
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => out.push((t.plain_text(), n.visible)),
|
||||
NodeKind::Group { children, .. } => out.extend(type_in(children)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An envelope whose contents hold type keeps the type as type: a warp, a mesh and a top object
|
||||
/// envelope, with Edit Contents off and on, open as the same live envelope with the type shown
|
||||
/// and editable in its contents beside the rest of them, no copy of the type left on the layer,
|
||||
/// and again when saved as they opened.
|
||||
#[test]
|
||||
fn an_envelope_of_type_is_read_back_live_with_its_type() {
|
||||
for (name, kind) in live_envelope_kinds() {
|
||||
for editing in [false, true] {
|
||||
let what = format!("{name}, editing {editing}");
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let mut envelope = typed_envelope_node(&mut doc, kind.clone(), editing);
|
||||
for round in ["saved", "saved again"] {
|
||||
doc = alone(envelope);
|
||||
let (top, warnings, notes) = read_with_page(&doc);
|
||||
assert!(!warnings.iter().any(|w| w == writer::ENVELOPES || w == writer::TYPE), "{what}, {round}: {warnings:?}");
|
||||
assert!(
|
||||
!notes.iter().any(|n| n == crate::import::ENVELOPE_TYPE_DRAWN || n == crate::import::ENVELOPE_DRAWN),
|
||||
"{what}, {round}: {notes:?}"
|
||||
);
|
||||
let [back] = top.as_slice() else { panic!("{what}, {round}: {top:#?} {notes:?}") };
|
||||
let NodeKind::Envelope { content, kind: got, editing: e, fidelity, .. } = &back.kind else {
|
||||
panic!("{what}, {round}: {back:#?} {notes:?}")
|
||||
};
|
||||
assert_eq!((*e, *fidelity), (editing, 50.0), "{what}, {round}");
|
||||
assert!(same_envelope_kind(&kind, got), "{what}, {round}: {got:?}");
|
||||
assert_eq!(back.name.as_deref(), Some("Bent"), "{what}, {round}");
|
||||
assert_eq!(type_in(content), [("Type".to_string(), true)], "{what}, {round}");
|
||||
let colors: Vec<Option<Color>> =
|
||||
content.iter().filter(|c| !matches!(c.kind, NodeKind::Text(_))).map(|c| c.appearance.fill_paint().color()).collect();
|
||||
assert_eq!(colors, [Some(Color::rgb(1.0, 0.0, 0.0)), Some(Color::rgb(0.0, 0.0, 1.0))], "{what}, {round}");
|
||||
envelope = (**back).clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A top object over (30, 10)-(170, 90) whose top side is `top` straight segments, falling and
|
||||
/// rising by 10 pt between its anchors, and whose other sides are one straight segment each.
|
||||
fn uneven_top_object(top: usize) -> kurbo::BezPath {
|
||||
@@ -10610,6 +10711,86 @@ fn a_top_object_with_uneven_sides_is_read_back_live() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An envelope of type is written live with room for every object the reader reads of it, the
|
||||
/// copy of its type it reads in the envelope's dictionary included. Written as its art, it takes
|
||||
/// its type's story out of the text document with it: where its record doesn't fit within the
|
||||
/// object limit, and where other type in its contents is written as its outlines (mirrored type).
|
||||
/// The file then has no text document.
|
||||
#[test]
|
||||
fn an_envelope_of_type_written_as_its_art_leaves_no_story() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
let kind = EnvelopeKind::Mesh { rows: 2, cols: 3, points: envelope_points(2, 3), handles: vec![] };
|
||||
for editing in [false, true] {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let envelope = typed_envelope_node(&mut doc, kind.clone(), editing);
|
||||
let doc = alone(envelope);
|
||||
let program = export_ai_editing(&doc, &opts()).unwrap().program;
|
||||
let all = String::from_utf8_lossy(&program).replace("\r\n", "\n");
|
||||
assert!(all.contains("AIDeformOptionsKey") && all.contains("%AI11_BeginTextDocument"), "editing {editing}: {all}");
|
||||
let read = crate::import::objects_read(&program).unwrap();
|
||||
let out = export_within(&doc, &opts(), read).unwrap();
|
||||
assert!(!out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT), "editing {editing}: {:?}", out.warnings);
|
||||
let out = export_within(&doc, &opts(), read - 1).unwrap();
|
||||
let short = String::from_utf8_lossy(&out.program).replace("\r\n", "\n");
|
||||
assert!(out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT), "editing {editing}: {:?}", out.warnings);
|
||||
assert!(!short.contains("AIDeformOptionsKey") && !short.contains("%AI11_BeginTextDocument"), "editing {editing}: {short}");
|
||||
}
|
||||
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let mut envelope = typed_envelope_node(&mut doc, kind, false);
|
||||
let mut t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 40.0), "Back", style(12.0, Color::BLACK));
|
||||
t.xf = Affine::new([-1.0, 0.0, 0.0, 1.0, 50.0, 40.0]);
|
||||
let mirrored = text_node(&mut doc, t);
|
||||
let NodeKind::Envelope { content, .. } = &mut envelope.kind else { unreachable_envelope() };
|
||||
content.push(Arc::new(mirrored));
|
||||
let out = export_ai_editing(&alone(envelope), &opts()).unwrap();
|
||||
let text = String::from_utf8_lossy(&out.program).replace("\r\n", "\n");
|
||||
assert!(out.warnings.iter().any(|w| w == writer::ENVELOPES) && !out.warnings.iter().any(|w| w == writer::TYPE), "{:?}", out.warnings);
|
||||
assert!(!text.contains("AIDeformOptionsKey") && !text.contains("%AI11_BeginTextDocument"), "{text}");
|
||||
}
|
||||
|
||||
/// Type in an envelope that is a symbol's art, or a frame of a thread, keeps the envelope written
|
||||
/// as its art: the reader would open its type as drawn art.
|
||||
#[test]
|
||||
fn an_envelope_of_type_in_a_symbol_or_a_thread_is_written_as_its_art() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
use vectorcraft_doc::{TextKind, TextObject};
|
||||
let kind = EnvelopeKind::Mesh { rows: 2, cols: 3, points: envelope_points(2, 3), handles: vec![] };
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let art = typed_envelope_node(&mut doc, kind.clone(), false);
|
||||
doc.symbols.push(Symbol { dynamic: false, name: "Bent".into(), art: Arc::new(art) });
|
||||
let placed = instance(&mut doc, "Bent", Affine::translate((10.0, 5.0)));
|
||||
let layer = doc.layers[0].id;
|
||||
doc.insert(Some(layer), usize::MAX, placed).unwrap();
|
||||
let (p, warnings) = private_data(&doc, &opts()).unwrap();
|
||||
assert!(warnings.iter().any(|w| w == writer::ENVELOPES), "{warnings:?}");
|
||||
let (back, notes) = crate::ai_alone(&p.data).unwrap();
|
||||
let symbol = back.symbols.iter().find(|s| s.name == "Bent").unwrap_or_else(|| panic!("{:?} {notes:?}", back.symbols));
|
||||
let mut live = 0;
|
||||
symbol.art.walk(&mut |n| live += usize::from(matches!(n.kind, NodeKind::Envelope { .. })));
|
||||
assert_eq!(live, 0, "{notes:?}");
|
||||
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let mut envelope = envelope_node(&mut doc, kind, 50.0, false, vectorcraft_doc::live::EnvelopeOptions::NEW);
|
||||
let area = |r: Rect, s: &str| {
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, s, style(10.0, Color::BLACK));
|
||||
(t.kind, t.xf) = (TextKind::Area { frame: shapes::rectangle(r) }, Affine::IDENTITY);
|
||||
t
|
||||
};
|
||||
let first = text_node(&mut doc, area(Rect::new(50.0, 30.0, 150.0, 70.0), "One two three four"));
|
||||
let second = text_node(&mut doc, area(Rect::new(10.0, 85.0, 190.0, 95.0), ""));
|
||||
doc.text_threads.push(vec![first.id, second.id]);
|
||||
let NodeKind::Envelope { content, .. } = &mut envelope.kind else { unreachable_envelope() };
|
||||
content.push(Arc::new(first));
|
||||
let layer = doc.layers[0].id;
|
||||
doc.insert(Some(layer), usize::MAX, envelope).unwrap();
|
||||
doc.insert(Some(layer), usize::MAX, second).unwrap();
|
||||
let text = program(&doc);
|
||||
assert!(!text.contains("AIDeformOptionsKey"), "{text}");
|
||||
let warnings = export_ai_editing(&doc, &opts()).unwrap().warnings;
|
||||
assert!(warnings.iter().any(|w| w == writer::ENVELOPES), "{warnings:?}");
|
||||
}
|
||||
|
||||
/// A top object with a top side of 50 segments over a bottom side of one is written live with a grid
|
||||
/// of 50 columns, the most the reader reads.
|
||||
#[test]
|
||||
|
||||
@@ -204,6 +204,12 @@ pub(super) struct Writer<'a> {
|
||||
/// The `hidden` and `nest` values at which the objects of the opacity mask's art being written
|
||||
/// are written: objects there may be written with their art styles.
|
||||
mask_level: Option<(u32, u32)>,
|
||||
/// The `hidden` and `nest` values at which the objects of the envelope contents being written
|
||||
/// are written ([`Self::envelope`]): type there may be written as type. With them, whether the
|
||||
/// contents are in the envelope's dictionary, where the reader reads a copy of each text object.
|
||||
contents_level: Option<(u32, u32, bool)>,
|
||||
/// How many text objects were written as their outlines ([`Self::text`]).
|
||||
outlined: usize,
|
||||
/// The image just written was written as a placed file.
|
||||
placed: bool,
|
||||
/// The image being written is the copy of a styled object on the layers, on `%_` lines: linked,
|
||||
@@ -322,6 +328,8 @@ impl<'a> Writer<'a> {
|
||||
payloads: vec![],
|
||||
hidden: 0,
|
||||
mask_level: None,
|
||||
contents_level: None,
|
||||
outlined: 0,
|
||||
placed: false,
|
||||
styled_copy: false,
|
||||
typed: false,
|
||||
@@ -1414,9 +1422,10 @@ impl<'a> Writer<'a> {
|
||||
// ---------- type ----------
|
||||
|
||||
/// Type `n`: a text object stub naming its story in the text document ([`texts`]) when the
|
||||
/// text document can hold it, outside opacity masks, pattern tiles, symbols' art and art cut to
|
||||
/// a clipping object; else its outlines ([`Self::outlined_text`]). A frame of threaded area type
|
||||
/// names the story of its thread, which its first frame holds.
|
||||
/// text document can hold it, on the layers or in an envelope's contents, outside opacity
|
||||
/// masks, pattern tiles, symbols' art and art cut to a clipping object; else its outlines
|
||||
/// ([`Self::outlined_text`]). A frame of threaded area type names the story of its thread,
|
||||
/// which its first frame holds.
|
||||
fn text(&mut self, n: &Node, xy: Xy) {
|
||||
let NodeKind::Text(t) = &n.kind else { return };
|
||||
// Dynamic Text is written as the sizes its layout draws the type at.
|
||||
@@ -1424,13 +1433,16 @@ impl<'a> Writer<'a> {
|
||||
let t = fitted.as_deref().unwrap_or(&**t);
|
||||
let doc = self.doc;
|
||||
let thread = doc.text_threads.iter().find(|th| th.contains(&n.id));
|
||||
let contents = self.contents_level.filter(|(hidden, nest, _)| (*hidden, *nest) == (self.hidden, self.nest));
|
||||
// Type that can't be a stub is written as its outlines, which count their own objects.
|
||||
if self.hidden > 0 || self.clip.is_some() || self.in_symbol || (thread.is_none() && !texts::holds(n, t)) {
|
||||
if (self.hidden > 0 && contents.is_none()) || self.clip.is_some() || self.in_symbol || (thread.is_none() && !texts::holds(n, t)) {
|
||||
self.outlined += 1;
|
||||
return self.outlined_text(n, xy);
|
||||
}
|
||||
// The stub's objects ([`texts::stub_objects`], [`Self::take_objects`]); without room for
|
||||
// them, the type is left out before its story goes into the text document.
|
||||
let stub = texts::stub_objects(t);
|
||||
// The stub's objects ([`texts::stub_objects`], [`Self::take_objects`]), and the copy the
|
||||
// reader reads of it in an envelope's dictionary; without room for them, the type is left
|
||||
// out before its story goes into the text document.
|
||||
let stub = texts::stub_objects(t) + usize::from(contents.is_some_and(|(.., copied)| copied));
|
||||
if !self.take_objects(stub) {
|
||||
return;
|
||||
}
|
||||
@@ -1444,6 +1456,7 @@ impl<'a> Writer<'a> {
|
||||
let Some(((story, frame), lost)) = placed else {
|
||||
// The text document can't hold it: the outlines count their own objects.
|
||||
self.objects -= stub;
|
||||
self.outlined += 1;
|
||||
return self.outlined_text(n, xy);
|
||||
};
|
||||
self.typed = true;
|
||||
|
||||
@@ -20,14 +20,14 @@ use vectorcraft_doc::live::{
|
||||
Coons, EnvelopeKind, EnvelopeOptions, GradientMesh, H_DOWN, H_LEFT, H_RIGHT, H_UP, MeshPoint, PreserveShape, WARP_NET, envelope_content_boxes,
|
||||
envelope_grid, envelope_stroke_margin, warp_grid,
|
||||
};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
use vectorcraft_geom::{Affine, PathData};
|
||||
|
||||
use super::{MAX_NEST, Writer, Xy, hide};
|
||||
use crate::import::{DEFORM_PLUGIN, EnvelopeGridPoint as GridPoint, envelope_outline, envelope_upright, warp_number};
|
||||
use crate::ps;
|
||||
|
||||
pub(in crate::ai_editing) const ENVELOPES: &str = "envelopes of type, envelopes without contents, envelopes in opacity masks, pattern tiles, art cut to a clipping object or other envelopes' contents, envelopes on the layers of a large canvas or in a turned or mirrored frame, warps drawn without their style's net, meshes and top objects that the data would open in another shape, envelopes with a Fidelity that isn't a whole number, envelopes that map from a box other than their contents' bounds with or without strokes or keep bounds margins with Distort Appearance off, envelopes after the first 256 envelopes and images with a Warp, and envelopes nested more than 8 deep are written as their art in the editing data for other apps";
|
||||
pub(in crate::ai_editing) const ENVELOPES: &str = "envelopes whose type is threaded, has text wrap options or is written as its outlines, envelopes without contents, envelopes in opacity masks, pattern tiles, art cut to a clipping object or other envelopes' contents, envelopes on the layers of a large canvas or in a turned or mirrored frame, warps drawn without their style's net, meshes and top objects that the data would open in another shape, envelopes with a Fidelity that isn't a whole number, envelopes that map from a box other than their contents' bounds with or without strokes or keep bounds margins with Distort Appearance off, envelopes after the first 256 envelopes and images with a Warp, and envelopes nested more than 8 deep are written as their art in the editing data for other apps";
|
||||
|
||||
/// The most rows or columns of an envelope's grid written (Make with Mesh's own limit, and the
|
||||
/// reader's).
|
||||
@@ -58,6 +58,15 @@ fn holds_type(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Text(_)) || n.children().is_some_and(|c| c.iter().any(|k| holds_type(k)))
|
||||
}
|
||||
|
||||
/// Is all the type in `n`, and in anything in it, type the reader can make an envelope's contents
|
||||
/// of: none of it is a frame of threaded type or has text wrap options.
|
||||
fn type_reads_back(n: &Node, doc: &Document) -> bool {
|
||||
match &n.kind {
|
||||
NodeKind::Text(_) => n.extras().wrap.is_none() && !doc.text_threads.iter().any(|th| th.contains(&n.id)),
|
||||
_ => n.children().is_none_or(|c| c.iter().all(|k| type_reads_back(k, doc))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The record of envelope `n` (its parts given), or `None` where the reader would open it in
|
||||
/// another shape than it has, or not as an envelope.
|
||||
fn record(
|
||||
@@ -73,7 +82,7 @@ fn record(
|
||||
// Distort Appearance on.
|
||||
let whole = (0.0..=100.0).contains(&fidelity) && fidelity.fract() == 0.0;
|
||||
let margin_kept = !options.distort_appearance && options.bounds_margin != [0.0; 4];
|
||||
if content.is_empty() || frame != Affine::IDENTITY || !whole || margin_kept || content.iter().any(|c| holds_type(c)) {
|
||||
if content.is_empty() || frame != Affine::IDENTITY || !whole || margin_kept {
|
||||
return None;
|
||||
}
|
||||
let mut keys = String::new();
|
||||
@@ -347,17 +356,24 @@ fn top_object_grid(path: &PathData, corners: Option<[u32; 4]>) -> Option<(usize,
|
||||
|
||||
impl Writer<'_> {
|
||||
/// Envelope `n`, of transparency `xy`, as a live envelope (see the module's form), outside opacity
|
||||
/// masks' art, pattern tiles, art cut to a clipping object and the layers of a large canvas. False,
|
||||
/// writing nothing, where it is written as its art instead: where the reader would open it in
|
||||
/// another shape, or some of it is left out for the object limit ([`Self::take_objects`]).
|
||||
/// masks' art, pattern tiles, art cut to a clipping object and the layers of a large canvas. Type
|
||||
/// in its contents is written as type, which the reader makes the envelope's contents again.
|
||||
/// False, writing nothing, where it is written as its art instead: where the reader would open it
|
||||
/// in another shape, or with its type as drawn art (threaded type, type with text wrap options,
|
||||
/// type written as its outlines), or some of it is left out for the object limit
|
||||
/// ([`Self::take_objects`]).
|
||||
pub(super) fn envelope(&mut self, n: &Node, xy: Xy) -> bool {
|
||||
let NodeKind::Envelope { content, kind, fidelity, editing, options, frame } = &n.kind else { return false };
|
||||
if self.hidden > 0 || self.clip.is_some() || self.nest >= MAX_NEST || !self.symbols_live || self.checks_left == 0 {
|
||||
return false;
|
||||
}
|
||||
if content.iter().any(|c| holds_type(c)) && !content.iter().all(|c| type_reads_back(c, self.doc)) {
|
||||
return false;
|
||||
}
|
||||
let Some(record) = record(n, content, kind, *fidelity, *editing, options, **frame) else { return false };
|
||||
let start = self.body.len();
|
||||
let (state, gs, charges, bound) = (self.state, self.gs, self.charges(), self.bound.len());
|
||||
let (outlined, texts, warnings) = (self.outlined, self.texts.written(), self.warnings.len());
|
||||
self.nest += 1;
|
||||
self.hidden += 1;
|
||||
// The group before the dictionary: the grid, or the contents while they are edited.
|
||||
@@ -365,7 +381,7 @@ impl Writer<'_> {
|
||||
let opened = self.open_group("u");
|
||||
if opened {
|
||||
if *editing {
|
||||
self.contents(content);
|
||||
self.contents(content, false);
|
||||
} else {
|
||||
self.grid(record.grid.as_ref());
|
||||
}
|
||||
@@ -383,23 +399,31 @@ impl Writer<'_> {
|
||||
if *editing {
|
||||
self.grid(record.grid.as_ref());
|
||||
} else if self.open_group("u") {
|
||||
self.contents(content);
|
||||
self.contents(content, true);
|
||||
self.end_group("U", Xy::PLAIN);
|
||||
}
|
||||
let block = std::mem::replace(&mut self.body, outer);
|
||||
hide(&block, &mut self.body);
|
||||
(self.state, self.gs, self.block_end) = (at_block, None, usize::MAX);
|
||||
let _ = writeln!(self.body, "%_X+\n%_ (AIDeformArtKey) ,\n%_{} /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW", record.style);
|
||||
// Type in the contents written as its outlines would open as drawn art, not as type.
|
||||
let outlines = self.outlined != outlined;
|
||||
self.hidden -= 1;
|
||||
self.nest -= 1;
|
||||
// The distorted art, which takes the envelope's lock, visibility, transparency, name and
|
||||
// opacity mask.
|
||||
self.state(n.locked, !n.visible);
|
||||
self.drawn_art(n, xy, None);
|
||||
if !opened || self.left_out_since(charges) {
|
||||
if !opened || outlines || self.left_out_since(charges) {
|
||||
let lines = self.body.split_off(start);
|
||||
self.give_back(charges, &lines);
|
||||
self.bound.truncate(bound);
|
||||
// The stories of the type in the lines taken out go with them.
|
||||
self.texts.take_back(texts);
|
||||
// The warnings of art written as its art again are that art's.
|
||||
if !self.left_out_since(charges) {
|
||||
self.warnings.truncate(warnings);
|
||||
}
|
||||
(self.state, self.gs, self.block_end) = (state, gs, usize::MAX);
|
||||
return false;
|
||||
}
|
||||
@@ -408,11 +432,14 @@ impl Writer<'_> {
|
||||
true
|
||||
}
|
||||
|
||||
/// An envelope's contents.
|
||||
fn contents(&mut self, content: &[Arc<Node>]) {
|
||||
/// An envelope's contents, where type may be written as type ([`Self::text`]): `copied` when
|
||||
/// they are in its dictionary, where the reader reads a copy of each text object.
|
||||
fn contents(&mut self, content: &[Arc<Node>], copied: bool) {
|
||||
let level = self.contents_level.replace((self.hidden, self.nest, copied));
|
||||
for c in content {
|
||||
self.node(c);
|
||||
}
|
||||
self.contents_level = level;
|
||||
}
|
||||
|
||||
/// An envelope's grid, when it has one, as a gray gradient mesh (`/Mesh X!` … `/End X!`): its
|
||||
|
||||
@@ -621,6 +621,15 @@ fn place_on(segments: &[[f64; 8]], f: f64) -> f64 {
|
||||
segments.len() as f64
|
||||
}
|
||||
|
||||
/// Where the text document ended at some point ([`TextDoc::written`]).
|
||||
#[derive(Clone, Copy)]
|
||||
pub(in crate::ai_editing) struct TextMark {
|
||||
at: (usize, usize, usize, usize, usize),
|
||||
values: usize,
|
||||
lost: bool,
|
||||
planned: usize,
|
||||
}
|
||||
|
||||
/// The text document being written.
|
||||
#[derive(Default)]
|
||||
pub(in crate::ai_editing) struct TextDoc {
|
||||
@@ -1023,6 +1032,24 @@ impl TextDoc {
|
||||
self.threaded.get(&id).copied()
|
||||
}
|
||||
|
||||
/// Where the text document written so far ends, for [`Self::take_back`].
|
||||
pub(in crate::ai_editing) fn written(&self) -> TextMark {
|
||||
TextMark { at: self.mark(), values: self.doc_values, lost: self.lost, planned: self.planned.len() }
|
||||
}
|
||||
|
||||
/// Take the frames, stories and fonts added since `m` out again, with what they counted. Where
|
||||
/// a thread was planned since, its frames name its story, so nothing is taken out.
|
||||
pub(in crate::ai_editing) fn take_back(&mut self, m: TextMark) {
|
||||
if self.planned.len() != m.planned {
|
||||
return;
|
||||
}
|
||||
let (frames, stories, bytes, text, fonts) = m.at;
|
||||
self.frames.truncate(frames);
|
||||
self.stories.truncate(stories);
|
||||
self.fonts.truncate(fonts);
|
||||
(self.doc_bytes, self.text_bytes, self.doc_values, self.lost) = (bytes, text, m.values, m.lost);
|
||||
}
|
||||
|
||||
/// Whether a thread written since the last call has attributes the text document leaves out.
|
||||
pub(in crate::ai_editing) fn take_lost(&mut self) -> bool {
|
||||
std::mem::take(&mut self.lost)
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
//! from those layers alone, without its page.
|
||||
//!
|
||||
//! The page is the file's `%%BoundingBox` (`%%HiResBoundingBox` when it has one), the first page
|
||||
//! of a PostScript file without one. A file whose program can't be read (an operator the
|
||||
//! of a PostScript file without one. A `.ai` in the legacy format ([`import_ai`]) that comes in
|
||||
//! through its layers keeps the artboard its header gives, with its art outside it; an EPS comes in
|
||||
//! on its page. A file whose program can't be read (an operator the
|
||||
//! interpreter doesn't know, an error, a limit reached) or that draws nothing comes in from its
|
||||
//! layers when its program has them (a `.ai` in PostScript form whose prolog is named but not
|
||||
//! included, as other apps write it), or with its art on one layer when it is in the legacy format
|
||||
@@ -82,8 +84,6 @@ use graphics::{GState, Out};
|
||||
use interp::{Fault, Interp};
|
||||
use obj::PsError;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use ai::ENVELOPE_DRAWN;
|
||||
pub use ai::NameEncoding;
|
||||
#[cfg(test)]
|
||||
pub(crate) use ai::objects_read;
|
||||
@@ -92,6 +92,8 @@ pub(crate) use ai::{
|
||||
MAX_LISTED, MAX_LISTED_NAME, MAX_SYMBOLS, brush_plugin_name, brushes_kept, envelope_outline, envelope_upright, read_art_style, read_brush_object,
|
||||
read_style_list, style_moves_with, symbol_art_member, warp_number,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use ai::{ENVELOPE_DRAWN, ENVELOPE_TYPE_DRAWN};
|
||||
pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES, record_version};
|
||||
pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from};
|
||||
pub(crate) use native::{MAX_TEXT, scale_document, scale_symbols};
|
||||
@@ -279,7 +281,20 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<
|
||||
/// and data sets) read in `names`. The type of the document it gives that is set in families that
|
||||
/// aren't available shows in the fallback font, and a warning names them.
|
||||
pub fn import_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
|
||||
let mut r = read_names(bytes, editing_data, trust, names)?;
|
||||
read_checked(bytes, editing_data, trust, names, false)
|
||||
}
|
||||
|
||||
/// [`import_names`] for a `.ai` (or `.ait`) in PostScript form: a program in the legacy format
|
||||
/// that comes in through its layers keeps the artboard its header gives, with its art outside it,
|
||||
/// where an EPS comes in on its page, its art's box. A `.ai` of versions 3 and 4, which doesn't
|
||||
/// give its artboard's size, has a US Letter artboard around its template box's center.
|
||||
pub fn import_ai(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
|
||||
read_checked(bytes, editing_data, trust, names, true)
|
||||
}
|
||||
|
||||
/// [`import_names`] (a `.ai` when `ai_file`, see [`import_ai`]).
|
||||
fn read_checked(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding, ai_file: bool) -> Result<Imported, String> {
|
||||
let mut r = read_names(bytes, editing_data, trust, names, ai_file)?;
|
||||
let missing = missing_fonts(&r.document);
|
||||
if !missing.is_empty() {
|
||||
r.warnings.push(format!("{MISSING_FONTS_NOTE}: {}", missing.join(", ")));
|
||||
@@ -314,8 +329,8 @@ fn missing_fonts(d: &Document) -> Vec<String> {
|
||||
missing
|
||||
}
|
||||
|
||||
/// [`import_names`] before the type's fonts are checked.
|
||||
fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
|
||||
/// [`import_names`] before the type's fonts are checked (a `.ai` when `ai_file`, see [`import_ai`]).
|
||||
fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding, ai_file: bool) -> Result<Imported, String> {
|
||||
let ps = program(bytes)?;
|
||||
let dsc = Dsc::read(ps);
|
||||
// A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no
|
||||
@@ -323,7 +338,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
|
||||
// printed).
|
||||
if editing_data
|
||||
&& dsc.own_legacy
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, names)
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, ai_file, names)
|
||||
{
|
||||
return Ok(Imported { document, warnings, preview: false });
|
||||
}
|
||||
@@ -343,7 +358,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
|
||||
}
|
||||
let drew = !out.drawn.is_empty();
|
||||
let why = match (&result, drew) {
|
||||
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, names)),
|
||||
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, ai_file, names)),
|
||||
(Ok(()), true) => return Ok(finish(out)),
|
||||
(Ok(()), false) => "it draws nothing VectorCraft's PostScript reader can show".to_string(),
|
||||
(Err(e), _) => reason(e, fault.as_ref()),
|
||||
@@ -351,7 +366,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
|
||||
// A file in the legacy Illustrator format whose program can't be run (a `.ai` that names the
|
||||
// prolog defining its operators without including it): its layers, which the program has, or
|
||||
// its art on one layer when it has none (Illustrator 3).
|
||||
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, names)).flatten() {
|
||||
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, ai_file, names)).flatten() {
|
||||
Some(Ok((document, warnings))) => return Ok(Imported { document, warnings, preview: false }),
|
||||
Some(Err(e)) => format!("{why}; its layers couldn't be read either ({e})"),
|
||||
None => why,
|
||||
|
||||
@@ -55,8 +55,8 @@ pub use read::{
|
||||
palette, read_beside, read_in, symbols, variables,
|
||||
};
|
||||
pub(crate) use read::{
|
||||
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_style_list, slot_frame, slot_of, style_moves_with,
|
||||
symbol_art_member,
|
||||
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_program, read_style_list, slot_frame, slot_of,
|
||||
style_moves_with, symbol_art_member,
|
||||
};
|
||||
|
||||
pub(crate) use read::is_look_slot;
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
//! - State: hidden `Xw`, locked `A`, transparency `mode opacity isolate knockout shape Xy`.
|
||||
//! - Names: an `/ArtDictionary` after an object, its `AI10_ArtUID` the name's XML id.
|
||||
//! - Artboards: the `ArtboardArray` of the `/Document` dictionary, with each one's display
|
||||
//! options, else `%AI3_Cropmarks`, else the art size around the template box's centre
|
||||
//! (`%AI3_TemplateBox`, `%%TemplateBox` in older files), else (a file in the legacy format
|
||||
//! another app wrote) the bounding box.
|
||||
//! options, else `%AI3_Cropmarks`, else the art size (`%AI5_ArtSize`; VectorCraft's own default,
|
||||
//! US Letter, in a `.ai` of versions 3 and 4, which don't give it) around the template box's center (`%AI3_TemplateBox`,
|
||||
//! `%%TemplateBox` in older files), which the version 7 specification (§2.2) puts at the
|
||||
//! artboard's center, else (a file in the legacy format another app wrote) the bounding box.
|
||||
//! - Document settings: Document Raster Effects Settings from the `/Document` dictionary's
|
||||
//! `AI Auto Rasterize`; the author from the header's `%%For`; swatches for the custom colors of
|
||||
//! `%%CMYKCustomColor` and `%%RGBCustomColor` that the document has none of.
|
||||
@@ -273,12 +274,28 @@ pub(crate) fn read_over_page(data: &[u8], large_canvas: bool, names: NameEncodin
|
||||
read_with(data, large_canvas, true, names)
|
||||
}
|
||||
|
||||
fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
/// [`read_in`] (or [`read_over_page`] when `page_art`) for the program of a `.ai` in the legacy
|
||||
/// format: one of versions 3 and 4 that has a template box but no art size has a US Letter
|
||||
/// artboard ([`Reader::sized_board`]).
|
||||
pub(crate) fn read_program(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
let mut r = Reader::with_names(data, names);
|
||||
r.large_canvas = large_canvas.then_some(1.0);
|
||||
r.page_art = page_art;
|
||||
r.run()?;
|
||||
r.finish()
|
||||
r.legacy_ai = true;
|
||||
r.read_as(large_canvas, page_art)
|
||||
}
|
||||
|
||||
fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
Reader::with_names(data, names).read_as(large_canvas, page_art)
|
||||
}
|
||||
|
||||
impl Reader<'_> {
|
||||
/// The structure read, the declared large canvas scale applied when `large_canvas`, the page's
|
||||
/// art replacing the objects not read when `page_art` ([`read_over_page`]).
|
||||
fn read_as(mut self, large_canvas: bool, page_art: bool) -> Result<Structure, String> {
|
||||
self.large_canvas = large_canvas.then_some(1.0);
|
||||
self.page_art = page_art;
|
||||
self.run()?;
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// How many objects [`read`] counts against [`MAX_NODES`] in the editing data `data`.
|
||||
@@ -645,6 +662,11 @@ struct Reader<'a> {
|
||||
/// The artboard's size and centre in files without artboards or crop marks.
|
||||
art_size: Option<(f64, f64)>,
|
||||
template_center: Option<(f64, f64)>,
|
||||
/// The data is the program of a `.ai` in the legacy format ([`read_program`]), not an EPS's:
|
||||
/// without an art size, its artboard can be US Letter ([`Reader::sized_board`]).
|
||||
legacy_ai: bool,
|
||||
/// The header has `%AI5_FileFormat`, which files of version 5 and later write.
|
||||
file_format: bool,
|
||||
/// `%AI24_LargeCanvasScale`: a Large Canvas document's editing data is written at 1/N scale
|
||||
/// (see [`Reader::ensure_space`]); 1 (no scaling) when the file doesn't declare one, `None`
|
||||
/// when the declared factor is not applied ([`read`]).
|
||||
@@ -827,6 +849,8 @@ impl<'a> Reader<'a> {
|
||||
xml_id_bytes: 0,
|
||||
art_size: None,
|
||||
template_center: None,
|
||||
legacy_ai: false,
|
||||
file_format: false,
|
||||
large_canvas: None,
|
||||
page_art: false,
|
||||
canvas_scale: 1.0,
|
||||
|
||||
@@ -242,10 +242,13 @@ impl<'a> Reader<'a> {
|
||||
&& let Ok(n) = v.trim().parse::<u8>()
|
||||
{
|
||||
self.ruler_units = Some(n);
|
||||
} else if self.frames.is_empty() && line.starts_with("AI5_FileFormat") {
|
||||
self.file_format = true;
|
||||
} else if let Some(v) = line.strip_prefix("AI5_ArtSize:") {
|
||||
self.art_size = two(v);
|
||||
} else if let Some(v) = line.strip_prefix("AI3_TemplateBox:").or_else(|| line.strip_prefix("%TemplateBox:")) {
|
||||
self.template_center = two(v);
|
||||
// The center of the box (a file without a template writes it with no width or height).
|
||||
self.template_center = four(v).map(|[x0, y0, x1, y1]| ((x0 + x1) / 2.0, (y0 + y1) / 2.0)).or_else(|| two(v));
|
||||
self.template = four(v).or(self.template);
|
||||
} else if let Some(v) = line.strip_prefix("AI24_LargeCanvasScale:") {
|
||||
self.large_canvas_scale(v);
|
||||
|
||||
@@ -871,9 +871,15 @@ impl<'a> Reader<'a> {
|
||||
// ---- the document ----------------------------------------------------------------------
|
||||
|
||||
/// The artboard of a file without artboards or crop marks: its size around the template
|
||||
/// box's centre (half a point off the grid).
|
||||
/// box's center (half a point off the grid). The program of a `.ai` of versions 3 and 4
|
||||
/// ([`Reader::legacy_ai`], no `%AI5_FileFormat`), which don't record the artboard's size, has
|
||||
/// VectorCraft's own default there, a US Letter artboard (612 × 792 pt); an EPS, or a newer
|
||||
/// file without `%AI5_ArtSize`, has none (its page is its bounding box).
|
||||
fn sized_board(&self) -> Option<Rect> {
|
||||
let ((w, h), (cx, cy)) = (self.art_size?, self.template_center?);
|
||||
let (cx, cy) = self.template_center?;
|
||||
let [_, _, letter_w, letter_h] = crate::import::LETTER;
|
||||
let letter = self.legacy_ai && !self.file_format;
|
||||
let (w, h) = self.art_size.or(letter.then_some((letter_w, letter_h)))?;
|
||||
// Rounded out, but not off a whole point the decimals in the file only just miss.
|
||||
let near = |v: f64| (v - v.round()).abs() < 1e-6;
|
||||
let (left, top) = (cx - w / 2.0, cy + h / 2.0);
|
||||
|
||||
@@ -19,6 +19,16 @@ pub(super) enum Page {
|
||||
Art,
|
||||
/// A `.ai` is read on its (first and only) artboard.
|
||||
Artboard,
|
||||
/// A `.ai` in the legacy format (PostScript, not an EPS): its page is its art's bounding box, as
|
||||
/// an EPS's is, and its artboard is the one its header gives.
|
||||
Program,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
/// Is the page the art's bounding box?
|
||||
fn of_art(self) -> bool {
|
||||
matches!(self, Page::Art | Page::Program)
|
||||
}
|
||||
}
|
||||
|
||||
/// How art moved from a page to its place in the layers' document transforms: its pattern tiles
|
||||
@@ -62,8 +72,12 @@ pub(super) fn build(
|
||||
) -> Result<(Document, Vec<String>), String> {
|
||||
// An EPS (its page is its art) draws a Large Canvas page at full size; a `.ai` keeps its art
|
||||
// space, as its PDF part may (#2031).
|
||||
let large_canvas = matches!(page, Page::Art);
|
||||
let structure = if visible.is_some() { ai::read_over_page(data, large_canvas, names)? } else { ai::read_in(data, large_canvas, names)? };
|
||||
let large_canvas = page.of_art();
|
||||
let structure = match page {
|
||||
Page::Program => ai::read_program(data, large_canvas, visible.is_some(), names)?,
|
||||
_ if visible.is_some() => ai::read_over_page(data, large_canvas, names)?,
|
||||
_ => ai::read_in(data, large_canvas, names)?,
|
||||
};
|
||||
build_from(structure, data, visible, page, outlined, trust, unit)
|
||||
}
|
||||
|
||||
@@ -184,7 +198,7 @@ fn page_scale_alone(structure: &Structure, pages: &[(f64, f64)], unit: f64) -> O
|
||||
/// page, which draws nothing for them, the linked images' `placeholders`. A PDF part draws those
|
||||
/// images' pixels, which the placeholders don't match.
|
||||
fn left_out_of_comparisons(page: Page, mut restyled: Vec<Rect>, placeholders: Vec<Rect>) -> Vec<Rect> {
|
||||
if matches!(page, Page::Art) {
|
||||
if page.of_art() {
|
||||
restyled.extend(placeholders);
|
||||
}
|
||||
restyled
|
||||
@@ -295,7 +309,7 @@ pub(super) fn build_weighing(
|
||||
let mut frame = None;
|
||||
if let Some(visible) = visible {
|
||||
let art_box = match page {
|
||||
Page::Art => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
|
||||
Page::Art | Page::Program => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
|
||||
Page::Artboard => artboard.ok_or("its editing data doesn't say where its artboard is")?,
|
||||
};
|
||||
let page_rect = visible.artboards.first().map(|a| a.rect).ok_or("it has no page")?;
|
||||
@@ -393,8 +407,9 @@ pub(super) fn build_weighing(
|
||||
// An EPS saved as its artboard has the art outside it left out of its page.
|
||||
let artboard_page =
|
||||
artboard.is_some_and(|a| [a.x0 - art_box.x0, a.y0 - art_box.y0, a.x1 - art_box.x1, a.y1 - art_box.y1].iter().all(|d| d.abs() <= 1.0));
|
||||
let whole = matches!(page, Page::Art) && !artboard_page;
|
||||
if whole {
|
||||
let whole = page.of_art() && !artboard_page;
|
||||
// A `.ai` keeps the artboard its editing copy gives, with the art outside it.
|
||||
if whole && matches!(page, Page::Art) {
|
||||
// Its page is the file's own (its bounding box), as when it comes in as its page: art
|
||||
// reaching past the editing data's artboard stays on it (#1102).
|
||||
doc.artboards.truncate(1);
|
||||
@@ -1815,7 +1830,7 @@ fn last_boards(boards: &[vectorcraft_doc::Artboard], pages: &[Rect]) -> Option<V
|
||||
/// the one whose art draws most like the page, the first of them on a tie. Else (and for an EPS,
|
||||
/// whose page is its art) the first artboard (0).
|
||||
fn shown_artboard(doc: &Document, visible: &Document, page: Page, page_rect: Rect, fits: impl Fn(&Rect) -> bool) -> usize {
|
||||
if matches!(page, Page::Art) || visible.artboards.len() >= doc.artboards.len() {
|
||||
if page.of_art() || visible.artboards.len() >= doc.artboards.len() {
|
||||
return 0;
|
||||
}
|
||||
let pages: Vec<Rect> = visible.artboards.iter().map(|a| a.rect).collect();
|
||||
|
||||
@@ -13,12 +13,16 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect
|
||||
/// ([`use_symbols`]) and its variable library ([`use_variables`]) when it could be read, and with
|
||||
/// its layer tree, empty, below the page's layer when that could be read (#2033). A program in the
|
||||
/// legacy format without layers (`illustrator`, see [`program_alone`]) is read with its art on one
|
||||
/// layer; when that can't be used, the file is `visible` as it was. `names`: the encoding of names
|
||||
/// that aren't UTF-8.
|
||||
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, names: ai::NameEncoding) -> Imported {
|
||||
/// layer; when that can't be used, the file is `visible` as it was. The program of a `.ai`
|
||||
/// (`ai_file`) in the legacy format keeps the artboard its header gives ([`Page::Program`]).
|
||||
/// `names`: the encoding of names that aren't UTF-8.
|
||||
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, ai_file: bool, names: ai::NameEncoding) -> Imported {
|
||||
let program_page = if ai_file { Page::Program } else { Page::Art };
|
||||
let Some(data) = editing_copy(ps) else {
|
||||
let read =
|
||||
illustrator.then(|| one_layer(ps)).flatten().and_then(|p| build(&p, Some(&visible.document), Page::Art, false, trust, 1.0, names).ok());
|
||||
let read = illustrator
|
||||
.then(|| one_layer(ps))
|
||||
.flatten()
|
||||
.and_then(|p| build(&p, Some(&visible.document), program_page, false, trust, 1.0, names).ok());
|
||||
return match read {
|
||||
Some((document, warnings)) => with_page_notes(document, warnings, &visible),
|
||||
None => visible,
|
||||
@@ -28,11 +32,17 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus
|
||||
// can't be used.
|
||||
// Private data kept apart from the program: the program's own header gives the author and
|
||||
// custom colors the private data's header may lack.
|
||||
let read =
|
||||
|d: &[u8]| if matches!(data, Ok(Cow::Owned(_))) { ai::read_beside(d, true, ps, true, names) } else { ai::read_over_page(d, true, names) };
|
||||
let private = matches!(data, Ok(Cow::Owned(_)));
|
||||
let read = |d: &[u8]| match (private, program_page) {
|
||||
(true, _) => ai::read_beside(d, true, ps, true, names),
|
||||
(false, Page::Program) => ai::read_program(d, true, true, names),
|
||||
(false, _) => ai::read_over_page(d, true, names),
|
||||
};
|
||||
// Private data is an EPS's editing copy, whatever the file is named.
|
||||
let page = if private { Page::Art } else { program_page };
|
||||
let (tree, result) = match data.as_deref() {
|
||||
Ok(d) => match read(d) {
|
||||
Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), Page::Art, false, trust, 1.0)),
|
||||
Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), page, false, trust, 1.0)),
|
||||
Err(why) => (vec![], Err(why)),
|
||||
},
|
||||
Err(why) => (vec![], Err(why.clone())),
|
||||
@@ -607,16 +617,23 @@ fn editing_copy(ps: &[u8]) -> Option<Result<Cow<'_, [u8]>, String>> {
|
||||
/// but whose program has no layers (Illustrator 3 had none; image editors still export their
|
||||
/// paths so, storytold/photocraft#6036) is read with its art on one layer. `None` when the program
|
||||
/// has no layers and isn't in the format: other PostScript isn't read as the format's operators.
|
||||
/// The program of a `.ai` (`ai_file`) has the artboard its header gives ([`Page::Program`]).
|
||||
/// `names`: the encoding of names that aren't UTF-8.
|
||||
pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool, names: ai::NameEncoding) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
pub(in super::super) fn program_alone(
|
||||
ps: &[u8],
|
||||
illustrator: bool,
|
||||
ai_file: bool,
|
||||
names: ai::NameEncoding,
|
||||
) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
if ai::has_eps_data(ps) {
|
||||
return None;
|
||||
}
|
||||
let page = if ai_file { Page::Program } else { Page::Art };
|
||||
if has_layers(ps) {
|
||||
return Some(build(ps, None, Page::Art, false, false, 1.0, names));
|
||||
return Some(build(ps, None, page, false, false, 1.0, names));
|
||||
}
|
||||
let program = illustrator.then(|| one_layer(ps)).flatten()?;
|
||||
Some(build(&program, None, Page::Art, false, false, 1.0, names))
|
||||
Some(build(&program, None, page, false, false, 1.0, names))
|
||||
}
|
||||
|
||||
/// `ps` with its art (from its `%%EndSetup` to its `%%PageTrailer`, else its `%%Trailer`, else its
|
||||
|
||||
@@ -9,7 +9,7 @@ use vectorcraft_doc::clipnest::MAX_NEST;
|
||||
use vectorcraft_doc::{LayerColor, LineCap, Node, NodeKind};
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use crate::import::import;
|
||||
use crate::import::{import, import_ai};
|
||||
|
||||
/// A minimal file in Illustrator's format: its header comments, a prolog of our own defining the
|
||||
/// operators `body` uses (`u` and `U` as `ugroup` and `Ugroup`), and `body`.
|
||||
@@ -329,6 +329,100 @@ fn a_legacy_file_with_its_prolog_comes_in_through_its_layers() {
|
||||
assert_eq!(page.document.layers[0].name.as_deref(), Some("Layer 1"));
|
||||
}
|
||||
|
||||
/// A `.ai` in the legacy format (version 8, `%AI5_FileFormat 4.0`) whose prolog is included: a
|
||||
/// square on its 200-point artboard and one far to its right. Its bounding box is the box of that
|
||||
/// art; its artboard is its art size around the template box's center (version 7 specification
|
||||
/// §2.2). Its first line names the EPSF version, as the format's files may.
|
||||
const LEGACY_AI_ART_OFF_ITS_ARTBOARD: &str = "%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 320 30\n\
|
||||
%%HiResBoundingBox: 10 10 320 30\n%AI5_FileFormat 4.0\n%AI3_TemplateBox: 100.5 99.5 100.5 99.5\n%AI5_ArtSize: 200 200\n\
|
||||
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def\n\
|
||||
/Lb {10 {pop} repeat} def /Ln {pop} def /LB {} def /A {pop} def /k {setcmykcolor} def\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n\
|
||||
%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n(Art) Ln\n0 A\n0 0 0 1 k\n\
|
||||
10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n300 10 m\n320 10 L\n320 30 L\n300 30 L\nf\nLB\n%AI5_EndLayer--\n\
|
||||
%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
|
||||
|
||||
/// `bytes` opened as a `.ai`.
|
||||
fn open_ai(bytes: &[u8]) -> crate::Imported {
|
||||
import_ai(bytes, true, false, crate::NameEncoding::default()).unwrap()
|
||||
}
|
||||
|
||||
/// Such a `.ai` keeps the artboard its header gives, with the square outside it, when it comes in
|
||||
/// through its layers. The same program opened as an EPS comes in on its page, its art's box
|
||||
/// (#1102).
|
||||
#[test]
|
||||
fn a_legacy_ai_keeps_its_artboard_and_the_art_outside_it() {
|
||||
let r = open_ai(LEGACY_AI_ART_OFF_ITS_ARTBOARD.as_bytes());
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let d = &r.document;
|
||||
let boards: Vec<_> = d.artboards.iter().map(|a| a.rect).collect();
|
||||
assert_eq!(boards, [Rect::new(0.0, 0.0, 200.0, 200.0)]);
|
||||
let art: Vec<_> = d.layers[0].children().unwrap().iter().map(|n| n.geometric_bounds().unwrap()).collect();
|
||||
// Art space has y up from the artboard's bottom: (10, 10) is 10 above it.
|
||||
assert_eq!(art, [Rect::new(10.0, 170.0, 30.0, 190.0), Rect::new(300.0, 170.0, 320.0, 190.0)]);
|
||||
let eps = import(LEGACY_AI_ART_OFF_ITS_ARTBOARD.as_bytes()).unwrap();
|
||||
let boards: Vec<_> = eps.document.artboards.iter().map(|a| a.rect.size()).collect();
|
||||
assert_eq!(boards, [kurbo::Size::new(310.0, 20.0)], "{:?}", eps.warnings);
|
||||
}
|
||||
|
||||
/// A version 3 `.ai` doesn't give its artboard's size (`%AI5_ArtSize` came with version 5): its
|
||||
/// artboard is VectorCraft's own default, a US Letter page, around the template box's center, the
|
||||
/// artboard's center (version 7 specification §2.2). Without a template box it is the art's box,
|
||||
/// and so is the page of the same file opened as an EPS.
|
||||
#[test]
|
||||
fn a_version_3_ai_has_a_letter_artboard_around_its_template_boxs_center() {
|
||||
let v3 = PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_TemplateBox: 100 100 100 100\n");
|
||||
let r = open_ai(v3.as_bytes());
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let d = &r.document;
|
||||
let boards: Vec<_> = d.artboards.iter().map(|a| a.rect).collect();
|
||||
assert_eq!(boards, [Rect::new(0.0, 0.0, 612.0, 792.0)]);
|
||||
// The artboard runs from -206 to 406 across and from -296 to 496 up in art space.
|
||||
let art = d.layers[0].children().unwrap();
|
||||
assert_eq!(art[1].geometric_bounds(), Some(Rect::new(226.0, 446.0, 266.0, 476.0)));
|
||||
// The center of a template box with a size (a file with a template image).
|
||||
let r = open_ai(PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_TemplateBox: 50 50 150 150\n").as_bytes());
|
||||
assert_eq!(r.document.layers[0].children().unwrap()[1].geometric_bounds(), Some(Rect::new(226.0, 446.0, 266.0, 476.0)));
|
||||
let r = open_ai(PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_ColorUsage: Color\n").as_bytes());
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
|
||||
let r = import(v3.as_bytes()).unwrap();
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
|
||||
// A newer file (`%AI5_FileFormat`, version 5 on) without an art size keeps the art's box too.
|
||||
let r = open_ai(v3.replace("%%EndComments", "%AI5_FileFormat 1.0\n%%EndComments").as_bytes());
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A version 3 `.ai` whose prolog is included and whose program has no layers: its page draws, and
|
||||
/// its art comes in on one layer around a US Letter artboard, the square far to the right outside
|
||||
/// it.
|
||||
#[test]
|
||||
fn a_version_3_ai_that_draws_keeps_its_letter_artboard_and_the_art_outside_it() {
|
||||
let v3 = "%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 720 30\n%AI3_TemplateBox: 100 100 100 100\n\
|
||||
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def /k {setcmykcolor} def\n\
|
||||
%%EndProlog\n%%BeginSetup\n%%EndSetup\n0 0 0 1 k\n10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n\
|
||||
700 10 m\n720 10 L\n720 30 L\n700 30 L\nf\n%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
|
||||
let r = open_ai(v3.as_bytes());
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let boards: Vec<_> = r.document.artboards.iter().map(|a| a.rect).collect();
|
||||
assert_eq!(boards, [Rect::new(0.0, 0.0, 612.0, 792.0)]);
|
||||
assert_eq!(r.document.layers.len(), 1);
|
||||
let art: Vec<_> = r.document.layers[0].children().unwrap().iter().map(|n| n.geometric_bounds().unwrap()).collect();
|
||||
// The artboard runs from -206 to 406 across and from -296 to 496 up in art space.
|
||||
assert_eq!(art, [Rect::new(216.0, 466.0, 236.0, 486.0), Rect::new(906.0, 466.0, 926.0, 486.0)]);
|
||||
let eps = import(v3.as_bytes()).unwrap();
|
||||
assert_eq!(eps.document.artboards[0].rect, Rect::new(0.0, 0.0, 710.0, 20.0), "{:?}", eps.warnings);
|
||||
}
|
||||
|
||||
/// A `.ai` another app wrote (#1027) that gives a template box but no art size opens on a US Letter
|
||||
/// artboard around the box's center, from -18 to 594 across and -12 to 780 up in art space (as an
|
||||
/// EPS it opens on its bounding box, see above).
|
||||
#[test]
|
||||
fn a_postscript_ai_with_a_template_box_and_no_art_size_opens_on_a_letter_artboard() {
|
||||
let r = open_ai(NAMED_PROLOG.as_bytes());
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 612.0, 792.0), "{:?}", r.warnings);
|
||||
let square = &r.document.layers[0].children().unwrap()[0];
|
||||
assert_eq!(square.geometric_bounds(), Some(Rect::new(28.0, 690.0, 108.0, 770.0)));
|
||||
}
|
||||
|
||||
/// A placed EPS in the legacy format (version 7 specification §16, no `%AI5_BeginPlace`), its file
|
||||
/// included whole: the layers come in, the placed file as the art the page draws in its box.
|
||||
#[test]
|
||||
|
||||
@@ -4791,6 +4791,9 @@ fn an_eps_page_of_its_art_stays_its_page_through_the_layers() {
|
||||
assert_eq!(boards.len(), 1);
|
||||
assert!((boards[0].width() - 4.0).abs() < 1e-6 && (boards[0].height() - 4.0).abs() < 1e-6, "{boards:?}");
|
||||
assert!(boards[0].inflate(1e-6, 1e-6).contains_rect(art), "{boards:?} round {art:?}");
|
||||
// Its private data is an EPS's editing copy, whatever the file is named.
|
||||
let named_ai = crate::import::import_ai(&file, true, false, crate::NameEncoding::default()).unwrap();
|
||||
assert_eq!(named_ai.document.artboards.iter().map(|a| a.rect).collect::<Vec<_>>(), boards, "{:?}", named_ai.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -49,7 +49,7 @@ mod tiff;
|
||||
pub use ai_editing::{AiEditingOptions, AiEditingOutput, CREATOR_VERSION, export_ai_editing, private_data as ai_private_data};
|
||||
pub use import::{
|
||||
AiLayers, Imported, NameEncoding, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard,
|
||||
family_style, import, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page,
|
||||
family_style, import, import_ai, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page,
|
||||
};
|
||||
pub use legacy_ai::{HiddenObjects, LegacyAiOptions, LegacyAiOutput, export_legacy_ai, writes_image_mask};
|
||||
pub use print::{PrintJob, PrintPage, print};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -758,7 +758,15 @@ struct Ctx {
|
||||
/// The path each `<textPath>` read so far follows ([`Self::text_path`]); `None` where it has
|
||||
/// no usable one. The styles of a `<use>` instance share it, so each path is found and parsed
|
||||
/// once however many `<use>` elements show its text.
|
||||
paths: Rc<RefCell<HashMap<roxmltree::NodeId, Option<Rc<BezPath>>>>>,
|
||||
paths: Rc<RefCell<HashMap<roxmltree::NodeId, Option<Rc<FollowedPath>>>>>,
|
||||
}
|
||||
|
||||
/// The path a `<textPath>` follows ([`Ctx::text_path`]).
|
||||
struct FollowedPath {
|
||||
path: BezPath,
|
||||
/// The `pathLength` of the element the `<textPath>` points at, 0 or greater; `None` when the
|
||||
/// `<textPath>` has its own `path` or the element has no such value.
|
||||
author_length: Option<f64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1172,7 +1180,7 @@ impl Ctx {
|
||||
|
||||
/// The path a `<textPath>` follows, in the text's user space. It is read from the file once
|
||||
/// ([`Self::paths`]).
|
||||
fn text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<Rc<BezPath>> {
|
||||
fn text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<Rc<FollowedPath>> {
|
||||
if let Some(known) = self.paths.borrow().get(&tp.id()) {
|
||||
return known.clone();
|
||||
}
|
||||
@@ -1184,13 +1192,14 @@ impl Ctx {
|
||||
/// How many elements the path of `<textPath>` `tp` has, when a text built so far follows it
|
||||
/// ([`Self::text_path`]); 0 for any other node.
|
||||
fn path_elements(&self, tp: XNode) -> usize {
|
||||
self.paths.borrow().get(&tp.id()).and_then(|p| p.as_deref()).map_or(0, |p| p.elements().len())
|
||||
self.paths.borrow().get(&tp.id()).and_then(|p| p.as_deref()).map_or(0, |p| p.path.elements().len())
|
||||
}
|
||||
|
||||
/// Find and parse the path `<textPath>` `tp` follows.
|
||||
fn read_text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<BezPath> {
|
||||
fn read_text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<FollowedPath> {
|
||||
#[cfg(test)]
|
||||
TEXT_PATH_READS.set(TEXT_PATH_READS.get() + 1);
|
||||
let mut author_length = None;
|
||||
let mut bp = match tp.attribute("path") {
|
||||
Some(d) => BezPath::from_svg(d).ok()?,
|
||||
None => {
|
||||
@@ -1202,6 +1211,8 @@ impl Ctx {
|
||||
};
|
||||
let mut bp = BezPath::from_svg(&d).ok()?;
|
||||
bp.apply_affine(parse_transform(el.attribute("transform")));
|
||||
author_length =
|
||||
el.attribute("pathLength").and_then(|v| v.trim().parse::<f64>().ok()).filter(|l| l.is_finite() && *l >= 0.0).map(f64::abs);
|
||||
bp
|
||||
}
|
||||
};
|
||||
@@ -1209,7 +1220,7 @@ impl Ctx {
|
||||
bp = bp.reverse_subpaths();
|
||||
}
|
||||
let usable = bp.segments().next().is_some();
|
||||
usable.then_some(bp)
|
||||
usable.then_some(FollowedPath { path: bp, author_length })
|
||||
}
|
||||
|
||||
/// The equivalent path of a basic shape (SVG 2 › Basic Shapes), as path data: what a
|
||||
@@ -1458,7 +1469,8 @@ impl Ctx {
|
||||
let mut obj = TextObject::point(Point::ZERO, "", CharStyle::default());
|
||||
let mut breaks: Vec<Break> = vec![];
|
||||
let mut para_styles: Vec<ParaStyle> = vec![];
|
||||
if let Some((tp, bp)) = path {
|
||||
if let Some((tp, followed)) = path {
|
||||
let bp = &followed.path;
|
||||
// Along the path: dx is extra advance, dy shifts off the path.
|
||||
let mut off = 0.0;
|
||||
for i in 0..n {
|
||||
@@ -1470,13 +1482,34 @@ impl Ctx {
|
||||
}
|
||||
let len: f64 = bp.segments().map(|s| s.arclen(1e-4)).sum();
|
||||
let em = self.font_size(tp);
|
||||
let offset = tp.attribute("startOffset").and_then(|v| self.length(v, em, len)).unwrap_or(0.0) + dx[0];
|
||||
// `startOffset` as a share of the path's length. A percentage is of the path's computed
|
||||
// length. Any other length is in the units of the referenced element's `pathLength`
|
||||
// when it has one, and a `pathLength` of 0 puts any offset other than 0 infinitely far
|
||||
// along (SVG 2 › Paths › The pathLength attribute). An offset infinitely far along
|
||||
// starts as far past the path's end, or before its start, as type may go.
|
||||
let share = tp
|
||||
.attribute("startOffset")
|
||||
.and_then(|v| {
|
||||
let o = self.length(v, em, len)?;
|
||||
let share = match followed.author_length {
|
||||
Some(p) if !v.trim().ends_with('%') => {
|
||||
if o == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
o / p
|
||||
}
|
||||
}
|
||||
_ => o / len,
|
||||
};
|
||||
Some(if share.is_infinite() { vectorcraft_doc::PATH_LEAD_MAX.copysign(share) } else { share })
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
let width = if anchor > 0.0 { advance(&point_text(&cells)) } else { 0.0 };
|
||||
// On a path of one closed subpath, text that starts before the path's start or past its
|
||||
// end wraps around the path. On an open path text may start before the path's start,
|
||||
// and its characters before the start are not drawn; text that starts past the end
|
||||
// starts at the end. Other paths start it at the path's start or end.
|
||||
let path = PathData::from_bezpath(&bp);
|
||||
let path = PathData::from_bezpath(bp);
|
||||
let wraps = path.subpaths.len() == 1 && path.is_closed();
|
||||
let open = !path.is_closed();
|
||||
let wrap = |s: f64| {
|
||||
@@ -1484,7 +1517,7 @@ impl Ctx {
|
||||
let s = s.rem_euclid(1.0);
|
||||
if s < 1.0 { s } else { 0.0 }
|
||||
};
|
||||
let start = Some((offset - anchor * width) / len).filter(|s| s.is_finite()).map_or(0.0, |s| {
|
||||
let start = Some(share + (dx[0] - anchor * width) / len).filter(|s| s.is_finite()).map_or(0.0, |s| {
|
||||
if wraps {
|
||||
wrap(s)
|
||||
} else if open {
|
||||
|
||||
@@ -21,4 +21,9 @@ fn uses_of_a_text_on_a_path_read_the_path_once() {
|
||||
// A path with no usable data is not read again either.
|
||||
let (texts, reads, _) = prepared(&format!(r##"<defs><text id="t"><textPath href="#none">Path</textPath></text></defs>{uses}"##));
|
||||
assert_eq!((texts, reads), (51, 1));
|
||||
// The path's pathLength is read with it.
|
||||
let (texts, reads, _) = prepared(&format!(
|
||||
r##"<defs><path id="p" d="M0 20L150 20" pathLength="10"/><text id="t"><textPath href="#p" startOffset="5">Path</textPath></text></defs>{uses}"##
|
||||
));
|
||||
assert_eq!((texts, reads), (51, 1));
|
||||
}
|
||||
|
||||
@@ -299,6 +299,38 @@ fn text_path_becomes_type_on_a_path() {
|
||||
assert_eq!(t[2].para.justify, Justify::Left);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_path_start_offset_is_scaled_by_the_path_length() {
|
||||
// A path 400 long whose pathLength is 100: a startOffset of 50 lies 200 along it. A
|
||||
// percentage stays a share of the computed length. Huge values of both still give their
|
||||
// ratio. With a pathLength of 0, an offset of 0 stays at the start and any other offset lies
|
||||
// past the end, as does an offset too large to scale on a short path.
|
||||
let d = open(
|
||||
r##"<path id="p" d="M0 100 H400" pathLength="100" fill="none" stroke="black"/>
|
||||
<path id="z" d="M0 200 H400" pathLength="0" fill="none" stroke="black"/>
|
||||
<path id="h" d="M0 300 H400" pathLength="1e308" fill="none" stroke="black"/>
|
||||
<path id="s" d="M0 400 H0.1" pathLength="1e-10" fill="none" stroke="black"/>
|
||||
<text font-size="10"><textPath href="#p" startOffset="50">Half</textPath></text>
|
||||
<text font-size="10"><textPath href="#p" startOffset="25%">Quarter</textPath></text>
|
||||
<text font-size="10"><textPath href="#h" startOffset="5e307">Huge</textPath></text>
|
||||
<text font-size="10"><textPath href="#z" startOffset="0">Zero</textPath></text>
|
||||
<text font-size="10"><textPath href="#z" startOffset="50">Fifty</textPath></text>
|
||||
<text font-size="10"><textPath href="#s" startOffset="1e308">Past</textPath></text>"##,
|
||||
);
|
||||
let t = texts(&d);
|
||||
assert_eq!(t.len(), 6);
|
||||
let start_of = |t: &TextObject| match t.kind {
|
||||
TextKind::OnPath { start, .. } => start,
|
||||
_ => panic!("{:?}", t.kind),
|
||||
};
|
||||
assert!(close(start_of(t[0]), 0.5, 1e-9), "{}", start_of(t[0]));
|
||||
assert!(close(start_of(t[1]), 0.25, 1e-9), "{}", start_of(t[1]));
|
||||
assert!(close(start_of(t[2]), 0.5, 1e-9), "{}", start_of(t[2]));
|
||||
assert_eq!(start_of(t[3]), 0.0);
|
||||
assert_eq!(start_of(t[4]), 1.0);
|
||||
assert_eq!(start_of(t[5]), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_on_a_closed_path_wraps_past_the_path_start() {
|
||||
// A closed path 400 long: text anchored before its start, or offset past its end, wraps
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::cache::RegionCache;
|
||||
use super::lookup::{
|
||||
connected_edges, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_at_where, lp_stroke_paint, node_outline, visible_edge_near,
|
||||
visible_face_at,
|
||||
connected_edges, edges, faces, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_at_where, lp_stroke_paint, node_outline,
|
||||
visible_edge_near, visible_face_at,
|
||||
};
|
||||
use super::map::{HIGHLIGHT_RED, sorted_roots};
|
||||
use super::{
|
||||
@@ -711,8 +711,14 @@ pub const LP_SELECT_WIDTH_RANGE: (f64, f64) = (1.0, 20.0);
|
||||
/// with Shift held at the press, each of them leaves the selection if it is selected and joins it
|
||||
/// 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.
|
||||
/// that have its stroke paint ([`LivePaintSelectionTool::reach_at`]). A triple-click on a face
|
||||
/// selects every face of its group with that face's fill; on an edge, every edge of its group with
|
||||
/// that edge's stroke paint ([`LivePaintSelectionTool::same_paint_at`]). With Shift held, each part a
|
||||
/// double-click reaches leaves the selection if it is selected and joins it otherwise, and after a
|
||||
/// Shift-triple-click each part with the clicked part's paint is toggled from the selection there
|
||||
/// was before the three clicks. Clicks of the three made without Shift select as they do alone, so
|
||||
/// when Shift is first held at a later click, the parts are toggled from the selection there was
|
||||
/// before that click.
|
||||
/// 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
|
||||
@@ -733,6 +739,33 @@ pub struct LivePaintSelectionTool {
|
||||
width: f64,
|
||||
/// The drag in progress, from the press to the release.
|
||||
drag: Option<LpDrag>,
|
||||
/// The part each of the last two clicks toggled with Shift held, the latest last (None for a
|
||||
/// click that toggled nothing). A click or marquee without Shift clears both.
|
||||
recent: [Option<NodeId>; 2],
|
||||
/// The parts the last double-click, the two clicks before it and the Shift-clicks after it
|
||||
/// toggled, each listed once when toggled an odd number of times. A Shift-triple-click toggles
|
||||
/// them back as it toggles the parts with the clicked part's paint, so the selection ends as it
|
||||
/// was before the clicks with those parts toggled.
|
||||
toggled: Vec<NodeId>,
|
||||
}
|
||||
|
||||
/// `a` (which lists each id once) with each id of `b` toggled in turn: an id `b` lists an odd number
|
||||
/// of times is removed when `a` holds it and added at the end otherwise, in `b`'s order.
|
||||
fn toggle_ids(a: &mut Vec<NodeId>, b: &[NodeId]) {
|
||||
use std::collections::HashSet;
|
||||
let mut odd = HashSet::new();
|
||||
for id in b {
|
||||
if !odd.insert(*id) {
|
||||
odd.remove(id);
|
||||
}
|
||||
}
|
||||
let held: HashSet<NodeId> = a.iter().copied().collect();
|
||||
a.retain(|x| !odd.contains(x));
|
||||
for id in b {
|
||||
if !held.contains(id) && odd.remove(id) {
|
||||
a.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A press and drag of the Live Paint Selection tool.
|
||||
@@ -770,6 +803,8 @@ impl Default for LivePaintSelectionTool {
|
||||
color: HIGHLIGHT_RED,
|
||||
width: LP_SELECT_WIDTH,
|
||||
drag: None,
|
||||
recent: [None; 2],
|
||||
toggled: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,6 +847,25 @@ impl LivePaintSelectionTool {
|
||||
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) }
|
||||
}
|
||||
|
||||
/// What a triple-click at `p` selects, Alt held or not: on the face a click there picks
|
||||
/// ([`LivePaintSelectionTool::picks_edge`]), that face and every other face of its group that is
|
||||
/// not hidden and has its fill; on the edge a click picks, that edge and every other edge of its
|
||||
/// group that is not hidden and draws with the stroke paint it draws with ([`lp_stroke_paint`]).
|
||||
/// The part under the pointer comes first. Empty where a click selects no face or edge.
|
||||
fn same_paint_at(&self, cx: &ToolContext, p: Point, alt: bool) -> Vec<NodeId> {
|
||||
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![] };
|
||||
let others: Vec<NodeId> = if edge {
|
||||
let like = lp_stroke_paint(part);
|
||||
edges(group).into_iter().filter(|e| e.visible && e.id != part.id && lp_stroke_paint(e) == like).map(|e| e.id).collect()
|
||||
} else {
|
||||
let like = part.appearance.fill_paint();
|
||||
faces(group).into_iter().filter(|f| f.visible && f.id != part.id && f.appearance.fill_paint() == like).map(|f| f.id).collect()
|
||||
};
|
||||
std::iter::once(part.id).chain(others).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The faces (with `faces`) and edges (with `edges`) that marquee `r` touches, among the faces and
|
||||
@@ -918,6 +972,17 @@ impl Tool for LivePaintSelectionTool {
|
||||
let d = LpDrag { cur: p, ..d };
|
||||
let Some(r) = d.marquee(cx) else {
|
||||
// A click selects what the press picked.
|
||||
// A click without Shift replaces the selection, so what earlier clicks toggled
|
||||
// no longer counts.
|
||||
if d.shift {
|
||||
self.recent = [self.recent[1], d.hit];
|
||||
if let Some(id) = d.hit {
|
||||
toggle_ids(&mut self.toggled, &[id]);
|
||||
}
|
||||
} else {
|
||||
self.recent = [None; 2];
|
||||
self.toggled.clear();
|
||||
}
|
||||
return match (d.hit, d.shift) {
|
||||
(Some(id), true) => vec![Action::Exec("select.toggle".into(), json!({ "id": id.0 }))],
|
||||
(Some(id), false) => vec![Action::Exec("select.set".into(), json!({ "ids": [id.0] }))],
|
||||
@@ -925,6 +990,8 @@ impl Tool for LivePaintSelectionTool {
|
||||
(None, false) => vec![Action::Exec("select.none".into(), json!({}))],
|
||||
};
|
||||
};
|
||||
self.recent = [None; 2];
|
||||
self.toggled.clear();
|
||||
let ids = lp_parts_in(cx, r, self.fills, self.strokes);
|
||||
match (ids.is_empty(), d.shift) {
|
||||
(true, true) => vec![],
|
||||
@@ -937,15 +1004,36 @@ impl Tool for LivePaintSelectionTool {
|
||||
// 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);
|
||||
self.toggled.clear();
|
||||
if ev.mods.shift {
|
||||
let clicks: Vec<NodeId> = self.recent.iter().flatten().copied().collect();
|
||||
toggle_ids(&mut self.toggled, &clicks);
|
||||
toggle_ids(&mut self.toggled, &ids);
|
||||
}
|
||||
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) }))],
|
||||
}
|
||||
}
|
||||
// The tool takes no action on a triple-click; its third press and release select as a
|
||||
// click does.
|
||||
PointerKind::TripleClick => vec![],
|
||||
PointerKind::TripleClick => {
|
||||
// The clicks before it already selected the part under the pointer; the
|
||||
// triple-click selects the part with every part of its group that has its paint.
|
||||
// With Shift held, the double-click and the third click toggled parts too, and the
|
||||
// triple-click toggles those back.
|
||||
let ids = self.same_paint_at(cx, p, ev.mods.alt);
|
||||
let toggled = std::mem::take(&mut self.toggled);
|
||||
self.recent = [None; 2];
|
||||
if ids.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
if !ev.mods.shift {
|
||||
return vec![Action::Exec("select.set".into(), json!({ "ids": json_ids(&ids) }))];
|
||||
}
|
||||
let mut flip = ids;
|
||||
toggle_ids(&mut flip, &toggled);
|
||||
if flip.is_empty() { vec![] } else { vec![Action::Exec("select.toggle".into(), json!({ "ids": json_ids(&flip) }))] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,6 +1051,8 @@ impl Tool for LivePaintSelectionTool {
|
||||
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.drag = None;
|
||||
self.recent = [None; 2];
|
||||
self.toggled.clear();
|
||||
vec![]
|
||||
}
|
||||
|
||||
|
||||
@@ -2573,20 +2573,167 @@ fn live_paint_selection_double_click_selects_connected_faces_or_edges() {
|
||||
assert!(dbl(&mut t, 25.0, 1.0, alt).is_empty());
|
||||
}
|
||||
|
||||
/// A triple-click on a face selects every face of its group with that face's fill, joined to it or
|
||||
/// not; on an edge (Alt), every edge of its group that draws with that edge's stroke paint. Shift
|
||||
/// toggles them. Hidden parts, other groups and parts with another paint are left out.
|
||||
#[test]
|
||||
fn live_paint_selection_triple_click_adds_nothing_to_its_clicks() {
|
||||
fn live_paint_selection_triple_click_selects_every_part_of_the_same_paint() {
|
||||
let mut d = Document::new(500.0, 500.0);
|
||||
let (_, [a, ..], _) = live_strip(&mut d);
|
||||
let (_, [a, b, c], [ab, bc, top_a, top_b, top_c, bottom]) = live_strip(&mut d);
|
||||
let (other, [_, other_top, ..]) = live_square(&mut d, Rect::new(200.0, 0.0, 300.0, 100.0));
|
||||
let other_face = faces(d.node(other).unwrap())[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));
|
||||
// A and C are red and B is blue; the other group's face is red and its top edge black, too.
|
||||
for (id, fill) in [(a, &red), (b, &blue), (c, &red), (other_face, &red)] {
|
||||
d.node_mut(id).unwrap().appearance.set_fill(fill.clone());
|
||||
}
|
||||
d.node_mut(other_top).unwrap().appearance.set_stroke(Paint::solid(vectorcraft_color::Color::BLACK));
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let set = |ids: &[NodeId]| vec![Action::Exec("select.set".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))];
|
||||
let toggle = |ids: &[NodeId]| vec![Action::Exec("select.toggle".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))];
|
||||
let alt = Mods { alt: true, ..Default::default() };
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
let tri = |d: &Document, t: &mut LivePaintSelectionTool, x: f64, y: f64, m: Mods| {
|
||||
let cx = cx(d, &s, &p);
|
||||
t.pointer(&cx, &ev(PointerKind::TripleClick, x, y).with_mods(m))
|
||||
};
|
||||
let mut t = LivePaintSelectionTool::default();
|
||||
// A face: every face of its group with its fill, even across the stroked edge BC; the clicked face first.
|
||||
assert_eq!(tri(&d, &mut t, 125.0, 25.0, Mods::default()), set(&[c, a]));
|
||||
assert_eq!(tri(&d, &mut t, 75.0, 25.0, Mods::default()), set(&[b]));
|
||||
// An edge: every edge of its group that draws with its stroke paint, joined to it or not.
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 1.0, alt), set(&[top_a, top_b, bottom]));
|
||||
assert_eq!(tri(&d, &mut t, 125.0, 1.0, alt), set(&[top_c, bc]));
|
||||
assert_eq!(tri(&d, &mut t, 50.0, 25.0, alt), set(&[ab]));
|
||||
// With Shift held, the parts leave the selection or join it.
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 25.0, shift), toggle(&[a, c]));
|
||||
assert_eq!(tri(&d, &mut t, 125.0, 1.0, Mods { alt: true, shift: true, ..Default::default() }), toggle(&[top_c, bc]));
|
||||
// Away from the group, a triple-click adds nothing to what its clicks did.
|
||||
assert!(tri(&d, &mut t, 400.0, 400.0, Mods::default()).is_empty());
|
||||
// A hidden face of the same fill is left out.
|
||||
d.node_mut(c).unwrap().visible = false;
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 25.0, Mods::default()), set(&[a]));
|
||||
// On the other group, a triple-click selects that group's parts alone.
|
||||
assert_eq!(tri(&d, &mut t, 250.0, 50.0, Mods::default()), set(&[other_face]));
|
||||
assert_eq!(tri(&d, &mut t, 250.0, 1.0, alt), set(&[other_top]));
|
||||
// Select Fills off: the triple-click picks the edge nearest the pointer; both off: nothing.
|
||||
t.set_option("selectFills", &json!(false));
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 1.0, Mods::default()), set(&[top_a, top_b, bottom]));
|
||||
t.set_option("selectStrokes", &json!(false));
|
||||
assert!(tri(&d, &mut t, 25.0, 25.0, Mods::default()).is_empty());
|
||||
assert!(tri(&d, &mut t, 25.0, 1.0, alt).is_empty());
|
||||
}
|
||||
|
||||
/// The whole event series of a triple-click (three presses and releases, the double-click after
|
||||
/// the second release and the triple-click after the third), applied to a selection: without
|
||||
/// Shift the selection ends as the parts with the clicked part's paint, and with Shift as the
|
||||
/// selection from before the clicks with those parts toggled, whatever the double-click reached.
|
||||
#[test]
|
||||
fn live_paint_selection_triple_click_series_ends_with_the_same_paint_parts() {
|
||||
let mut d = Document::new(500.0, 500.0);
|
||||
let (_, [a, b, c], [_, bc, top_a, top_b, top_c, bottom]) = live_strip(&mut d);
|
||||
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));
|
||||
// A and C are red and B is blue; a double-click on A also reaches B across the unstroked edge AB.
|
||||
for (id, fill) in [(a, &red), (b, &blue), (c, &red)] {
|
||||
d.node_mut(id).unwrap().appearance.set_fill(fill.clone());
|
||||
}
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let apply = |sel: &mut Vec<u64>, acts: Vec<Action>| {
|
||||
for act in acts {
|
||||
let Action::Exec(cmd, args) = act else { panic!("{act:?}") };
|
||||
let ids: Vec<u64> = match (args.get("ids"), args.get("id")) {
|
||||
(Some(v), _) => v.as_array().unwrap().iter().map(|i| i.as_u64().unwrap()).collect(),
|
||||
(None, Some(i)) => vec![i.as_u64().unwrap()],
|
||||
(None, None) => vec![],
|
||||
};
|
||||
match cmd.as_str() {
|
||||
"select.set" => *sel = ids,
|
||||
"select.none" => sel.clear(),
|
||||
"select.toggle" => {
|
||||
for i in ids {
|
||||
match sel.iter().position(|x| *x == i) {
|
||||
Some(k) => {
|
||||
sel.remove(k);
|
||||
}
|
||||
None => sel.push(i),
|
||||
}
|
||||
}
|
||||
}
|
||||
other => panic!("{other}"),
|
||||
}
|
||||
}
|
||||
};
|
||||
// The first click at `first`, every later event at (`x`, `y`).
|
||||
let series = |t: &mut LivePaintSelectionTool, sel: &mut Vec<u64>, first: (f64, f64), x: f64, y: f64, m: Mods| {
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
apply(sel, t.pointer(&cx, &ev(kind, first.0, first.1).with_mods(m)));
|
||||
}
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
apply(sel, t.pointer(&cx, &ev(kind, x, y).with_mods(m)));
|
||||
}
|
||||
};
|
||||
let sorted = |ids: &[NodeId]| {
|
||||
let mut v: Vec<u64> = ids.iter().map(|i| i.0).collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
};
|
||||
let alt = Mods { alt: true, ..Default::default() };
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
let mut t = LivePaintSelectionTool::default();
|
||||
// The third click's press and release select the face under the pointer, as a click does.
|
||||
assert_eq!(lp_click(&mut t, &cx, ev(PointerKind::Down, 25.0, 25.0)), vec![Action::Exec("select.set".into(), json!({ "ids": [a.0] }))]);
|
||||
for m in [Mods::default(), Mods { alt: true, ..Default::default() }, Mods { shift: true, ..Default::default() }] {
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::TripleClick, 25.0, 25.0).with_mods(m)).is_empty());
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::TripleClick, 25.0, 1.0).with_mods(m)).is_empty());
|
||||
for (start, first, x, y, m, want) in [
|
||||
(vec![], (25.0, 25.0), 25.0, 25.0, Mods::default(), sorted(&[a, c])),
|
||||
(vec![top_c.0], (25.0, 25.0), 25.0, 25.0, Mods::default(), sorted(&[a, c])),
|
||||
(vec![], (25.0, 25.0), 25.0, 25.0, shift, sorted(&[a, c])),
|
||||
(vec![top_c.0, c.0], (25.0, 25.0), 25.0, 25.0, shift, sorted(&[a, top_c])),
|
||||
(vec![], (25.0, 1.0), 25.0, 1.0, Mods { alt: true, shift: true, ..Default::default() }, sorted(&[top_a, top_b, bottom])),
|
||||
(vec![bc.0], (125.0, 1.0), 125.0, 1.0, alt, sorted(&[top_c, bc])),
|
||||
// The first click on A and the rest on B, beside it: B alone has B's fill.
|
||||
(vec![], (49.0, 25.0), 51.0, 25.0, shift, sorted(&[b])),
|
||||
(vec![c.0], (49.0, 25.0), 51.0, 25.0, shift, sorted(&[b, c])),
|
||||
] {
|
||||
let mut sel = start.clone();
|
||||
series(&mut t, &mut sel, first, x, y, m);
|
||||
sel.sort_unstable();
|
||||
assert_eq!(sel, want, "from {start:?} at {first:?} then ({x}, {y}) with {m:?}");
|
||||
}
|
||||
// Shift first held at the third click: the two clicks and the double-click select A and B, and
|
||||
// the parts with A's fill are toggled from that selection.
|
||||
let mut sel = vec![top_c.0];
|
||||
for (kind, m) in [
|
||||
(PointerKind::Down, Mods::default()),
|
||||
(PointerKind::Up, Mods::default()),
|
||||
(PointerKind::Down, Mods::default()),
|
||||
(PointerKind::Up, Mods::default()),
|
||||
(PointerKind::DoubleClick, Mods::default()),
|
||||
(PointerKind::Down, shift),
|
||||
(PointerKind::Up, shift),
|
||||
(PointerKind::TripleClick, shift),
|
||||
] {
|
||||
apply(&mut sel, t.pointer(&cx, &ev(kind, 25.0, 25.0).with_mods(m)));
|
||||
}
|
||||
sel.sort_unstable();
|
||||
assert_eq!(sel, sorted(&[b, c]));
|
||||
// A Shift-click on C, then a click on A without Shift, which selects A alone, and Shift held
|
||||
// from the double-click on: the parts with A's fill are toggled from that selection.
|
||||
let mut sel = vec![];
|
||||
for (kind, x, m) in [
|
||||
(PointerKind::Down, 125.0, shift),
|
||||
(PointerKind::Up, 125.0, shift),
|
||||
(PointerKind::Down, 25.0, Mods::default()),
|
||||
(PointerKind::Up, 25.0, Mods::default()),
|
||||
(PointerKind::DoubleClick, 25.0, shift),
|
||||
(PointerKind::Down, 25.0, shift),
|
||||
(PointerKind::Up, 25.0, shift),
|
||||
(PointerKind::TripleClick, 25.0, shift),
|
||||
] {
|
||||
apply(&mut sel, t.pointer(&cx, &ev(kind, x, 25.0).with_mods(m)));
|
||||
}
|
||||
assert_eq!(sel, vec![c.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -188,6 +188,8 @@ fn snapped(d: Vec2, unsnapped: Vec2, shift: bool, guides: &[Overlay]) -> bool {
|
||||
pub struct DirectSelectionTool {
|
||||
group: bool,
|
||||
state: State,
|
||||
/// How far the move's last preview carried the anchors or the objects ([`Tool::readout`]).
|
||||
moved: Option<Vec2>,
|
||||
/// The spine point last clicked (blend, anchor): its handles show and can be dragged.
|
||||
spine: Option<(NodeId, usize)>,
|
||||
/// Other points of that spine picked with it (Shift-click adds or takes one away); dragging
|
||||
@@ -251,6 +253,7 @@ impl DirectSelectionTool {
|
||||
Self {
|
||||
group,
|
||||
state: State::Idle,
|
||||
moved: None,
|
||||
spine: None,
|
||||
spine_more: vec![],
|
||||
mesh: MeshEdit::default(),
|
||||
@@ -733,6 +736,10 @@ impl Tool for DirectSelectionTool {
|
||||
fn busy(&self) -> bool {
|
||||
!matches!(self.state, State::Idle) || self.guide.busy()
|
||||
}
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<crate::Readout> {
|
||||
let moving = matches!(self.state, State::MoveAnchors { began: true, .. } | State::MoveObject { began: true, .. });
|
||||
self.moved.filter(|_| moving).map(crate::Readout::drag)
|
||||
}
|
||||
fn options(&self) -> Value {
|
||||
self.mesh.options()
|
||||
}
|
||||
@@ -1486,6 +1493,7 @@ impl DirectSelectionTool {
|
||||
(d, self.guides) = (q - grab, guides);
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview("path.moveAnchors".into(), json!({"dx": d.x, "dy": d.y})));
|
||||
out
|
||||
}
|
||||
@@ -1538,6 +1546,7 @@ impl DirectSelectionTool {
|
||||
(d, self.guides) = (q - held, guides);
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview("path.moveAnchors".into(), json!({"dx": d.x, "dy": d.y})));
|
||||
return out;
|
||||
}
|
||||
@@ -1553,6 +1562,7 @@ impl DirectSelectionTool {
|
||||
(d, self.guides) = if ev.mods.shift { snap.snap_along(cx, hold, d, ev.mods.alt) } else { snap.snap(cx, hold, d, ev.mods.alt) };
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview(
|
||||
"object.transform".into(),
|
||||
json!({"matrix": matrix_json(vectorcraft_geom::Affine::translate(d)), "copy": ev.mods.alt}),
|
||||
|
||||
@@ -378,9 +378,11 @@ impl PerspectiveGrid {
|
||||
if (self.origin[1] - self.horizon).abs() < 1e-6 {
|
||||
return Err("ground level must differ from the horizon".into());
|
||||
}
|
||||
// A moved plane stays in front of the viewer.
|
||||
let behind = |p: Plane| self.axes(p).is_some_and(|[.., n]| 1.0 + self.offset(p) * self.axis_col(n)[2] <= 1e-9);
|
||||
if Plane::ALL.iter().any(|p| !self.offset(*p).is_finite() || self.offset(*p).abs() > MAX_DEPTH || behind(*p)) {
|
||||
// A moved plane stays in front of the viewer, both where Automatic Plane Positioning holds
|
||||
// it and in its own place, which it goes back to.
|
||||
let behind = |p: Plane, d: f64| self.axes(p).is_some_and(|[.., n]| 1.0 + d * self.axis_col(n)[2] <= 1e-9);
|
||||
let bad = |p: Plane, d: f64| !d.is_finite() || d.abs() > MAX_DEPTH || behind(p, d);
|
||||
if Plane::ALL.iter().any(|p| bad(*p, self.offset(*p)) || bad(*p, self.placed_offset(*p))) {
|
||||
return Err("a plane is moved too far (behind the viewer)".into());
|
||||
}
|
||||
self.validate_definition()
|
||||
|
||||
@@ -131,17 +131,32 @@ impl PerspectiveGrid {
|
||||
for plane in self.shown_planes() {
|
||||
let depth = self.offset(plane);
|
||||
let Some(h) = self.homography_at(plane, depth) else { continue };
|
||||
let Some(q) = h.inverse().and_then(|hi| hi.apply(p)) else { continue };
|
||||
let Some(hi) = h.inverse() else { continue };
|
||||
let Some(((su, nu), (sv, nv))) = self.line_steps(plane) else { continue };
|
||||
// The drawn lines nearest the pointer along each axis, and their neighbors: the
|
||||
// projection skews and shrinks the cells, so the intersection nearest on the page can be
|
||||
// one over from the nearest in the plane.
|
||||
let near = |v: f64, step: f64, last: usize| {
|
||||
let i = (v / step).round().clamp(0.0, last as f64) as i64;
|
||||
(i - 2..=i + 2).filter(move |k| (0..=last as i64).contains(k)).map(move |k| k as f64 * step)
|
||||
// On each drawn line, the intersections either side of the point of the line nearest
|
||||
// the pointer on the page. The projection keeps the order of points along a line, so
|
||||
// the nearest intersection on that line is one of those two, however skewed the cells.
|
||||
let mut candidates = Vec::with_capacity(2 * (nu + nv + 2));
|
||||
let mut along = |line: &dyn Fn(f64) -> Point, step: f64, last: usize, coord: fn(Point) -> f64| {
|
||||
let (Some(a), Some(b)) = (h.apply(line(0.0)), h.apply(line(last as f64 * step))) else { return };
|
||||
let ab = b - a;
|
||||
let len2 = ab.x * ab.x + ab.y * ab.y;
|
||||
let t = if len2 > 0.0 { (((p - a).x * ab.x + (p - a).y * ab.y) / len2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let Some(at) = hi.apply(a + ab * t).map(coord).filter(|v| v.is_finite()) else { return };
|
||||
let k = (at / step).floor().clamp(0.0, last as f64);
|
||||
for k in [k, (k + 1.0).min(last as f64)] {
|
||||
candidates.push(line(k * step));
|
||||
}
|
||||
};
|
||||
for (u, v) in near(q.x, su, nu).flat_map(|u| near(q.y, sv, nv).map(move |v| (u, v))) {
|
||||
let q = Point::new(u, v);
|
||||
for i in 0..=nu {
|
||||
let u = i as f64 * su;
|
||||
along(&|v| Point::new(u, v), sv, nv, |q| q.y);
|
||||
}
|
||||
for i in 0..=nv {
|
||||
let v = i as f64 * sv;
|
||||
along(&|u| Point::new(u, v), su, nu, |q| q.x);
|
||||
}
|
||||
for q in candidates {
|
||||
let Some(d) = h.apply(q).map(|at| at.distance(p)) else { continue };
|
||||
if d <= tol
|
||||
&& best.is_none_or(|b| d < b.0)
|
||||
@@ -268,6 +283,33 @@ mod tests {
|
||||
assert!(g.infer_at(&d, past, 3.0, PlaneTargets::ALL).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_held_plane_still_checks_its_own_place() {
|
||||
// Held at x = 0, the left wall's own place must stay in front of the viewer too: it goes
|
||||
// back there once positioning ends.
|
||||
let g = PerspectiveGrid { plane: Plane::Left, inferred: Some([0.0; 3]), ..grid() };
|
||||
assert!(g.merged(&json!({"leftOffset": -800})).is_err());
|
||||
assert!(PerspectiveGrid { inferred: None, ..g.clone() }.merged(&json!({"leftOffset": -800})).is_err());
|
||||
let moved = g.merged(&json!({"leftOffset": 40})).unwrap();
|
||||
assert_eq!((moved.placed_offset(Plane::Left), moved.offset(Plane::Left), moved.inferred), (40.0, 0.0, Some([0.0; 3])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_nearest_intersection_on_the_page_is_found_on_a_skewed_plane() {
|
||||
// The left vanishing point next to the origin skews the left wall's cells hard: the
|
||||
// intersection nearest on the page lies many cells from the pointer's inverse.
|
||||
let g = PerspectiveGrid { vp_left: 399.0, plane: Plane::Left, inferred: Some([0.0; 3]), ..grid() };
|
||||
let d = Document::new(800.0, 600.0);
|
||||
let gridlines = PlaneTargets { anchors: false, gridlines: true };
|
||||
let p = Point::new(400.0, 219.761194);
|
||||
let want = Point::new(10.0 * g.cell, 10.0 * g.cell);
|
||||
assert!((g.to_page(Plane::Left, want).unwrap() - p).hypot() < 0.5);
|
||||
let (_, at) = g.infer_at(&d, p, 6.0, gridlines).unwrap();
|
||||
for (got, want) in at.iter().zip([0.0, want.y, want.x]) {
|
||||
assert!((got - want).abs() < 1e-6, "{at:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hidden_path_inside_art_in_perspective_has_no_anchor_to_take() {
|
||||
let g = PerspectiveGrid { plane: Plane::Right, ..grid() };
|
||||
|
||||
@@ -57,10 +57,12 @@ pub use catalog::{TOOL_GROUPS, ToolInfo, tool_info};
|
||||
mod context;
|
||||
mod cursor;
|
||||
mod input;
|
||||
mod readout;
|
||||
|
||||
pub use context::{GlyphSnap, MarqueeMode, PaintDefaults, PixelOp, PixelSnap, ScreenFrame, ToolContext};
|
||||
pub use cursor::Cursor;
|
||||
pub use input::{Action, Mods, PenPose, PointerEvent, PointerKind, ToolKey};
|
||||
pub use readout::Readout;
|
||||
|
||||
/// Visual feedback drawn by the UI in screen space (coordinates here are document space).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -265,6 +267,12 @@ pub trait Tool: Send {
|
||||
fn wants_rehover(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// What the Info panel shows for the tool as it is now: the drag in progress (a move of the
|
||||
/// selection, a gradient vector, a Scale, Rotate, Reflect or Shear tool drag), or the
|
||||
/// Paintbrush's brush. None for a tool with nothing of its own to show.
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<Readout> {
|
||||
None
|
||||
}
|
||||
/// Called when the user switches away (finish pending work).
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
vec![]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! What the tool in use gives the Info panel to show ([`crate::Tool::readout`]).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
/// The Info panel's values for the tool in use. Lengths and points are in document units, and
|
||||
/// angles are in degrees as the matching command takes them: counter-clockwise for
|
||||
/// `object.rotate`'s `angle` and `object.reflect`'s `axis`, and `object.shear`'s `angle`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Readout {
|
||||
/// A drag that moves the selection or draws a gradient vector: how far it has gone in x and y.
|
||||
Drag { dx: f64, dy: f64 },
|
||||
/// A Scale tool drag: the horizontal and vertical scale (1 leaves the size as it is).
|
||||
Scale { sx: f64, sy: f64 },
|
||||
/// A Rotate tool drag: where the center of the selection's bounding box is, and the angle
|
||||
/// turned.
|
||||
Rotate { center: Point, angle: f64 },
|
||||
/// A Reflect tool drag: where the center of the selection's bounding box is, and the angle of
|
||||
/// the axis.
|
||||
Reflect { center: Point, angle: f64 },
|
||||
/// A Shear tool drag: where the center of the selection's bounding box is, the angle of the
|
||||
/// shear axis (0 horizontal, 90 vertical) and the shear angle.
|
||||
Shear { center: Point, axis: f64, angle: f64 },
|
||||
/// The Paintbrush: the brush it paints with, if any.
|
||||
Brush { name: Option<String> },
|
||||
}
|
||||
|
||||
impl Readout {
|
||||
/// The readout of a drag by `d`.
|
||||
pub fn drag(d: Vec2) -> Self {
|
||||
Readout::Drag { dx: d.x, dy: d.y }
|
||||
}
|
||||
|
||||
/// The length of a [`Readout::Drag`] and its angle, as the Measure tool gives them.
|
||||
pub fn distance_angle(&self) -> Option<(f64, f64)> {
|
||||
match *self {
|
||||
Readout::Drag { dx, dy } => {
|
||||
let (dist, angle, ..) = crate::xform::measure(Point::ORIGIN, Point::new(dx, dy));
|
||||
Some((dist, angle))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The readout as `info.readout` reports it.
|
||||
pub fn to_json(&self) -> Value {
|
||||
let at = |p: Point| json!([p.x, p.y]);
|
||||
match self {
|
||||
Readout::Drag { dx, dy } => {
|
||||
let (distance, angle) = self.distance_angle().unwrap_or_default();
|
||||
json!({ "kind": "drag", "dx": dx, "dy": dy, "distance": distance, "angle": angle })
|
||||
}
|
||||
Readout::Scale { sx, sy } => json!({ "kind": "scale", "width": sx * 100.0, "height": sy * 100.0 }),
|
||||
Readout::Rotate { center, angle } => json!({ "kind": "rotate", "center": at(*center), "angle": angle }),
|
||||
Readout::Reflect { center, angle } => json!({ "kind": "reflect", "center": at(*center), "angle": angle }),
|
||||
Readout::Shear { center, axis, angle } => json!({ "kind": "shear", "center": at(*center), "axis": axis, "angle": angle }),
|
||||
Readout::Brush { name } => json!({ "kind": "brush", "brush": name }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_drag_gives_its_length_and_its_angle_up_the_page() {
|
||||
let r = Readout::drag(Vec2::new(30.0, -40.0));
|
||||
let (d, a) = r.distance_angle().unwrap();
|
||||
assert!((d - 50.0).abs() < 1e-9 && (a - 53.13010235415598).abs() < 1e-9, "{d} {a}");
|
||||
assert_eq!(r.to_json()["dx"], 30.0);
|
||||
assert_eq!(Readout::Scale { sx: 1.5, sy: 0.5 }.to_json(), json!({"kind": "scale", "width": 150.0, "height": 50.0}));
|
||||
assert_eq!(Readout::Scale { sx: 1.0, sy: 1.0 }.distance_angle(), None);
|
||||
assert_eq!(Readout::Brush { name: None }.to_json(), json!({"kind": "brush", "brush": null}));
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,8 @@ pub struct SelectionTool {
|
||||
/// The params of the move's last preview: in Shaper's Construction mode the move ends with
|
||||
/// them and `releaseOutside` ([`construction`]).
|
||||
last_move: Option<Value>,
|
||||
/// How far the move's last preview carried the selection ([`Tool::readout`]).
|
||||
moved: Option<Vec2>,
|
||||
/// Held state used for the most recent bounding-box preview.
|
||||
last_backquote: bool,
|
||||
/// Pointer position used for the most recent bounding-box preview.
|
||||
@@ -898,6 +900,10 @@ impl Tool for SelectionTool {
|
||||
matches!(self.state, State::Moving { began: true, .. })
|
||||
}
|
||||
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<crate::Readout> {
|
||||
self.moved.filter(|_| self.moving()).map(crate::Readout::drag)
|
||||
}
|
||||
|
||||
fn transforming(&self) -> bool {
|
||||
match &self.state {
|
||||
State::Moving { began: true, .. } | State::Scaling { .. } | State::Rotating { .. } => true,
|
||||
@@ -1753,6 +1759,7 @@ impl SelectionTool {
|
||||
params["patterns"] = json!(true);
|
||||
}
|
||||
self.last_move = Some(params.clone());
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview("object.transform".into(), params));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -299,6 +299,9 @@ impl Tool for BrushPaintTool {
|
||||
fn busy(&self) -> bool {
|
||||
self.inner.busy()
|
||||
}
|
||||
fn readout(&self, cx: &ToolContext) -> Option<crate::Readout> {
|
||||
Some(crate::Readout::Brush { name: self.brush_name(cx) })
|
||||
}
|
||||
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
let acts = self.inner.deactivate(cx);
|
||||
self.map(cx, acts)
|
||||
|
||||
@@ -545,6 +545,12 @@ impl Tool for GradientTool {
|
||||
self.gesture.as_ref().is_some_and(|g| g.began) || self.free.as_ref().is_some_and(freeform::Gesture::began)
|
||||
}
|
||||
|
||||
/// The vector a drag on the art has drawn so far.
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<crate::Readout> {
|
||||
let (start, end) = self.gesture.as_ref().filter(|g| g.began)?.vector?;
|
||||
Some(crate::Readout::drag(end - start))
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
// A freeform gradient: its points instead of the bar.
|
||||
match (ev.kind, &mut self.free) {
|
||||
|
||||
@@ -20,7 +20,7 @@ use super::target_overlays;
|
||||
use crate::bbox::rotate_for_drag;
|
||||
use crate::guides::{PointSnap, Targets, snap_pick};
|
||||
use crate::select::{matrix_json, selection_bounds, selection_box};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Readout, Tool, ToolContext, ToolKey};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TransformKind {
|
||||
@@ -98,6 +98,8 @@ pub struct TransformTool {
|
||||
origin: Option<(Point, Vec<NodeId>)>,
|
||||
drag: Option<Drag>,
|
||||
measure: Option<(Point, String)>,
|
||||
/// What the Info panel shows for the drag in progress.
|
||||
readout: Option<Readout>,
|
||||
/// Snap feedback while hovering ("anchor" / "center" label), and for the dragged pointer.
|
||||
hover: Vec<Overlay>,
|
||||
/// What the dragged pointer snaps to, gathered when the drag begins.
|
||||
@@ -126,7 +128,7 @@ pub fn reflect_matrix(o: Point, theta: f64) -> Affine {
|
||||
|
||||
impl TransformTool {
|
||||
pub fn new(kind: TransformKind) -> Self {
|
||||
Self { kind, origin: None, drag: None, measure: None, hover: vec![], snap: None }
|
||||
Self { kind, origin: None, drag: None, measure: None, readout: None, hover: vec![], snap: None }
|
||||
}
|
||||
|
||||
/// The effective reference point: the custom one if it belongs to the current selection,
|
||||
@@ -154,12 +156,44 @@ impl TransformTool {
|
||||
/// and Shift's 45° steps start from the box's axes. A Reflect axis drawn by the drag is the
|
||||
/// line the pointer draws on the page.
|
||||
pub fn matrix_in(&self, angle: f64, o: Point, start: Point, p: Point, shift: bool, bounds: Rect) -> (Affine, String) {
|
||||
let (m, a, b) = self.solve(angle, o, start, p, shift, bounds);
|
||||
(m, self.label(a, b))
|
||||
}
|
||||
|
||||
/// The canvas label for the two values [`Self::solve`] gives.
|
||||
fn label(&self, a: f64, b: f64) -> String {
|
||||
match self.kind {
|
||||
TransformKind::Rotate => format!("{:.1}°", -a),
|
||||
TransformKind::Reflect => format!("{a:.1}°"),
|
||||
TransformKind::Scale => format!("W: {:.1}%\nH: {:.1}%", a * 100.0, b * 100.0),
|
||||
TransformKind::Shear => format!("{:.1}°", if a == 0.0 { b } else { -b }),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the Info panel shows for the transform `m` of a selection with `bounds`, with the two
|
||||
/// values [`Self::solve`] gives.
|
||||
fn readout_of(&self, m: Affine, a: f64, b: f64, bounds: Rect) -> Readout {
|
||||
let center = m * bounds.center();
|
||||
match self.kind {
|
||||
TransformKind::Rotate => Readout::Rotate { center, angle: a },
|
||||
TransformKind::Reflect => Readout::Reflect { center, angle: a },
|
||||
TransformKind::Scale => Readout::Scale { sx: a, sy: b },
|
||||
TransformKind::Shear => Readout::Shear { center, axis: a, angle: b },
|
||||
}
|
||||
}
|
||||
|
||||
/// [`TransformTool::matrix_in`]'s transform, with the two values of its readout: the angle
|
||||
/// turned, in degrees counter-clockwise as `object.rotate` takes it (Rotate), the angle of the
|
||||
/// axis in degrees counter-clockwise (Reflect), the scale along the box's width and height
|
||||
/// (Scale), or the angle of the shear axis (0 horizontal, 90 vertical) and the shear angle as
|
||||
/// `object.shear` takes it (Shear).
|
||||
fn solve(&self, angle: f64, o: Point, start: Point, p: Point, shift: bool, bounds: Rect) -> (Affine, f64, f64) {
|
||||
let frame = OrientedBox { angle, rect: Rect::ZERO };
|
||||
let local = |v: Vec2| frame.to_doc().inverse() * v.to_point();
|
||||
match self.kind {
|
||||
TransformKind::Rotate => {
|
||||
let (a, deg) = rotate_for_drag(o, start, p, shift);
|
||||
(a, format!("{:.1}°", -deg))
|
||||
(a, deg, 0.0)
|
||||
}
|
||||
TransformKind::Reflect => {
|
||||
let mut v = p - o;
|
||||
@@ -169,7 +203,7 @@ impl TransformTool {
|
||||
v = (frame.to_doc() * vectorcraft_geom::constrain_angle(local(v).to_vec2(), 45.0).to_point()).to_vec2();
|
||||
}
|
||||
let theta = v.y.atan2(v.x);
|
||||
(reflect_matrix(o, theta), format!("{:.1}°", vectorcraft_geom::normalize_deg(-theta.to_degrees())))
|
||||
(reflect_matrix(o, theta), vectorcraft_geom::normalize_deg(-theta.to_degrees()), 0.0)
|
||||
}
|
||||
TransformKind::Scale => {
|
||||
let d0 = local(start - o).to_vec2();
|
||||
@@ -184,7 +218,7 @@ impl TransformTool {
|
||||
}
|
||||
let clamp = |s: f64| if s.abs() < 1e-4 { 1e-4f64.copysign(s) } else { s };
|
||||
let (sx, sy) = (clamp(sx), clamp(sy));
|
||||
(about(o, frame.conjugate(Affine::scale_non_uniform(sx, sy))), format!("W: {:.1}%\nH: {:.1}%", sx * 100.0, sy * 100.0))
|
||||
(about(o, frame.conjugate(Affine::scale_non_uniform(sx, sy))), sx, sy)
|
||||
}
|
||||
TransformKind::Shear => {
|
||||
// The shear axis follows the dominant drag direction. Shift keeps the drag on that
|
||||
@@ -223,7 +257,7 @@ impl TransformTool {
|
||||
};
|
||||
}
|
||||
}
|
||||
(m, format!("{:.1}°", (-k.atan()).to_degrees()))
|
||||
if horizontal { (m, 0.0, (-k.atan()).to_degrees()) } else { (m, 90.0, k.atan().to_degrees()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +265,7 @@ impl TransformTool {
|
||||
/// Ends the drag, its snapping and readouts with it.
|
||||
fn end_drag(&mut self) -> Option<Drag> {
|
||||
self.measure = None;
|
||||
self.readout = None;
|
||||
self.snap = None;
|
||||
self.hover.clear();
|
||||
self.drag.take()
|
||||
@@ -250,6 +285,10 @@ impl Tool for TransformTool {
|
||||
self.drag.is_some_and(|d| d.began)
|
||||
}
|
||||
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<Readout> {
|
||||
self.readout.clone()
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
match ev.kind {
|
||||
@@ -294,10 +333,11 @@ impl Tool for TransformTool {
|
||||
}
|
||||
let (p, guides) = self.snap.as_ref().map_or((p, vec![]), |s| s.snap(cx, p));
|
||||
self.hover = guides;
|
||||
let (m, text) = self.matrix_in(d.angle_for(ev.mods.alt, ev.backquote), d.origin, d.pick, p, ev.mods.shift, d.bounds);
|
||||
let (m, a, b) = self.solve(d.angle_for(ev.mods.alt, ev.backquote), d.origin, d.pick, p, ev.mods.shift, d.bounds);
|
||||
(d.last, d.pointer, d.shift) = (m, p, ev.mods.shift);
|
||||
self.drag = Some(d);
|
||||
self.measure = Some((p, text));
|
||||
self.measure = Some((p, self.label(a, b)));
|
||||
self.readout = Some(self.readout_of(m, a, b, d.bounds));
|
||||
out.push(preview(m, ev.mods.alt, ev.backquote));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ pub(crate) use cut_edge::{continuous_tone, settle_cut_edges};
|
||||
pub use denoise::denoise;
|
||||
pub(crate) use denoise::denoise_keeping;
|
||||
pub use palette::FULL_TONE_MAX;
|
||||
use palette::{Bin, HistKey, TracedAs, Used, color_palette, flat_colors, kmeans_1d, labs, nearest_1d, nearest_lab, nearest_library};
|
||||
use palette::{Bin, HistKey, TracedAs, Used, color_palette, flat_count, flat_palette, kmeans_1d, labs, nearest_1d, nearest_lab, nearest_library};
|
||||
#[cfg(test)]
|
||||
pub(crate) use rare::kept_rare;
|
||||
use rare::{rare_colours, refine_around};
|
||||
@@ -126,12 +126,18 @@ pub(crate) fn quantize_keeping(img: &Raster, params: &TraceParams) -> (Quantized
|
||||
// Automatic on flat art: colours too rare for its count, but distinct and drawn as
|
||||
// regions of their own, are added (see `rare_colours`).
|
||||
let detail = (params.color_detail / 100.0).clamp(0.0, 1.0);
|
||||
let flat_auto = params.palette == Palette::Automatic && flat_colors(from, detail).is_some();
|
||||
let flat = if params.palette == Palette::Automatic { flat_count(from, detail) } else { None };
|
||||
let flat_auto = flat.is_some();
|
||||
let mut rare: Vec<[u8; 3]> = vec![];
|
||||
// The picture with `colors` colours asked for; fewer come back when it has fewer, or
|
||||
// when noise made several of one (see `merge_mixed`).
|
||||
let mut reduce = |colors: u32| {
|
||||
let mut centers = color_palette(from, &TraceParams { colors, ..params.clone() });
|
||||
// The picture in the colors `params` asks for, or in one color per clean level when
|
||||
// `levels` is given; fewer come back when it has fewer, or when noise made several of
|
||||
// one (see `merge_mixed`). Also whether `merge_mixed` merged any.
|
||||
let mut reduce = |levels: Option<usize>| {
|
||||
rare.clear();
|
||||
let mut centers = match levels {
|
||||
Some(n) => flat_palette(from, n),
|
||||
None => color_palette(from, params),
|
||||
};
|
||||
// The anti-aliasing beside a rare color along the cut edge, with the indexes of
|
||||
// that color and of the color on the other side of it (see `rare_colours`).
|
||||
let mut traced_as: Vec<TracedAs> = vec![];
|
||||
@@ -168,9 +174,24 @@ pub(crate) fn quantize_keeping(img: &Raster, params: &TraceParams) -> (Quantized
|
||||
let palette = centers.iter().map(|c| [c[0].round() as u8, c[1].round() as u8, c[2].round() as u8]).collect();
|
||||
let q = compact(labels, palette);
|
||||
// A swatch library's colours are used as they are.
|
||||
if library { q } else { merge_mixed(q, img.width as usize, img.height as usize, &rare) }
|
||||
if library {
|
||||
return (q, false);
|
||||
}
|
||||
let before = q.palette.len();
|
||||
let q = merge_mixed(q, img.width as usize, img.height as usize, &rare);
|
||||
let merged = q.palette.len() < before;
|
||||
(q, merged)
|
||||
};
|
||||
// Clean levels (see `flat_count`) each take a color unless `merge_mixed` finds two of
|
||||
// them mixed (dithering, or a thin region of a level beside a close one): such a
|
||||
// picture takes the colors median cut counts, with rare colors added.
|
||||
let q = match flat {
|
||||
Some((_, Some(levels))) => match reduce(Some(levels)) {
|
||||
(q, false) => q,
|
||||
(_, true) => reduce(None).0,
|
||||
},
|
||||
_ => reduce(None).0,
|
||||
};
|
||||
let q = reduce(params.colors);
|
||||
let q = if settles_wide(params) { settled_wide(q, img, &own, false) } else { settled(q, img, &own) };
|
||||
rare.retain(|c| q.palette.contains(c));
|
||||
(q, rare)
|
||||
|
||||
@@ -158,7 +158,7 @@ pub(super) fn color_palette(used: &[Used], params: &TraceParams) -> Vec<[f64; 3]
|
||||
Palette::Limited => limited(),
|
||||
Palette::FullTone => full_tone(),
|
||||
Palette::Automatic => match flat_colors(used, detail) {
|
||||
Some(n) => kmeans_rgb(used, n, 0.0),
|
||||
Some(n) => flat_palette(used, n),
|
||||
None => full_tone(),
|
||||
},
|
||||
Palette::DocumentLibrary if !params.swatches.is_empty() => library_colors(used, ¶ms.swatches, k),
|
||||
@@ -172,9 +172,20 @@ pub(super) fn color_palette(used: &[Used], params: &TraceParams) -> Vec<[f64; 3]
|
||||
/// noise aside): the bins holding 90% of the pixels are few, and each colour's pixels sit mostly
|
||||
/// in one bin. `detail` (0–1) lets smaller colour areas count as colours of their own.
|
||||
pub(super) fn flat_colors(used: &[Used], detail: f64) -> Option<usize> {
|
||||
flat_count(used, detail).map(|(n, _)| n)
|
||||
}
|
||||
|
||||
/// Automatic's palette of `n` colors for flat art.
|
||||
pub(super) fn flat_palette(used: &[Used], n: usize) -> Vec<[f64; 3]> {
|
||||
kmeans_rgb(used, n, 0.0)
|
||||
}
|
||||
|
||||
/// [`flat_colors`], and how many clean levels the picture is made of when it is made of them (see
|
||||
/// [`clean_levels`]).
|
||||
pub(super) fn flat_count(used: &[Used], detail: f64) -> Option<(usize, Option<usize>)> {
|
||||
let total: f64 = used.iter().map(|u| u.2).sum();
|
||||
if total <= 0.0 {
|
||||
return Some(2);
|
||||
return Some((2, None));
|
||||
}
|
||||
let mut by_count: Vec<&Used> = used.iter().collect();
|
||||
by_count.sort_by(|a, b| b.2.total_cmp(&a.2));
|
||||
@@ -195,12 +206,38 @@ pub(super) fn flat_colors(used: &[Used], detail: f64) -> Option<usize> {
|
||||
let colours: Vec<&ColorBox> = boxes.iter().filter(|b| b.weight >= min_share).collect();
|
||||
let weight: f64 = colours.iter().map(|b| b.weight).sum();
|
||||
let peaks: f64 = colours.iter().map(|b| b.peak).sum();
|
||||
(weight >= 0.9 * total && peaks >= 0.5 * weight && colours.len() <= 32).then_some(colours.len().max(2))
|
||||
if weight < 0.9 * total || peaks < 0.5 * weight || colours.len() > 32 {
|
||||
return None;
|
||||
}
|
||||
Some((colours.len().max(2), clean_levels(used, min_share)))
|
||||
}
|
||||
|
||||
/// The most histogram bins flat art's main colours spread over (a few bins per colour).
|
||||
const FLAT_BINS: usize = 96;
|
||||
|
||||
/// The most colors [`clean_levels`] counts.
|
||||
const CLEAN_LEVELS: usize = 16;
|
||||
|
||||
/// How far apart (levels of some channel) the mean colors of two bins must lie for
|
||||
/// [`clean_levels`] to count them as two colors.
|
||||
const LEVELS_APART: f64 = 12.0;
|
||||
|
||||
/// Flat art of 2 to [`CLEAN_LEVELS`] clean colors, one histogram bin each: the number of bins,
|
||||
/// when each holds at least `min_share` pixels and every two of them lie at least [`LEVELS_APART`]
|
||||
/// apart on some channel; otherwise `None`. Median cut keeps two colors less than 24 levels apart
|
||||
/// in one box, so evenly spaced levels 12 to 24 apart would otherwise count as fewer colors.
|
||||
/// Noise adds bins closer than that, or more than 16. An anti-aliased edge's blended pixels between
|
||||
/// colors more than 20 levels apart are not among `used` (see `own_colours`), and the steps of a
|
||||
/// blend between closer colors are less than [`LEVELS_APART`], so such pictures are counted by
|
||||
/// median cut.
|
||||
fn clean_levels(used: &[Used], min_share: f64) -> Option<usize> {
|
||||
if !(2..=CLEAN_LEVELS).contains(&used.len()) || used.iter().any(|u| u.2 < min_share) {
|
||||
return None;
|
||||
}
|
||||
let apart = |a: [f64; 3], b: [f64; 3]| (0..3).any(|ch| (a[ch] - b[ch]).abs() >= LEVELS_APART);
|
||||
used.iter().enumerate().all(|(i, a)| used[i + 1..].iter().all(|b| apart(a.1, b.1))).then_some(used.len())
|
||||
}
|
||||
|
||||
/// Document Library: the `k` swatches (at most) that the most pixels are nearest to (see
|
||||
/// [`nearest_library`]).
|
||||
fn library_colors(used: &[Used], swatches: &[[u8; 3]], k: usize) -> Vec<[f64; 3]> {
|
||||
@@ -438,6 +475,53 @@ mod tests {
|
||||
assert_eq!(palette(Palette::Automatic, 16), automatic);
|
||||
}
|
||||
|
||||
/// `colors`, each with `n` pixels, as histogram bins.
|
||||
fn bins(colors: impl IntoIterator<Item = ([f64; 3], f64)>) -> Vec<Used> {
|
||||
colors.into_iter().enumerate().map(|(i, (c, n))| (i, c, n)).collect()
|
||||
}
|
||||
|
||||
/// `levels` grays evenly spaced from black to white, 1,200 pixels each.
|
||||
fn gray_levels(levels: u32) -> Vec<([f64; 3], f64)> {
|
||||
(0..levels).map(|i| ([f64::from(i * 255 / (levels - 1)); 3], 1200.0)).collect()
|
||||
}
|
||||
|
||||
/// The clean levels `used` is made of, at the default Color Detail (see [`flat_count`]).
|
||||
fn clean(used: &[Used]) -> Option<usize> {
|
||||
flat_count(used, (TraceParams::default().color_detail / 100.0).clamp(0.0, 1.0)).and_then(|(_, levels)| levels)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_counts_every_clean_level_of_flat_art() {
|
||||
for levels in [2, 3, 10, 12, 14, 16] {
|
||||
assert_eq!(clean(&bins(gray_levels(levels))), Some(levels as usize), "{levels} grays");
|
||||
}
|
||||
// Sixteen sepia levels, about 13 apart on each channel.
|
||||
let sepia = (0..16u32).map(|i| {
|
||||
let t = f64::from(i) / 15.0;
|
||||
([45.0 + 205.0 * t, 28.0 + 207.0 * t, 15.0 + 190.0 * t].map(f64::round), 1200.0)
|
||||
});
|
||||
assert_eq!(clean(&bins(sepia)), Some(16), "16 sepia levels");
|
||||
// Levels 12 apart are the closest counted.
|
||||
let close = |apart: f64| bins((0..16).map(|i| ([40.0 + apart * f64::from(i); 3], 1200.0)));
|
||||
assert_eq!(clean(&close(12.0)), Some(16), "16 grays 12 apart");
|
||||
assert_eq!(clean(&close(11.0)), None, "16 grays 11 apart");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_and_anti_aliasing_add_no_clean_levels() {
|
||||
// Eight grays 34 apart, each with a second bin 6 levels off it holding as many pixels (noise).
|
||||
let noisy = gray_levels(8).into_iter().flat_map(|(c, n)| [(c, n), (c.map(|v| (v + 6.0).min(255.0)), n)]);
|
||||
assert_eq!(clean(&bins(noisy)), None, "eight noisy grays");
|
||||
// Eight grays with a few blended pixels halfway between each two (anti-aliasing).
|
||||
let levels = gray_levels(8);
|
||||
let blends = levels.windows(2).map(|w| ([(w[0].0[0] + w[1].0[0]) / 2.0; 3], 60.0));
|
||||
assert_eq!(clean(&bins(levels.iter().copied().chain(blends))), None, "eight anti-aliased grays");
|
||||
// Seventeen levels or more are counted by median cut.
|
||||
assert_eq!(clean(&bins((0..17).map(|i| ([15.0 * f64::from(i); 3], 1200.0)))), None, "17 grays");
|
||||
// A continuous-tone picture has no count at all.
|
||||
assert_eq!(clean(&ground_and_ramp()), None, "ground and ramp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dark_colored_figure_on_a_nearly_flat_dark_ground_keeps_its_color() {
|
||||
// Dark navy text (a few pixels) on the near-black ground: only cuts between two dark grays
|
||||
|
||||
@@ -2322,6 +2322,65 @@ fn thin_lines_on_flat_art_of_up_to_sixteen_clean_levels_are_traced() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_keeps_every_level_of_clean_flat_art_of_up_to_sixteen_levels() {
|
||||
let tints: [(&str, &[[f64; 3]]); 4] = [("gray", &GRAY_RAMP), ("sepia", &SEPIA), ("blue", &BLUE), ("duotone", &DUOTONE)];
|
||||
let params = TraceParams { mode: Mode::Color, palette: Palette::Automatic, ..TraceParams::default() };
|
||||
for (name, stops) in tints {
|
||||
for levels in [2, 10, 12, 14, 16] {
|
||||
let bands = tinted(&even_gray_bands(levels), stops, 0.0);
|
||||
let res = trace(&bands, ¶ms);
|
||||
assert_eq!(res.palette.len(), levels as usize, "{levels} {name} bands: {:?}", res.palette);
|
||||
for band in 0..levels {
|
||||
let i = ((band * 20 + 10) * 4) as usize;
|
||||
let want = [bands.rgba[i], bands.rgba[i + 1], bands.rgba[i + 2]];
|
||||
assert!(
|
||||
res.palette.iter().any(|c| (0..3).all(|k| c[k].abs_diff(want[k]) <= 1)),
|
||||
"{levels} {name} bands: no {want:?} in {:?}",
|
||||
res.palette
|
||||
);
|
||||
}
|
||||
assert_eq!(res.paths.len(), levels as usize, "{levels} {name} bands");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_counts_no_anti_aliased_edge_of_clean_levels_as_a_level() {
|
||||
// Four gray bands 10 px wide, `apart` levels apart, each edge blended over `width` px: every
|
||||
// blended column holds 2.5 in 100 of the pixels, more than a color needs to be counted.
|
||||
let params = TraceParams { mode: Mode::Color, palette: Palette::Automatic, ..TraceParams::default() };
|
||||
for apart in [24.0, 40.0, 64.0] {
|
||||
for width in 1..=4u32 {
|
||||
let level = |b: u32| 20.0 + apart * f64::from(b);
|
||||
let img = Raster::from_fn(40, 60, |x, _| {
|
||||
let (b, j) = (x / 10, x % 10);
|
||||
let v = if b > 0 && j < width { level(b - 1) + apart * f64::from(j + 1) / f64::from(width + 1) } else { level(b) };
|
||||
let v = v.round() as u8;
|
||||
[v, v, v, 255]
|
||||
});
|
||||
let res = trace(&img, ¶ms);
|
||||
assert_eq!(res.palette.len(), 4, "levels {apart} apart, edges {width} px: {:?}", res.palette);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_merges_a_dithered_close_level_as_noise() {
|
||||
// A gray ground with 5 to 20 in 100 of its pixels, scattered, 12 levels lighter: two clean
|
||||
// levels by their histogram, but mixed through each other, so they trace as one color.
|
||||
let params = TraceParams { mode: Mode::Color, palette: Palette::Automatic, ..TraceParams::default() };
|
||||
let hash = |x: u32, y: u32| (x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663)).wrapping_mul(2_654_435_761);
|
||||
for percent in [5, 10, 20] {
|
||||
let img = Raster::from_fn(120, 120, |x, y| {
|
||||
let v = if hash(x, y) % 100 < percent { 112 } else { 100 };
|
||||
[v, v, v, 255]
|
||||
});
|
||||
let res = trace(&img, ¶ms);
|
||||
assert_eq!(res.palette.len(), 1, "{percent} in 100 dithered: {:?}", res.palette);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pictures_without_tones_are_not_continuous_tone() {
|
||||
let photo = tinted(&photo_scene(6.0, true), &SEPIA, 2.0);
|
||||
|
||||
@@ -56,6 +56,10 @@ pub struct Job {
|
||||
pub progress: Option<Arc<Progress>>,
|
||||
/// Continue in Background: the progress dialog no longer shows this job.
|
||||
pub hidden: bool,
|
||||
/// [`crate::state::UiState::status_sets`] before the job set its status line.
|
||||
sets: u64,
|
||||
/// The status line the job set ("<label>…"); empty when it set none.
|
||||
line: String,
|
||||
then: Then,
|
||||
}
|
||||
|
||||
@@ -167,13 +171,13 @@ pub fn run_with_progress(
|
||||
if let Some(tx) = app.background.worker() {
|
||||
match tx.send(Task { id, work, write, scope: ScopeHandle::capture() }) {
|
||||
Ok(()) => {
|
||||
let (sets, status) = (app.ui.status_sets, if label.is_empty() { String::new() } else { format!("{label}…") });
|
||||
let bg = &mut app.background;
|
||||
bg.next_id += 1;
|
||||
bg.jobs.push(Job { id, label: label.clone(), doc, progress, hidden: false, then: Box::new(then) });
|
||||
bg.jobs.push(Job { id, label: label.clone(), doc, progress, hidden: false, sets, line: status.clone(), then: Box::new(then) });
|
||||
if label.is_empty() {
|
||||
return Ok(json!({ "background": true }));
|
||||
}
|
||||
let status = format!("{label}…");
|
||||
app.status(status.clone());
|
||||
return Ok(json!({ "background": true, "status": status }));
|
||||
}
|
||||
@@ -250,9 +254,27 @@ fn finish(app: &mut VectorcraftApp, (id, r): Done) {
|
||||
apply(app, job, r);
|
||||
}
|
||||
|
||||
/// Hand a job its result; a failure goes to the status bar (the call that started it returned).
|
||||
/// Does the status bar still show what a job left there? No line has been set since `sets`
|
||||
/// ([`crate::state::UiState::status_sets`] when the job started), or the one line set since is
|
||||
/// the job's own `line` ("<label>…"; empty for a job that set none) and it is still shown. A job's
|
||||
/// result goes to the status bar only then: a line set since (by another command or another job)
|
||||
/// stays, also when it has the same text as an earlier one.
|
||||
pub(crate) fn status_unchanged(app: &VectorcraftApp, sets: u64, line: &str) -> bool {
|
||||
let now = app.ui.status_sets;
|
||||
now == sets || (!line.is_empty() && now == sets.wrapping_add(1) && app.ui.status == line)
|
||||
}
|
||||
|
||||
/// Hand a job its result; a failure goes to the status bar (the call that started it returned)
|
||||
/// while the status bar still shows the job's own line ([`status_unchanged`]) and the job's
|
||||
/// callback set no line of its own.
|
||||
fn apply(app: &mut VectorcraftApp, job: Job, r: Result<Value, String>) {
|
||||
if let Err(e) = (job.then)(app, r) {
|
||||
app.status(format!("{} failed: {e}", job.label));
|
||||
let Job { label, sets, line, then, .. } = job;
|
||||
let shown = status_unchanged(app, sets, &line);
|
||||
let before = app.ui.status_sets;
|
||||
if let Err(e) = then(app, r)
|
||||
&& shown
|
||||
&& app.ui.status_sets == before
|
||||
{
|
||||
app.status(format!("{label} failed: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,8 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo
|
||||
drag_released(app, ui, &f, d, release_pos(ui, button).unwrap_or(p));
|
||||
}
|
||||
} else if let Some(p) = hover_tool
|
||||
// A press on an artboard's name starts no drag: the moves while it is held are not hover.
|
||||
&& !(pointer.primary_down() && ui.data(|d| d.get_temp::<(usize, bool)>(name_press_id()).is_some()))
|
||||
&& (pointer.is_moving() || mods_changed || !was_over || tool_switched || (context_changed && app.session.tool_wants_rehover()))
|
||||
{
|
||||
let ev = PointerEvent {
|
||||
|
||||
@@ -1027,6 +1027,49 @@ fn shift_clicking_canvas_artboard_label_toggles_multi_selection() {
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([2]), "Plain click selects that artboard alone");
|
||||
}
|
||||
|
||||
/// A press on an artboard's name starts no drag. Moving with the button still held is not hover,
|
||||
/// so Shift over a gridline intersection does not position the Perspective Selection tool's plane
|
||||
/// until the button is up.
|
||||
#[test]
|
||||
fn moves_while_an_artboard_name_is_pressed_do_not_hover_the_tool() {
|
||||
use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300, "artboards": 2})).unwrap();
|
||||
app.select_tool("perspectiveSelection");
|
||||
app.session.execute("perspective.grid.show", &json!({"visible": true})).unwrap();
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.center = Point::new(200.0, 150.0);
|
||||
v.zoom = 1.0;
|
||||
v.fitted = true;
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let drawn = shapes(&mut app, &ctx);
|
||||
let label = drawn
|
||||
.iter()
|
||||
.find_map(|s| match s {
|
||||
Shape::Text(t) if t.galley.text() == "01 - Artboard 1" => Some(t.pos + vec2(10.0, 5.0)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let xf = Xf::new(rect, app.view().unwrap());
|
||||
let doc = |app: &VectorcraftApp| app.session.active().unwrap().doc.clone();
|
||||
let g = PerspectiveGrid::current(&doc(&app));
|
||||
let target = xf.to_screen(g.to_page(Plane::Left, Point::new(2.0 * g.cell, 3.0 * g.cell)).unwrap());
|
||||
assert!(rect.contains(label) && rect.contains(target), "{label:?} {target:?} {rect:?}");
|
||||
|
||||
let shift = egui::Modifiers::SHIFT;
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: shift };
|
||||
frame(&mut app, &ctx, vec![egui::Event::ModifiersChanged(shift), egui::Event::PointerMoved(label), button(label, true)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(target)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(target + vec2(0.5, 0.0))]);
|
||||
assert!(!PerspectiveGrid::positioning(&doc(&app)), "the button is still down");
|
||||
frame(&mut app, &ctx, vec![button(target, false)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(target)]);
|
||||
assert!(PerspectiveGrid::positioning(&doc(&app)), "with the button up, Shift over the intersection positions the plane");
|
||||
}
|
||||
|
||||
/// Enable Touch Gestures (#585): a two-finger tap undoes and a three-finger one redoes, and the
|
||||
/// first finger's press (the pointer egui makes of it) draws nothing. Off, a tap does nothing.
|
||||
#[test]
|
||||
|
||||
@@ -56,6 +56,7 @@ pub mod freehand;
|
||||
mod gradient_stop;
|
||||
pub mod graph_data;
|
||||
pub mod graph_design;
|
||||
pub mod graph_type;
|
||||
pub mod graphic_style_options;
|
||||
pub mod halftone;
|
||||
pub mod hyphenation;
|
||||
@@ -342,6 +343,7 @@ registry! {
|
||||
GraphDesign: [graph_design::DESIGN] => graph_design::DESIGN_SPEC,
|
||||
GraphColumn: [graph_design::COLUMN] => graph_design::COLUMN_SPEC,
|
||||
GraphMarker: [graph_design::MARKER] => graph_design::MARKER_SPEC,
|
||||
GraphType: [graph_type::KIND] => graph_type::SPEC,
|
||||
GradientStop: ["gradientStop"] => gradient_stop::SPEC,
|
||||
ColorPicker: [color_picker::KIND] => color_picker::SPEC,
|
||||
GraphicStyleOptions: [graphic_style_options::KIND] => graphic_style_options::SPEC,
|
||||
@@ -518,6 +520,8 @@ fn hidden_controls(d: &Dialog) -> &'static [&'static str] {
|
||||
Some(DialogKind::FileInfo) => &["__keyword"],
|
||||
// The preset name being typed.
|
||||
Some(DialogKind::FlattenTransparency) => &["__presetName"],
|
||||
// The page shown.
|
||||
Some(DialogKind::GraphType) => &[graph_type::PAGE],
|
||||
// The Resolution menu's Other.
|
||||
Some(DialogKind::RasterOptions | DialogKind::RasterFormatOptions | DialogKind::PsdOptions) => &["__otherPpi"],
|
||||
// The category shown, the search text and the three settings kept outside the preferences.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! The generic parameter dialog (`ui.paramDialog`): edits a command's parameters (`__command`,
|
||||
//! headed `__label`) and runs it on OK, with the fixed ones in `__params` (not shown) added.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{DialogSpec, form};
|
||||
@@ -176,12 +174,6 @@ fn choices(command: &str, key: &str) -> Option<form::Choices> {
|
||||
("text.areaOptions", "firstBaseline") => Some(FIRST_BASELINE),
|
||||
("text.areaOptions", "verticalAlign") => Some(VERTICAL_ALIGN),
|
||||
(crate::addons::cutting::WEED, "direction") => Some(crate::addons::cutting::WEED_DIRECTIONS),
|
||||
("graph.setType", "type") => Some(GRAPH_KINDS.as_slice()),
|
||||
("graph.setType", "valueAxis") => Some(GRAPH_VALUE_AXIS),
|
||||
("graph.setType", "tickLength" | "rightTickLength" | "categoryTickLength") => Some(GRAPH_TICK_LENGTH),
|
||||
("graph.setType", "pieLegend") => Some(PIE_LEGEND),
|
||||
("graph.setType", "piePosition") => Some(PIE_POSITION),
|
||||
("graph.setType", "pieSort") => Some(PIE_SORT),
|
||||
(crate::addons::image::REMOVE_BACKGROUND | crate::addons::image::COLOR_MASK, "background") => Some(crate::addons::image::BACKGROUNDS),
|
||||
(crate::addons::image::REMOVE_BACKGROUND | crate::addons::image::COLOR_MASK, "match") => Some(crate::addons::image::MATCHES),
|
||||
_ => None,
|
||||
@@ -214,21 +206,6 @@ const FIRST_BASELINE: form::Choices =
|
||||
/// Area Type Options › Align (vertical alignment of the lines in each row/column).
|
||||
const VERTICAL_ALIGN: form::Choices = &[("Top", "top"), ("Center", "center"), ("Bottom", "bottom"), ("Justify", "justify")];
|
||||
|
||||
/// Graph Type › Type: the nine graph kinds, in the Graph tool group's order.
|
||||
static GRAPH_KINDS: LazyLock<Vec<(&'static str, &'static str)>> =
|
||||
LazyLock::new(|| vectorcraft_doc::GraphKind::ALL.iter().map(|k| (k.label(), k.id())).collect());
|
||||
|
||||
/// Graph Type › Value Axis (series picked on both axes show none, and OK leaves each where it is).
|
||||
const GRAPH_VALUE_AXIS: form::Choices = &[("On Left Side", "left"), ("On Right Side", "right"), ("On Both Sides", "both")];
|
||||
|
||||
/// Graph Type › Legend, Position and Sort for pie graphs.
|
||||
const PIE_LEGEND: form::Choices = &[("No Legend", "none"), ("Standard Legend", "standard"), ("Legends in Wedges", "wedges")];
|
||||
const PIE_POSITION: form::Choices = &[("Ratio", "ratio"), ("Even", "even"), ("Stacked", "stacked")];
|
||||
const PIE_SORT: form::Choices = &[("All", "all"), ("First", "first"), ("None", "none")];
|
||||
|
||||
/// Graph Type › Tick Marks › Length.
|
||||
const GRAPH_TICK_LENGTH: form::Choices = &[("None", "none"), ("Short", "short"), ("Full Width", "full")];
|
||||
|
||||
/// Closes before running, so a dialog the command opens stays open. A previewed one keeps its
|
||||
/// preview as the one undo step.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
@@ -405,42 +382,6 @@ mod tests {
|
||||
assert!(p[0] == p[1] && p[1] == p[2] && p[0] < 128, "red in grayscale: {p:?}");
|
||||
}
|
||||
|
||||
/// Graph Type › Value Axis is a dropdown of the values `graph.setType` takes, and OK puts the
|
||||
/// value axis on the side picked.
|
||||
#[test]
|
||||
fn graph_type_value_axis_is_a_dropdown_of_its_sides() {
|
||||
let doc = vectorcraft_engine::find_command("graph.setType").unwrap().params;
|
||||
let values: Vec<&str> = choices("graph.setType", "valueAxis").unwrap().iter().map(|(_, v)| *v).collect();
|
||||
assert!(doc.contains(&format!("valueAxis?: {}", values.join("|"))), "{values:?}");
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
app.run("graph.create", json!({"type": "column", "x": 0, "y": 0, "width": 200, "height": 150})).unwrap();
|
||||
crate::menus::graph_dialog(&mut app, "graph.setType").unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("valueAxis"), "left");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("valueAxis".into(), json!("both"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.execute("graph.setType", &json!({})).unwrap()["valueAxis"], "both");
|
||||
}
|
||||
|
||||
/// Graph Type › Type is a dropdown of the nine graph kinds (#2175), and OK changes the graph's
|
||||
/// type and keeps its data.
|
||||
#[test]
|
||||
fn graph_type_is_a_dropdown_of_the_graph_kinds() {
|
||||
let kinds: Vec<(&str, &str)> = vectorcraft_doc::GraphKind::ALL.iter().map(|k| (k.label(), k.id())).collect();
|
||||
assert_eq!(choices("graph.setType", "type").unwrap().to_vec(), kinds);
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
app.run("graph.create", json!({"type": "column", "x": 0, "y": 0, "width": 200, "height": 150, "csv": ",a,b\nQ1,1,2\nQ2,3,4"})).unwrap();
|
||||
let data = app.session.execute("graph.setData", &json!({})).unwrap();
|
||||
assert_eq!(data["rows"], json!([[1.0, 2.0], [3.0, 4.0]]));
|
||||
crate::menus::graph_dialog(&mut app, "graph.setType").unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("type"), "column");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("type".into(), json!("stackedBar"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.execute("graph.setType", &json!({})).unwrap()["type"], "stackedBar");
|
||||
assert_eq!(app.session.execute("graph.setData", &json!({})).unwrap(), data);
|
||||
}
|
||||
|
||||
/// A document with a rectangle made a 2 x 2 Grid Repeat and selected, its Repeat Options
|
||||
/// dialog open.
|
||||
fn grid_options_app() -> VectorcraftApp {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@
|
||||
//! whose OK sets it in the model it was picked in), the color mode menu and its sliders (HSB, RGB,
|
||||
//! CMYK or Lab, and Tint when the new color is a spot color of the library), which edit the
|
||||
//! first selected row's new color (once they have, OK gives the art each new color in its own
|
||||
//! model), Merge and Separate, Randomize order or
|
||||
//! model), New Row (a row with no current colors above the first selected row, or at the top,
|
||||
//! with that row's new color; it recolors nothing until Merge moves colors into it), Merge and
|
||||
//! Separate, Randomize order or
|
||||
//! saturation and brightness, Reset, the Method and Limit to Library (its folder button imports a
|
||||
//! library file and limits to it; a library's colours apply as its swatches: a spot colour stays a
|
||||
//! spot colour, and joins the document's swatches). Edit tab: a hue and
|
||||
@@ -566,9 +568,11 @@ fn selection(d: &Dialog) -> Vec<usize> {
|
||||
fn apply_params(d: &Dialog) -> Value {
|
||||
let rs = rows(d);
|
||||
let group = d.fields.get("group").and_then(Value::as_str).map(|g| {
|
||||
let rest =
|
||||
d.fields.get("__assign").and_then(Value::as_array).filter(|_| d.bool("__art")).map_or(&[][..], |a| a.get(rs.len()..).unwrap_or_default());
|
||||
let colors: Vec<&Value> = rs.iter().map(|r| &r["to"]).chain(rest).collect();
|
||||
// A row made with New Row that holds no current colors gives the group no color.
|
||||
let filled: Vec<&Value> = rs.iter().filter(|r| !r["from"].as_array().is_some_and(Vec::is_empty)).map(|r| &r["to"]).collect();
|
||||
let assign = d.fields.get("__assign").and_then(Value::as_array).filter(|_| d.bool("__art"));
|
||||
let rest = assign.map_or(&[][..], |a| a.get(filled.len()..).unwrap_or_default());
|
||||
let colors: Vec<&Value> = filled.into_iter().chain(rest).collect();
|
||||
let name = d.str("groupName");
|
||||
let rename = (!name.trim().is_empty() && name != g).then_some(name);
|
||||
json!({"group": g, "groupColors": colors, "rename": rename})
|
||||
@@ -778,6 +782,10 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
let x = cur.left() + j as f32 * w;
|
||||
chip(ui, Rect::from_min_max(pos2(x, cur.top()), pos2(x + w - if w > 4.0 { 2.0 } else { 0.0 }, cur.bottom())), *c, false);
|
||||
}
|
||||
// A row made with New Row shows an empty slot until colors are merged into it.
|
||||
if from.is_empty() {
|
||||
ui.painter().rect_stroke(cur, 1.0, egui::Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
}
|
||||
if cr.clicked() {
|
||||
clicked = Some(i);
|
||||
}
|
||||
@@ -826,6 +834,14 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
// New Row is available only with art.
|
||||
if widgets::icon_button_enabled(ui, "plus", tl!("New Row"), false, d.bool("__art"), 24.0).clicked() {
|
||||
let at = new_row(rows, &sel);
|
||||
d.fields.insert("__sel".into(), json!([at]));
|
||||
// The slider edit was kept for a row index that now names another row.
|
||||
d.fields.remove(COMPS);
|
||||
changed = true;
|
||||
}
|
||||
let can_merge = sel.len() > 1;
|
||||
if widgets::icon_button_enabled(ui, "combine", tl!("Merge colors into one row"), false, can_merge, 24.0).clicked() {
|
||||
merge(rows, &sel);
|
||||
@@ -1218,6 +1234,16 @@ fn separate(rows: &mut Vec<Value>, sel: &[usize]) {
|
||||
*rows = out;
|
||||
}
|
||||
|
||||
/// New Row: insert a row with no current colors above the first selected row, or at the top
|
||||
/// when none is selected, and return its index. Its new color is the new color of the row it goes
|
||||
/// above (black without one). It recolors nothing until current colors are merged into it.
|
||||
fn new_row(rows: &mut Vec<Value>, sel: &[usize]) -> usize {
|
||||
let at = sel.iter().copied().filter(|i| *i < rows.len()).min().unwrap_or(0);
|
||||
let to = rows.get(at).and_then(to_color).unwrap_or_default();
|
||||
rows.insert(at, json!({"from": [], "to": key(&to)}));
|
||||
at
|
||||
}
|
||||
|
||||
/// The rows the Edit tab shows: those with a new colour, not excluded.
|
||||
fn active_rows(rows: &[Value]) -> Vec<usize> {
|
||||
(0..rows.len()).filter(|i| !excluded(&rows[*i]) && to_color(&rows[*i]).is_some()).collect()
|
||||
@@ -1540,6 +1566,35 @@ mod tests {
|
||||
assert_eq!(r.len(), 2);
|
||||
}
|
||||
|
||||
/// New Row goes above the first selected row with that row's new color, or at the top; it has
|
||||
/// no current colors, and a merge with it as the first selected row fills it.
|
||||
#[test]
|
||||
fn a_new_row_goes_above_the_selection_and_takes_merged_colors() {
|
||||
let mut r = rows();
|
||||
assert_eq!(new_row(&mut r, &[2, 1]), 1);
|
||||
assert_eq!(r.len(), 4);
|
||||
assert_eq!(r[1], json!({"from": [], "to": "rgb 0 0 255"}));
|
||||
assert_eq!(r[2]["from"], json!(["rgb 0 0 255", "rgb 0 0 128"]), "the selected row moved down");
|
||||
merge(&mut r, &[1, 3]);
|
||||
assert_eq!(r[1], json!({"from": ["rgb 0 255 0"], "to": "rgb 0 0 255"}));
|
||||
assert_eq!(new_row(&mut r, &[]), 0);
|
||||
assert_eq!(r[0], json!({"from": [], "to": "rgb 255 0 0"}));
|
||||
let mut none = vec![];
|
||||
assert_eq!(new_row(&mut none, &[5]), 0);
|
||||
assert_eq!(none, vec![json!({"from": [], "to": key(&Color::BLACK)})]);
|
||||
}
|
||||
|
||||
/// Opened on a color group, OK rewrites it with the new colors of the rows that hold current
|
||||
/// colors, then the group's colors no such row took: an empty row gives it no color.
|
||||
#[test]
|
||||
fn an_empty_row_gives_the_group_no_color() {
|
||||
let mut r = rows();
|
||||
new_row(&mut r, &[]);
|
||||
let fields = json!({"rows": r, "group": "Trio", "__art": true, "__assign": ["rgb 255 0 0", "rgb 0 0 255", "rgb 0 255 0", "rgb 9 9 9"]});
|
||||
let p = apply_params(&Dialog::new("recolor", fields));
|
||||
assert_eq!(p["groupColors"], json!(["rgb 255 0 0", "rgb 0 0 255", "rgb 0 255 0", "rgb 9 9 9"]));
|
||||
}
|
||||
|
||||
/// Randomize after a seed past `u64::MAX` keeps the largest seed.
|
||||
#[test]
|
||||
fn next_seed_holds_at_the_largest() {
|
||||
@@ -1825,6 +1880,35 @@ mod tests {
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo, "the art is unchanged: no undo step");
|
||||
}
|
||||
|
||||
/// A row made with New Row recolors nothing while it is empty, and OK keeps it; the blue art's
|
||||
/// color merged into it takes its new color, in the preview and at OK.
|
||||
#[test]
|
||||
fn colors_merged_into_a_new_row_take_its_new_color() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
let [red, blue] = theme_setup(&mut app, &ctx);
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let set_rows = |app: &mut VectorcraftApp, rs: Vec<Value>| {
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("rows".into(), Value::Array(rs));
|
||||
};
|
||||
let mut rs = super::rows(app.ui.dialog.as_ref().unwrap());
|
||||
let at = new_row(&mut rs, &[]);
|
||||
rs[at]["to"] = json!("rgb 0 255 0");
|
||||
set_rows(&mut app, rs.clone());
|
||||
frame(&mut app, &ctx, 0.1, vec![], 0);
|
||||
assert_eq!((fill_hex(&app, red), fill_hex(&app, blue)), ("#ff0000".into(), "#0000ff".into()), "an empty row recolors nothing");
|
||||
let b = rs.iter().position(|r| r["from"] == json!([BLUE])).unwrap();
|
||||
merge(&mut rs, &[at, b]);
|
||||
// A second empty row stays empty and is sent at OK too.
|
||||
new_row(&mut rs, &[]);
|
||||
set_rows(&mut app, rs);
|
||||
frame(&mut app, &ctx, 0.2, vec![], 0);
|
||||
assert_eq!(fill_hex(&app, blue), "#00ff00", "the art previews the merge");
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!((fill_hex(&app, red), fill_hex(&app, blue)), ("#ff0000".into(), "#00ff00".into()));
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
}
|
||||
|
||||
const RED: &str = "rgb 255 0 0";
|
||||
const PURPLE: &str = "rgb 128 0 204";
|
||||
const BLUE: &str = "rgb 0 0 255";
|
||||
|
||||
@@ -37,10 +37,12 @@ pub(super) fn body(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let id = d.str("__effect");
|
||||
let defaults = vectorcraft_effects::default_params(&id).unwrap_or(json!({}));
|
||||
// Reset all resets the shape, view and light; the mapped graphics stay.
|
||||
// Reset all resets the shape, view and light; the mapped graphics and the render
|
||||
// settings stay.
|
||||
let defaults = match defaults {
|
||||
Value::Object(mut m) => {
|
||||
m.remove(vectorcraft_effects::GRAPHICS);
|
||||
m.remove(vectorcraft_effects::RENDER);
|
||||
Value::Object(m)
|
||||
}
|
||||
other => other,
|
||||
|
||||
@@ -1154,6 +1154,7 @@ font-technology Variable Променливи
|
||||
Rotate (Classic)… Ротация (класическа)…
|
||||
Surface Повърхност
|
||||
Wireframe Телена рамка
|
||||
Render Settings Настройки за рендериране
|
||||
No Shading Без засенчване
|
||||
Plastic Пластмаса
|
||||
Highlight Intensity Интензивност на отблясъка
|
||||
@@ -1368,6 +1369,40 @@ font-technology Variable Променливи
|
||||
Graph Диаграма
|
||||
Graph Data Данни за диаграма
|
||||
Graph Type Тип диаграма
|
||||
Graph Options Опции на диаграмата
|
||||
Left Axis Лява ос
|
||||
Right Axis Дясна ос
|
||||
Category Axis Ос на категориите
|
||||
Separate Scales Отделни скали
|
||||
Add Drop Shadow Добавяне на падаща сянка
|
||||
Add Legend Across Top Добавяне на легенда отгоре
|
||||
First Row in Front Първият ред отпред
|
||||
First Column in Front Първата колона отпред
|
||||
Legend Легенда
|
||||
Column Width Ширина на колоната
|
||||
Bar Width Ширина на стълба
|
||||
Cluster Width Ширина на групата
|
||||
Mark Data Points Маркиране на точките от данни
|
||||
Connect Data Points Свързване на точките от данни
|
||||
Edge-to-Edge Lines Линии от край до край
|
||||
Draw Filled Lines Чертане на запълнени линии
|
||||
Tick Values Стойности на отметките
|
||||
Override Calculated Values Замяна на изчислените стойности
|
||||
Divisions Деления
|
||||
Tick Marks Отметки
|
||||
Tick Marks per Division Отметки на деление
|
||||
Draw Tick Marks Between Labels Чертане на отметки между етикетите
|
||||
Add Labels Добавяне на етикети
|
||||
Maximum must be more than Minimum. Максимумът трябва да е по-голям от минимума.
|
||||
Column Graph Колонна диаграма
|
||||
Stacked Column Graph Колонна диаграма с натрупване
|
||||
Bar Graph Стълбчеста диаграма
|
||||
Stacked Bar Graph Стълбчеста диаграма с натрупване
|
||||
Line Graph Линейна диаграма
|
||||
Area Graph Площна диаграма
|
||||
Scatter Graph Диаграма на разсейване
|
||||
Pie Graph Кръгова диаграма
|
||||
Radar Graph Радарна диаграма
|
||||
Graph Design Дизайн на диаграма
|
||||
Graph Marker Маркер на диаграма
|
||||
New Design Нов дизайн
|
||||
@@ -1487,6 +1522,7 @@ font-technology Variable Променливи
|
||||
Highlight anchors on mouse over Маркиране на опорните точки при задържане на мишката
|
||||
Highlight Rows Маркиране на редовете
|
||||
Highlight Variable Маркиране на променливи
|
||||
Sort Variables Сортиране на променливите
|
||||
Highlight Substituted Fonts Маркиране на заменени шрифтове
|
||||
Highlight Substituted Glyphs Маркиране на заменени глифи
|
||||
History История
|
||||
@@ -1949,6 +1985,7 @@ font-technology Variable Променливи
|
||||
Menu Dialog Диалогов прозорец на менюто
|
||||
Merge Сливане
|
||||
Merge colors into one row Сливане на цветовете в един ред
|
||||
New Row Нов ред
|
||||
Merge Graphic Styles Сливане на графични стилове
|
||||
Merge Graphic Styles… Сливане на графични стилове…
|
||||
Merge Layers Сливане на слоеве
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -513,6 +513,7 @@
|
||||
Rotate (Classic)… Rotiraj (klasično)…
|
||||
Surface Površina
|
||||
Wireframe Žičani model
|
||||
Render Settings Postavke renderovanja
|
||||
No Shading Bez sjenčenja
|
||||
Plastic Plastika
|
||||
Highlight Intensity Intenzitet odsjaja
|
||||
@@ -2894,6 +2895,7 @@ digits Malayalam Malajalam
|
||||
Limit to Library: Ograniči na biblioteku:
|
||||
Link harmony colors Poveži boje harmonije
|
||||
Merge colors into one row Spoji boje u jedan red
|
||||
New Row Novi red
|
||||
No colors to recolor (preserved colors are left out). Nema boja za ponovno bojenje (sačuvane boje su izostavljene).
|
||||
Preserve: Očuvaj:
|
||||
Randomly change color order Nasumično promijeni redoslijed boja
|
||||
@@ -3576,6 +3578,40 @@ theme Appearance Izgled
|
||||
Create Graph Kreiraj grafikon
|
||||
Graph Data Podaci grafikona
|
||||
Graph Type Vrsta grafikona
|
||||
Graph Options Opcije grafikona
|
||||
Left Axis Lijeva osa
|
||||
Right Axis Desna osa
|
||||
Category Axis Osa kategorija
|
||||
Separate Scales Odvojene skale
|
||||
Add Drop Shadow Dodaj sjenu
|
||||
Add Legend Across Top Dodaj legendu na vrh
|
||||
First Row in Front Prvi red naprijed
|
||||
First Column in Front Prva kolona naprijed
|
||||
Legend Legenda
|
||||
Column Width Širina stupca
|
||||
Bar Width Širina trake
|
||||
Cluster Width Širina grupe
|
||||
Mark Data Points Označi tačke podataka
|
||||
Connect Data Points Poveži tačke podataka
|
||||
Edge-to-Edge Lines Linije od ivice do ivice
|
||||
Draw Filled Lines Crtaj ispunjene linije
|
||||
Tick Values Vrijednosti crtica
|
||||
Override Calculated Values Zamijeni izračunate vrijednosti
|
||||
Divisions Podioci
|
||||
Tick Marks Crtice
|
||||
Tick Marks per Division Crtica po podioku
|
||||
Draw Tick Marks Between Labels Crtaj crtice između oznaka
|
||||
Add Labels Dodaj oznake
|
||||
Maximum must be more than Minimum. Maksimum mora biti veći od minimuma.
|
||||
Column Graph Stupčasti grafikon
|
||||
Stacked Column Graph Naslagani stupčasti grafikon
|
||||
Bar Graph Trakasti grafikon
|
||||
Stacked Bar Graph Naslagani trakasti grafikon
|
||||
Line Graph Linijski grafikon
|
||||
Area Graph Površinski grafikon
|
||||
Scatter Graph Raspršeni grafikon
|
||||
Pie Graph Tortni grafikon
|
||||
Radar Graph Radarski grafikon
|
||||
Vector Halftone Vektorski polutonovi
|
||||
spot spot
|
||||
Text Tekst
|
||||
@@ -4182,6 +4218,7 @@ axis Both Obje
|
||||
Replace: Zamijeni:
|
||||
Type on a Path Options Opcije teksta na putanji
|
||||
Highlight Variable Istakni varijablu
|
||||
Sort Variables Sortiraj varijable
|
||||
New Variable Nova varijabla
|
||||
List Variables Izlistaj varijable
|
||||
Bind Variable Veži varijablu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1629,6 +1629,7 @@ antiAlias Sharp Nítid
|
||||
Rotate (Classic)… Gira (clàssic)…
|
||||
Surface Superfície
|
||||
Wireframe Filferro
|
||||
Render Settings Configuració de renderització
|
||||
No Shading Sense ombra
|
||||
Plastic Plàstic
|
||||
Highlight Intensity Intensitat de la brillantor
|
||||
@@ -1901,6 +1902,40 @@ digits Malayalam Malaiàlam
|
||||
Graph Design Disseny del gràfic
|
||||
Graph Marker Marcador del gràfic
|
||||
Graph Type Tipus de gràfic
|
||||
Graph Options Opcions del gràfic
|
||||
Left Axis Eix esquerre
|
||||
Right Axis Eix dret
|
||||
Category Axis Eix de categories
|
||||
Separate Scales Escales separades
|
||||
Add Drop Shadow Afegeix una ombra paral·lela
|
||||
Add Legend Across Top Afegeix la llegenda a la part superior
|
||||
First Row in Front Primera fila al davant
|
||||
First Column in Front Primera columna al davant
|
||||
Legend Llegenda
|
||||
Column Width Amplada de columna
|
||||
Bar Width Amplada de barra
|
||||
Cluster Width Amplada del grup
|
||||
Mark Data Points Marca els punts de dades
|
||||
Connect Data Points Connecta els punts de dades
|
||||
Edge-to-Edge Lines Línies de vora a vora
|
||||
Draw Filled Lines Dibuixa línies emplenades
|
||||
Tick Values Valors de les marques
|
||||
Override Calculated Values Substitueix els valors calculats
|
||||
Divisions Divisions
|
||||
Tick Marks Marques de graduació
|
||||
Tick Marks per Division Marques per divisió
|
||||
Draw Tick Marks Between Labels Dibuixa les marques entre les etiquetes
|
||||
Add Labels Afegeix etiquetes
|
||||
Maximum must be more than Minimum. El màxim ha de ser més gran que el mínim.
|
||||
Column Graph Gràfic de columnes
|
||||
Stacked Column Graph Gràfic de columnes apilades
|
||||
Bar Graph Gràfic de barres
|
||||
Stacked Bar Graph Gràfic de barres apilades
|
||||
Line Graph Gràfic de línies
|
||||
Area Graph Gràfic d’àrea
|
||||
Scatter Graph Gràfic de dispersió
|
||||
Pie Graph Gràfic de sectors
|
||||
Radar Graph Gràfic de radar
|
||||
Graphic Gràfic
|
||||
Graphic Pen Ploma gràfica
|
||||
Graphic Pen… Ploma gràfica…
|
||||
@@ -2042,6 +2077,7 @@ digits Malayalam Malaiàlam
|
||||
Highlight Substituted Fonts Ressalta els tipus de lletra substituïts
|
||||
Highlight Substituted Glyphs Ressalta els glifs substituïts
|
||||
Highlight Variable Ressalta la variable
|
||||
Sort Variables Ordena les variables
|
||||
Highlight: Ressalta:
|
||||
Hindi Hindi
|
||||
History Historial
|
||||
@@ -2563,6 +2599,7 @@ digits Malayalam Malaiàlam
|
||||
Menu Dialog Diàleg de menú
|
||||
Merge Fusiona
|
||||
Merge colors into one row Fusiona els colors en una fila
|
||||
New Row Fila nova
|
||||
Merge Graphic Styles Fusiona els estils gràfics
|
||||
Merge Graphic Styles… Fusiona els estils gràfics…
|
||||
Merge Layers Fusiona les capes
|
||||
@@ -4690,6 +4727,48 @@ digits Malayalam Malaiàlam
|
||||
Composite Fonts Tipus de lletra compostos
|
||||
Export Composite Fonts Exporta tipus de lletra compostos
|
||||
Import Composite Fonts Importa tipus de lletra compostos
|
||||
@msg each font must be {name, parts} cada tipus de lletra ha de ser {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts clau desconeguda `{k}`; un tipus de lletra admet name i parts
|
||||
@msg each font needs a name cada tipus de lletra necessita un nom
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` no pot anomenar un tipus de lletra compost (d’1 a {COMPOSITE_NAME_MAX} caràcters, sense caràcters de control)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts ha de ser un objecte
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe desconeguda `{k}`; les classes són kanji, kana, punctuation, symbols, roman i numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} ha de ser un objecte
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: clau desconeguda `{k}` a {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: indiqueu la part kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un document conté com a màxim {COMPOSITE_FONTS_MAX} tipus de lletra compostos
|
||||
@msg two composite fonts are named `{_1}` dos tipus de lletra compostos es diuen `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` és el nom d’un tipus de lletra instal·lat
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, un tipus de lletra compost
|
||||
@msg give fonts: [{name, parts}…] indiqueu fonts: [{name, parts}…]
|
||||
@msg path is empty path és buit
|
||||
@msg fonts must be a list of composite fonts fonts ha de ser una llista de tipus de lletra compostos
|
||||
@msg names must be strings names ha de contenir cadenes
|
||||
@msg no composite font named `{n}` cap tipus de lletra compost anomenat `{n}`
|
||||
@msg names must be a list of names names ha de ser una llista de noms
|
||||
@msg no composite fonts to export cap tipus de lletra compost per exportar
|
||||
@msg not a composite fonts file: {e} no és un fitxer de tipus de lletra compostos: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list no és un fitxer de tipus de lletra compostos: no hi ha cap llista compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts el fitxer conté més de {COMPOSITE_FONTS_MAX} tipus de lletra compostos
|
||||
@msg the file is larger than 4 MB el fitxer ocupa més de 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 no és base64
|
||||
@msg give one of path, data or dataBase64 indiqueu path, data o dataBase64
|
||||
@msg the file holds no composite fonts el fitxer no conté cap tipus de lletra compost
|
||||
@msg the file holds two composite fonts named `{_1}` el fitxer conté dos tipus de lletra compostos anomenats `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` no pot anomenar un tipus de lletra compost
|
||||
@msg {k}: give a font {k}: indiqueu un tipus de lletra
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: indiqueu un tipus de lletra
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size ha d’estar entre {_1} i {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size ha d’estar entre {_1} i {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline ha d’estar entre {_1} i {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline ha d’estar entre {_1} i {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: l’escala ha d’estar entre {_1} i {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: l’escala ha d’estar entre {_1} i {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: només kana, roman i numerals es poden escalar
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: només kana, roman i numerals es poden escalar
|
||||
@msg {k}: only kana can be scaled at the center {k}: només kana es pot escalar des del centre
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: només kana es pot escalar des del centre
|
||||
@msg no composite font cap tipus de lletra compost
|
||||
Delete Font Suprimeix el tipus de lletra
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -6219,6 +6298,7 @@ digits Malayalam Malaiàlam
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un efecte 3D al farciment o al traç d’un grup no dibuixa res per exportar
|
||||
@msg The 3D object is too complex to export L’objecte 3D és massa complex per exportar-lo
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l’objecte no té cap efecte 3D (Extrudeix i bisella, Revoluciona, Infla o Gira)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` ha de ser un objecte que només contingui `mode`, amb el valor shaded o wireframe
|
||||
@msg its 3D effect builds no solid el seu efecte 3D no construeix cap sòlid
|
||||
@msg bevel must be none or one of {_1} bevel ha de ser none o un de {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} no pot construir sòlids a partir d’aquesta il·lustració: les seves instàncies de símbol, fusions i grups de retall contenen massa objectes
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -526,6 +526,7 @@
|
||||
Rotate (Classic)… Otočit (klasické)…
|
||||
Surface Povrch
|
||||
Wireframe Drátový model
|
||||
Render Settings Nastavení vykreslování
|
||||
No Shading Bez stínování
|
||||
Plastic Plast
|
||||
Highlight Intensity Intenzita odlesku
|
||||
@@ -1664,6 +1665,48 @@ digits Malayalam Malajálamské
|
||||
Composite Fonts Složená písma
|
||||
Export Composite Fonts Exportovat složená písma
|
||||
Import Composite Fonts Importovat složená písma
|
||||
@msg each font must be {name, parts} každé písmo musí být {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts neznámý klíč `{k}`; písmo přijímá name a parts
|
||||
@msg each font needs a name každé písmo potřebuje název
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nemůže být názvem složeného písma (1 až {COMPOSITE_NAME_MAX} znaků, bez řídicích znaků)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts musí být objekt
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: neznámá třída `{k}`; třídy jsou kanji, kana, punctuation, symbols, roman a numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} musí být objekt
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: neznámý klíč `{k}` v {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: zadejte část kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts dokument obsahuje nejvýše {COMPOSITE_FONTS_MAX} složených písem
|
||||
@msg two composite fonts are named `{_1}` dvě složená písma se jmenují `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` je název nainstalovaného písma
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} používá `{font}`, což je složené písmo
|
||||
@msg give fonts: [{name, parts}…] zadejte fonts: [{name, parts}…]
|
||||
@msg path is empty path je prázdné
|
||||
@msg fonts must be a list of composite fonts fonts musí být seznam složených písem
|
||||
@msg names must be strings names musí být řetězce
|
||||
@msg no composite font named `{n}` žádné složené písmo s názvem `{n}`
|
||||
@msg names must be a list of names names musí být seznam názvů
|
||||
@msg no composite fonts to export žádná složená písma k exportu
|
||||
@msg not a composite fonts file: {e} nejde o soubor složených písem: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list nejde o soubor složených písem: chybí seznam compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts soubor obsahuje více než {COMPOSITE_FONTS_MAX} složených písem
|
||||
@msg the file is larger than 4 MB soubor je větší než 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 není base64
|
||||
@msg give one of path, data or dataBase64 zadejte path, data nebo dataBase64
|
||||
@msg the file holds no composite fonts soubor neobsahuje žádná složená písma
|
||||
@msg the file holds two composite fonts named `{_1}` soubor obsahuje dvě složená písma s názvem `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` nemůže být názvem složeného písma
|
||||
@msg {k}: give a font {k}: zadejte písmo
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: zadejte písmo
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size musí být {_1} až {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size musí být {_1} až {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline musí být {_1} až {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline musí být {_1} až {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: měřítko musí být {_1} až {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: měřítko musí být {_1} až {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: měnit měřítko lze jen u kana, roman a numerals
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: měnit měřítko lze jen u kana, roman a numerals
|
||||
@msg {k}: only kana can be scaled at the center {k}: měřítko od středu lze měnit jen u kana
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: měřítko od středu lze měnit jen u kana
|
||||
@msg no composite font žádné složené písmo
|
||||
Delete Font Odstranit písmo
|
||||
Kanji Kandži
|
||||
Kana Kana
|
||||
@@ -2930,6 +2973,7 @@ digits Malayalam Malajálamské
|
||||
Limit to Library: Omezit na knihovnu:
|
||||
Link harmony colors Propojit barvy harmonie
|
||||
Merge colors into one row Sloučit barvy do jednoho řádku
|
||||
New Row Nový řádek
|
||||
No colors to recolor (preserved colors are left out). Žádné barvy k přebarvení (zachované barvy jsou vynechány).
|
||||
Preserve: Zachovat:
|
||||
Randomly change color order Náhodně změnit pořadí barev
|
||||
@@ -3630,6 +3674,40 @@ theme Appearance Motiv
|
||||
Create Graph Vytvořit graf
|
||||
Graph Data Data grafu
|
||||
Graph Type Typ grafu
|
||||
Graph Options Možnosti grafu
|
||||
Left Axis Levá osa
|
||||
Right Axis Pravá osa
|
||||
Category Axis Osa kategorií
|
||||
Separate Scales Samostatná měřítka
|
||||
Add Drop Shadow Přidat vržený stín
|
||||
Add Legend Across Top Přidat legendu nahoru
|
||||
First Row in Front První řádek vpředu
|
||||
First Column in Front První sloupec vpředu
|
||||
Legend Legenda
|
||||
Column Width Šířka sloupce
|
||||
Bar Width Šířka pruhu
|
||||
Cluster Width Šířka skupiny
|
||||
Mark Data Points Označit datové body
|
||||
Connect Data Points Spojit datové body
|
||||
Edge-to-Edge Lines Čáry od okraje k okraji
|
||||
Draw Filled Lines Kreslit vyplněné čáry
|
||||
Tick Values Hodnoty značek
|
||||
Override Calculated Values Přepsat vypočítané hodnoty
|
||||
Divisions Dílky
|
||||
Tick Marks Značky
|
||||
Tick Marks per Division Značek na dílek
|
||||
Draw Tick Marks Between Labels Kreslit značky mezi popisky
|
||||
Add Labels Přidat popisky
|
||||
Maximum must be more than Minimum. Maximum musí být větší než minimum.
|
||||
Column Graph Sloupcový graf
|
||||
Stacked Column Graph Skládaný sloupcový graf
|
||||
Bar Graph Pruhový graf
|
||||
Stacked Bar Graph Skládaný pruhový graf
|
||||
Line Graph Spojnicový graf
|
||||
Area Graph Plošný graf
|
||||
Scatter Graph Bodový graf
|
||||
Pie Graph Výsečový graf
|
||||
Radar Graph Paprskový graf
|
||||
Vector Halftone Vektorový polotón
|
||||
spot přímá
|
||||
Text Text
|
||||
@@ -4238,6 +4316,7 @@ axis Both Obě
|
||||
Replace: Nahradit:
|
||||
Type on a Path Options Možnosti textu na cestě
|
||||
Highlight Variable Zvýraznit proměnnou
|
||||
Sort Variables Seřadit proměnné
|
||||
New Variable Nová proměnná
|
||||
List Variables Vypsat proměnné
|
||||
Bind Variable Svázat proměnnou
|
||||
@@ -6028,6 +6107,7 @@ mark-style Japanese Japonské
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 3D efekt na výplni nebo tahu skupiny nekreslí nic k exportu
|
||||
@msg The 3D object is too complex to export 3D objekt je pro export příliš složitý
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objekt nemá žádný 3D efekt (Vysunutí a zkosení, Rotační těleso, Nafouknutí ani Otočit)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` musí být objekt obsahující pouze `mode` s hodnotou shaded nebo wireframe
|
||||
@msg its 3D effect builds no solid jeho 3D efekt nevytváří žádné těleso
|
||||
@msg bevel must be none or one of {_1} bevel musí být none nebo jedno z {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} nemůže z této kresby vytvořit tělesa: její instance symbolů, prolnutí a ořezové skupiny obsahují příliš mnoho objektů
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -291,6 +291,7 @@
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export en 3D-effekt på en gruppes udfyldning eller streg tegner intet at eksportere
|
||||
@msg The 3D object is too complex to export 3D-objektet er for komplekst til at blive eksporteret
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objektet har ingen 3D-effekt (Ekstrudér og facet, Omdrej, Pust op eller Rotér)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` skal være et objekt, der kun indeholder `mode` med værdien shaded eller wireframe
|
||||
@msg its 3D effect builds no solid dens 3D-effekt danner intet legeme
|
||||
@msg bevel must be none or one of {_1} bevel skal være none eller en af {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kan ikke danne legemer af denne grafik: dens symbolforekomster, blandinger og beskæringsgrupper indeholder for mange objekter
|
||||
@@ -1089,6 +1090,7 @@
|
||||
Rotate (Classic)… Rotér (klassisk)…
|
||||
Surface Overflade
|
||||
Wireframe Trådmodel
|
||||
Render Settings Renderingsindstillinger
|
||||
No Shading Ingen skyggelægning
|
||||
Plastic Plastik
|
||||
Highlight Intensity Højlysintensitet
|
||||
@@ -2179,6 +2181,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Sammensatte skrifttyper
|
||||
Export Composite Fonts Eksportér sammensatte skrifttyper
|
||||
Import Composite Fonts Importér sammensatte skrifttyper
|
||||
@msg each font must be {name, parts} hver skrifttype skal være {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts ukendt nøgle `{k}`; en skrifttype tager name og parts
|
||||
@msg each font needs a name hver skrifttype skal have et navn
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan ikke være navnet på en sammensat skrifttype (1 til {COMPOSITE_NAME_MAX} tegn, ingen kontroltegn)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts skal være et objekt
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: ukendt klasse `{k}`; klasserne er kanji, kana, punctuation, symbols, roman og numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} skal være et objekt
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: ukendt nøgle `{k}` i {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: angiv delen kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts et dokument har højst {COMPOSITE_FONTS_MAX} sammensatte skrifttyper
|
||||
@msg two composite fonts are named `{_1}` to sammensatte skrifttyper hedder `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` er navnet på en installeret skrifttype
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} er sat med `{font}`, en sammensat skrifttype
|
||||
@msg give fonts: [{name, parts}…] angiv fonts: [{name, parts}…]
|
||||
@msg path is empty path er tom
|
||||
@msg fonts must be a list of composite fonts fonts skal være en liste med sammensatte skrifttyper
|
||||
@msg names must be strings names skal være strenge
|
||||
@msg no composite font named `{n}` ingen sammensat skrifttype hedder `{n}`
|
||||
@msg names must be a list of names names skal være en liste med navne
|
||||
@msg no composite fonts to export ingen sammensatte skrifttyper at eksportere
|
||||
@msg not a composite fonts file: {e} ikke en fil med sammensatte skrifttyper: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ikke en fil med sammensatte skrifttyper: ingen liste compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts filen indeholder mere end {COMPOSITE_FONTS_MAX} sammensatte skrifttyper
|
||||
@msg the file is larger than 4 MB filen er større end 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 er ikke base64
|
||||
@msg give one of path, data or dataBase64 angiv én af path, data eller dataBase64
|
||||
@msg the file holds no composite fonts filen indeholder ingen sammensatte skrifttyper
|
||||
@msg the file holds two composite fonts named `{_1}` filen indeholder to sammensatte skrifttyper, der hedder `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` kan ikke være navnet på en sammensat skrifttype
|
||||
@msg {k}: give a font {k}: angiv en skrifttype
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: angiv en skrifttype
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size skal være {_1} til {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size skal være {_1} til {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline skal være {_1} til {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline skal være {_1} til {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: skaleringen skal være {_1} til {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skaleringen skal være {_1} til {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: kun kana, roman og numerals kan skaleres
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: kun kana, roman og numerals kan skaleres
|
||||
@msg {k}: only kana can be scaled at the center {k}: kun kana kan skaleres fra midten
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: kun kana kan skaleres fra midten
|
||||
@msg no composite font ingen sammensat skrifttype
|
||||
Delete Font Slet skrifttype
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -3321,6 +3365,7 @@ digits Malayalam Malayalam
|
||||
Limit to Library: Begræns til bibliotek:
|
||||
Link harmony colors Sammenkæd harmonifarver
|
||||
Merge colors into one row Flet farver til én række
|
||||
New Row Ny række
|
||||
No colors to recolor (preserved colors are left out). Ingen farver at omfarve (bevarede farver er udeladt).
|
||||
Preserve: Bevar:
|
||||
Randomly change color order Ændr farverækkefølgen tilfældigt
|
||||
@@ -3970,6 +4015,40 @@ theme Appearance Udseende
|
||||
Create Graph Opret graf
|
||||
Graph Data Grafdata
|
||||
Graph Type Graftype
|
||||
Graph Options Grafindstillinger
|
||||
Left Axis Venstre akse
|
||||
Right Axis Højre akse
|
||||
Category Axis Kategoriakse
|
||||
Separate Scales Separate skalaer
|
||||
Add Drop Shadow Tilføj slagskygge
|
||||
Add Legend Across Top Tilføj forklaring øverst
|
||||
First Row in Front Første række forrest
|
||||
First Column in Front Første kolonne forrest
|
||||
Legend Forklaring
|
||||
Column Width Søjlebredde
|
||||
Bar Width Bjælkebredde
|
||||
Cluster Width Gruppebredde
|
||||
Mark Data Points Markér datapunkter
|
||||
Connect Data Points Forbind datapunkter
|
||||
Edge-to-Edge Lines Linjer fra kant til kant
|
||||
Draw Filled Lines Tegn udfyldte linjer
|
||||
Tick Values Afmærkningsværdier
|
||||
Override Calculated Values Tilsidesæt beregnede værdier
|
||||
Divisions Inddelinger
|
||||
Tick Marks Afmærkninger
|
||||
Tick Marks per Division Afmærkninger pr. inddeling
|
||||
Draw Tick Marks Between Labels Tegn afmærkninger mellem etiketter
|
||||
Add Labels Tilføj etiketter
|
||||
Maximum must be more than Minimum. Maksimum skal være større end minimum.
|
||||
Column Graph Søjlediagram
|
||||
Stacked Column Graph Stablet søjlediagram
|
||||
Bar Graph Bjælkediagram
|
||||
Stacked Bar Graph Stablet bjælkediagram
|
||||
Line Graph Linjediagram
|
||||
Area Graph Områdediagram
|
||||
Scatter Graph Spredningsdiagram
|
||||
Pie Graph Cirkeldiagram
|
||||
Radar Graph Radardiagram
|
||||
Vector Halftone Vektorraster
|
||||
spot spot
|
||||
Text Tekst
|
||||
@@ -4557,6 +4636,7 @@ axis Both Begge
|
||||
Replace: Erstat:
|
||||
Type on a Path Options Indstillinger for tekst på kurve
|
||||
Highlight Variable Fremhæv variabel
|
||||
Sort Variables Sortér variabler
|
||||
New Variable Ny variabel
|
||||
List Variables Oplist variabler
|
||||
Bind Variable Tilknyt variabel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1165,6 +1165,7 @@ font-technology Variable Variabel
|
||||
Rotate (Classic)… Drehen (klassisch)…
|
||||
Surface Oberfläche
|
||||
Wireframe Drahtmodell
|
||||
Render Settings Rendereinstellungen
|
||||
No Shading Keine Schattierung
|
||||
Plastic Kunststoff
|
||||
Highlight Intensity Glanzpunktintensität
|
||||
@@ -1411,6 +1412,40 @@ file-filter Templates Vorlagen
|
||||
Graph Diagramm
|
||||
Graph Data Diagrammdaten
|
||||
Graph Type Diagrammtyp
|
||||
Graph Options Diagrammoptionen
|
||||
Left Axis Linke Achse
|
||||
Right Axis Rechte Achse
|
||||
Category Axis Kategorieachse
|
||||
Separate Scales Getrennte Skalen
|
||||
Add Drop Shadow Schlagschatten hinzufügen
|
||||
Add Legend Across Top Legende oben hinzufügen
|
||||
First Row in Front Erste Zeile vorne
|
||||
First Column in Front Erste Spalte vorne
|
||||
Legend Legende
|
||||
Column Width Säulenbreite
|
||||
Bar Width Balkenbreite
|
||||
Cluster Width Gruppenbreite
|
||||
Mark Data Points Datenpunkte markieren
|
||||
Connect Data Points Datenpunkte verbinden
|
||||
Edge-to-Edge Lines Linien von Kante zu Kante
|
||||
Draw Filled Lines Gefüllte Linien zeichnen
|
||||
Tick Values Teilstrichwerte
|
||||
Override Calculated Values Berechnete Werte überschreiben
|
||||
Divisions Teilungen
|
||||
Tick Marks Teilstriche
|
||||
Tick Marks per Division Teilstriche pro Teilung
|
||||
Draw Tick Marks Between Labels Teilstriche zwischen Beschriftungen zeichnen
|
||||
Add Labels Beschriftungen hinzufügen
|
||||
Maximum must be more than Minimum. Das Maximum muss größer als das Minimum sein.
|
||||
Column Graph Säulendiagramm
|
||||
Stacked Column Graph Gestapeltes Säulendiagramm
|
||||
Bar Graph Balkendiagramm
|
||||
Stacked Bar Graph Gestapeltes Balkendiagramm
|
||||
Line Graph Liniendiagramm
|
||||
Area Graph Flächendiagramm
|
||||
Scatter Graph Streudiagramm
|
||||
Pie Graph Kreisdiagramm
|
||||
Radar Graph Netzdiagramm
|
||||
Graph Design Diagrammdesign
|
||||
Graph Marker Diagrammmarkierung
|
||||
New Design Neues Design
|
||||
@@ -2023,6 +2058,7 @@ file-filter Templates Vorlagen
|
||||
Menu Dialog Menüdialog
|
||||
Merge Zusammenfügen
|
||||
Merge colors into one row Farben in einer Zeile zusammenführen
|
||||
New Row Neue Zeile
|
||||
Merge Graphic Styles Grafikstile zusammenfügen
|
||||
Merge Graphic Styles… Grafikstile zusammenfügen…
|
||||
Merge Layers Ebenen zusammenfügen
|
||||
@@ -3657,6 +3693,48 @@ effect Points Punkte
|
||||
Composite Fonts Zusammengesetzte Schriften
|
||||
Export Composite Fonts Zusammengesetzte Schriften exportieren
|
||||
Import Composite Fonts Zusammengesetzte Schriften importieren
|
||||
@msg each font must be {name, parts} jede Schrift muss {name, parts} sein
|
||||
@msg unknown key `{k}`; a font takes name and parts unbekannter Schlüssel `{k}`; eine Schrift nimmt name und parts
|
||||
@msg each font needs a name jede Schrift braucht einen Namen
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kann keine zusammengesetzte Schrift benennen (1 bis {COMPOSITE_NAME_MAX} Zeichen, keine Steuerzeichen)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts muss ein Objekt sein
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: unbekannte Klasse `{k}`; die Klassen sind kanji, kana, punctuation, symbols, roman und numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} muss ein Objekt sein
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: unbekannter Schlüssel `{k}` in {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: geben Sie den Teil kanji an
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ein Dokument enthält höchstens {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften
|
||||
@msg two composite fonts are named `{_1}` zwei zusammengesetzte Schriften heißen `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` ist der Name einer installierten Schrift
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} ist in `{font}` gesetzt, einer zusammengesetzten Schrift
|
||||
@msg give fonts: [{name, parts}…] geben Sie fonts an: [{name, parts}…]
|
||||
@msg path is empty path ist leer
|
||||
@msg fonts must be a list of composite fonts fonts muss eine Liste zusammengesetzter Schriften sein
|
||||
@msg names must be strings names müssen Zeichenketten sein
|
||||
@msg no composite font named `{n}` keine zusammengesetzte Schrift namens `{n}`
|
||||
@msg names must be a list of names names muss eine Liste von Namen sein
|
||||
@msg no composite fonts to export keine zusammengesetzten Schriften zum Exportieren
|
||||
@msg not a composite fonts file: {e} keine Datei mit zusammengesetzten Schriften: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list keine Datei mit zusammengesetzten Schriften: keine Liste compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts die Datei enthält mehr als {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften
|
||||
@msg the file is larger than 4 MB die Datei ist größer als 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 ist kein Base64
|
||||
@msg give one of path, data or dataBase64 geben Sie path, data oder dataBase64 an
|
||||
@msg the file holds no composite fonts die Datei enthält keine zusammengesetzten Schriften
|
||||
@msg the file holds two composite fonts named `{_1}` die Datei enthält zwei zusammengesetzte Schriften namens `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` kann keine zusammengesetzte Schrift benennen
|
||||
@msg {k}: give a font {k}: geben Sie eine Schrift an
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: geben Sie eine Schrift an
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size muss {_1} bis {_2} % sein
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size muss {_1} bis {_2} % sein
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline muss {_1} bis {_2} % sein
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline muss {_1} bis {_2} % sein
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: die Skalierung muss {_1} bis {_2} % sein
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: die Skalierung muss {_1} bis {_2} % sein
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: nur kana, roman und numerals lassen sich skalieren
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: nur kana, roman und numerals lassen sich skalieren
|
||||
@msg {k}: only kana can be scaled at the center {k}: nur kana lässt sich am Mittelpunkt skalieren
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: nur kana lässt sich am Mittelpunkt skalieren
|
||||
@msg no composite font keine zusammengesetzte Schrift
|
||||
Delete Font Schrift löschen
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5360,6 +5438,7 @@ theme Appearance Erscheinungsbild
|
||||
Texture Image Strukturbild
|
||||
Canvas Leinwand
|
||||
Highlight Variable Variable hervorheben
|
||||
Sort Variables Variablen sortieren
|
||||
New Variable Neue Variable
|
||||
Delete Variable Variable löschen
|
||||
Lock Variables Variablen sperren
|
||||
@@ -5600,6 +5679,7 @@ lighting Light Licht
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ein 3D-Effekt auf der Fläche oder Kontur einer Gruppe zeichnet nichts, das exportiert werden kann
|
||||
@msg The 3D object is too complex to export Das 3D-Objekt ist zu komplex für den Export
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) das Objekt hat keinen 3D-Effekt (Extrudieren und abgeflachte Kante, Kreiseln, Aufblasen oder Drehen)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` muss ein Objekt sein, das nur `mode` mit dem Wert shaded oder wireframe enthält
|
||||
@msg its 3D effect builds no solid sein 3D-Effekt erzeugt keinen Körper
|
||||
@msg bevel must be none or one of {_1} bevel muss none oder einer der folgenden Werte sein: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kann aus diesem Bildmaterial keine Körper erstellen: Seine Symbolinstanzen, Angleichungen und Schnittgruppen enthalten zu viele Objekte
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1132,6 +1132,7 @@ font-technology Variable Μεταβλητές
|
||||
Rotate (Classic)… Περιστροφή (κλασικό)…
|
||||
Surface Επιφάνεια
|
||||
Wireframe Συρματόπλεγμα
|
||||
Render Settings Ρυθμίσεις απόδοσης
|
||||
No Shading Χωρίς σκίαση
|
||||
Plastic Πλαστικό
|
||||
Highlight Intensity Ένταση ανταύγειας
|
||||
@@ -1343,6 +1344,40 @@ font-technology Variable Μεταβλητές
|
||||
Graph Γράφημα
|
||||
Graph Data Δεδομένα γραφήματος
|
||||
Graph Type Τύπος γραφήματος
|
||||
Graph Options Επιλογές γραφήματος
|
||||
Left Axis Αριστερός άξονας
|
||||
Right Axis Δεξιός άξονας
|
||||
Category Axis Άξονας κατηγοριών
|
||||
Separate Scales Ξεχωριστές κλίμακες
|
||||
Add Drop Shadow Προσθήκη εξωτερικής σκιάς
|
||||
Add Legend Across Top Προσθήκη υπομνήματος στο επάνω μέρος
|
||||
First Row in Front Πρώτη γραμμή μπροστά
|
||||
First Column in Front Πρώτη στήλη μπροστά
|
||||
Legend Υπόμνημα
|
||||
Column Width Πλάτος στήλης
|
||||
Bar Width Πλάτος ράβδου
|
||||
Cluster Width Πλάτος ομάδας
|
||||
Mark Data Points Σήμανση σημείων δεδομένων
|
||||
Connect Data Points Σύνδεση σημείων δεδομένων
|
||||
Edge-to-Edge Lines Γραμμές από άκρη σε άκρη
|
||||
Draw Filled Lines Σχεδίαση γεμισμένων γραμμών
|
||||
Tick Values Τιμές υποδιαιρέσεων
|
||||
Override Calculated Values Αντικατάσταση υπολογισμένων τιμών
|
||||
Divisions Διαιρέσεις
|
||||
Tick Marks Σημάδια υποδιαίρεσης
|
||||
Tick Marks per Division Σημάδια ανά διαίρεση
|
||||
Draw Tick Marks Between Labels Σχεδίαση σημαδιών ανάμεσα στις ετικέτες
|
||||
Add Labels Προσθήκη ετικετών
|
||||
Maximum must be more than Minimum. Το μέγιστο πρέπει να είναι μεγαλύτερο από το ελάχιστο.
|
||||
Column Graph Γράφημα στηλών
|
||||
Stacked Column Graph Γράφημα στοιβαγμένων στηλών
|
||||
Bar Graph Ραβδόγραμμα
|
||||
Stacked Bar Graph Στοιβαγμένο ραβδόγραμμα
|
||||
Line Graph Γραμμικό γράφημα
|
||||
Area Graph Γράφημα περιοχής
|
||||
Scatter Graph Διάγραμμα διασποράς
|
||||
Pie Graph Κυκλικό γράφημα
|
||||
Radar Graph Αραχνοειδές γράφημα
|
||||
Graph Design Σχεδίαση γραφήματος
|
||||
Graph Marker Δείκτης γραφήματος
|
||||
New Design Νέο σχέδιο
|
||||
@@ -1462,6 +1497,7 @@ font-technology Variable Μεταβλητές
|
||||
Highlight anchors on mouse over Επισήμανση σημείων αγκύρωσης με το ποντίκι
|
||||
Highlight Rows Επισήμανση γραμμών
|
||||
Highlight Variable Επισήμανση μεταβλητής
|
||||
Sort Variables Ταξινόμηση μεταβλητών
|
||||
Highlight Substituted Fonts Επισήμανση υποκατεστημένων γραμματοσειρών
|
||||
Highlight Substituted Glyphs Επισήμανση υποκατεστημένων γλύφων
|
||||
History Ιστορικό
|
||||
@@ -3002,6 +3038,7 @@ theme Appearance Εμφάνιση
|
||||
Menu Dialog Παράθυρο μενού
|
||||
Merge Συγχώνευση
|
||||
Merge colors into one row Συγχώνευση χρωμάτων σε μία γραμμή
|
||||
New Row Νέα γραμμή
|
||||
Merge Graphic Styles Συγχώνευση στυλ γραφικών
|
||||
Merge Graphic Styles… Συγχώνευση στυλ γραφικών…
|
||||
Merge Selected Συγχώνευση επιλεγμένων
|
||||
@@ -4191,6 +4228,48 @@ digits Malayalam Μαλαγιάλαμ
|
||||
Composite Fonts Σύνθετες γραμματοσειρές
|
||||
Export Composite Fonts Εξαγωγή σύνθετων γραμματοσειρών
|
||||
Import Composite Fonts Εισαγωγή σύνθετων γραμματοσειρών
|
||||
@msg each font must be {name, parts} κάθε γραμματοσειρά πρέπει να είναι {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts άγνωστο κλειδί `{k}`· μια γραμματοσειρά δέχεται name και parts
|
||||
@msg each font needs a name κάθε γραμματοσειρά χρειάζεται όνομα
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) το `{raw}` δεν μπορεί να είναι όνομα σύνθετης γραμματοσειράς (1 έως {COMPOSITE_NAME_MAX} χαρακτήρες, χωρίς χαρακτήρες ελέγχου)
|
||||
@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}`: {_1} must be an object `{name}`: το {_1} πρέπει να είναι αντικείμενο
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: άγνωστο κλειδί `{k}` στο {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: δώστε το τμήμα kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ένα έγγραφο περιέχει το πολύ {COMPOSITE_FONTS_MAX} σύνθετες γραμματοσειρές
|
||||
@msg two composite fonts are named `{_1}` δύο σύνθετες γραμματοσειρές ονομάζονται `{_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 give fonts: [{name, parts}…] δώστε fonts: [{name, parts}…]
|
||||
@msg path is empty το path είναι κενό
|
||||
@msg fonts must be a list of composite fonts το fonts πρέπει να είναι λίστα σύνθετων γραμματοσειρών
|
||||
@msg names must be strings τα names πρέπει να είναι συμβολοσειρές
|
||||
@msg no composite font named `{n}` δεν υπάρχει σύνθετη γραμματοσειρά με όνομα `{n}`
|
||||
@msg names must be a list of names το names πρέπει να είναι λίστα ονομάτων
|
||||
@msg no composite fonts to export δεν υπάρχουν σύνθετες γραμματοσειρές για εξαγωγή
|
||||
@msg not a composite fonts file: {e} δεν είναι αρχείο σύνθετων γραμματοσειρών: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list δεν είναι αρχείο σύνθετων γραμματοσειρών: δεν υπάρχει λίστα compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts το αρχείο περιέχει περισσότερες από {COMPOSITE_FONTS_MAX} σύνθετες γραμματοσειρές
|
||||
@msg the file is larger than 4 MB το αρχείο είναι μεγαλύτερο από 4 MB
|
||||
@msg dataBase64 is not base64 το dataBase64 δεν είναι base64
|
||||
@msg give one of path, data or dataBase64 δώστε path, data ή dataBase64
|
||||
@msg the file holds no composite fonts το αρχείο δεν περιέχει σύνθετες γραμματοσειρές
|
||||
@msg the file holds two composite fonts named `{_1}` το αρχείο περιέχει δύο σύνθετες γραμματοσειρές με όνομα `{_1}`
|
||||
@msg `{_1}` can't name a composite font το `{_1}` δεν μπορεί να είναι όνομα σύνθετης γραμματοσειράς
|
||||
@msg {k}: give a font {k}: δώστε γραμματοσειρά
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: δώστε γραμματοσειρά
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: το size πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: το size πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: το baseline πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: το baseline πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: η κλίμακα πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: η κλίμακα πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: μόνο τα kana, roman και numerals αλλάζουν κλίμακα
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: μόνο τα kana, roman και numerals αλλάζουν κλίμακα
|
||||
@msg {k}: only kana can be scaled at the center {k}: μόνο το kana αλλάζει κλίμακα από το κέντρο
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: μόνο το kana αλλάζει κλίμακα από το κέντρο
|
||||
@msg no composite font καμία σύνθετη γραμματοσειρά
|
||||
Delete Font Διαγραφή γραμματοσειράς
|
||||
Kanji Κάντζι
|
||||
Kana Κάνα
|
||||
@@ -5391,6 +5470,7 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ένα εφέ 3D στο γέμισμα ή στην πινελιά μιας ομάδας δεν σχεδιάζει τίποτα για εξαγωγή
|
||||
@msg The 3D object is too complex to export Το αντικείμενο 3D είναι πολύ σύνθετο για εξαγωγή
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) το αντικείμενο δεν έχει εφέ 3D (Εξώθηση και λοξότμηση, Περιστροφή γύρω από άξονα, Φούσκωμα ή Περιστροφή)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe το `render` πρέπει να είναι αντικείμενο που περιέχει μόνο `mode`, με τιμή shaded ή wireframe
|
||||
@msg its 3D effect builds no solid το εφέ 3D του δεν δημιουργεί στερεό
|
||||
@msg bevel must be none or one of {_1} το bevel πρέπει να είναι none ή ένα από τα {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: δεν μπορούν να δημιουργηθούν στερεά από αυτό το σχέδιο· οι παρουσίες συμβόλων, οι αναμείξεις και οι ομάδες αποκοπής του περιέχουν πάρα πολλά αντικείμενα
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1188,6 +1188,7 @@ font-technology Variable Variables
|
||||
Rotate (Classic)… Rotar (clásico)…
|
||||
Surface Superficie
|
||||
Wireframe Malla
|
||||
Render Settings Ajustes de renderizado
|
||||
No Shading Sin sombreado
|
||||
Plastic Plástico
|
||||
Highlight Intensity Intensidad del brillo
|
||||
@@ -1422,6 +1423,40 @@ font-technology Variable Variables
|
||||
Graph Gráfico
|
||||
Graph Data Datos del gráfico
|
||||
Graph Type Tipo de gráfico
|
||||
Graph Options Opciones de gráfico
|
||||
Left Axis Eje izquierdo
|
||||
Right Axis Eje derecho
|
||||
Category Axis Eje de categorías
|
||||
Separate Scales Escalas separadas
|
||||
Add Drop Shadow Añadir sombra paralela
|
||||
Add Legend Across Top Añadir leyenda en la parte superior
|
||||
First Row in Front Primera fila delante
|
||||
First Column in Front Primera columna delante
|
||||
Legend Leyenda
|
||||
Column Width Anchura de columna
|
||||
Bar Width Anchura de barra
|
||||
Cluster Width Anchura de grupo
|
||||
Mark Data Points Marcar puntos de datos
|
||||
Connect Data Points Conectar puntos de datos
|
||||
Edge-to-Edge Lines Líneas de borde a borde
|
||||
Draw Filled Lines Dibujar líneas rellenas
|
||||
Tick Values Valores de marcas
|
||||
Override Calculated Values Sustituir valores calculados
|
||||
Divisions Divisiones
|
||||
Tick Marks Marcas de graduación
|
||||
Tick Marks per Division Marcas por división
|
||||
Draw Tick Marks Between Labels Dibujar marcas entre etiquetas
|
||||
Add Labels Añadir etiquetas
|
||||
Maximum must be more than Minimum. El máximo debe ser mayor que el mínimo.
|
||||
Column Graph Gráfico de columnas
|
||||
Stacked Column Graph Gráfico de columnas apiladas
|
||||
Bar Graph Gráfico de barras
|
||||
Stacked Bar Graph Gráfico de barras apiladas
|
||||
Line Graph Gráfico de líneas
|
||||
Area Graph Gráfico de áreas
|
||||
Scatter Graph Gráfico de dispersión
|
||||
Pie Graph Gráfico circular
|
||||
Radar Graph Gráfico de radar
|
||||
Graph Design Diseño de gráfico
|
||||
Graph Marker Marcador de gráfico
|
||||
New Design Nuevo diseño
|
||||
@@ -1542,6 +1577,7 @@ font-technology Variable Variables
|
||||
Highlight anchors on mouse over Resaltar anclas al pasar el puntero
|
||||
Highlight Rows Resaltar filas
|
||||
Highlight Variable Resaltar variable
|
||||
Sort Variables Ordenar variables
|
||||
Highlight Substituted Fonts Resaltar fuentes sustituidas
|
||||
Highlight Substituted Glyphs Resaltar glifos sustituidos
|
||||
History Historial
|
||||
@@ -2019,6 +2055,7 @@ font-technology Variable Variables
|
||||
Menu Dialog Cuadro de diálogo de menú
|
||||
Merge Combinar
|
||||
Merge colors into one row Combinar colores en una fila
|
||||
New Row Nueva fila
|
||||
Merge Graphic Styles Combinar estilos gráficos
|
||||
Merge Graphic Styles… Combinar estilos gráficos…
|
||||
Merge Layers Combinar capas
|
||||
@@ -3705,6 +3742,48 @@ digits Malayalam Malayálam
|
||||
Composite Fonts Fuentes compuestas
|
||||
Export Composite Fonts Exportar fuentes compuestas
|
||||
Import Composite Fonts Importar fuentes compuestas
|
||||
@msg each font must be {name, parts} cada fuente debe ser {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts clave desconocida `{k}`; una fuente admite name y parts
|
||||
@msg each font needs a name cada fuente necesita un nombre
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` no puede nombrar una fuente compuesta (de 1 a {COMPOSITE_NAME_MAX} caracteres, sin caracteres de control)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts debe ser un objeto
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: clase desconocida `{k}`; las clases son kanji, kana, punctuation, symbols, roman y numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} debe ser un objeto
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: clave desconocida `{k}` en {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: indique la parte kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un documento admite como máximo {COMPOSITE_FONTS_MAX} fuentes compuestas
|
||||
@msg two composite fonts are named `{_1}` dos fuentes compuestas se llaman `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` es el nombre de una fuente instalada
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, una fuente compuesta
|
||||
@msg give fonts: [{name, parts}…] indique fonts: [{name, parts}…]
|
||||
@msg path is empty path está vacío
|
||||
@msg fonts must be a list of composite fonts fonts debe ser una lista de fuentes compuestas
|
||||
@msg names must be strings names debe contener cadenas
|
||||
@msg no composite font named `{n}` no hay ninguna fuente compuesta llamada `{n}`
|
||||
@msg names must be a list of names names debe ser una lista de nombres
|
||||
@msg no composite fonts to export no hay fuentes compuestas para exportar
|
||||
@msg not a composite fonts file: {e} no es un archivo de fuentes compuestas: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list no es un archivo de fuentes compuestas: no hay lista compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts el archivo contiene más de {COMPOSITE_FONTS_MAX} fuentes compuestas
|
||||
@msg the file is larger than 4 MB el archivo ocupa más de 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 no es base64
|
||||
@msg give one of path, data or dataBase64 indique path, data o dataBase64
|
||||
@msg the file holds no composite fonts el archivo no contiene fuentes compuestas
|
||||
@msg the file holds two composite fonts named `{_1}` el archivo contiene dos fuentes compuestas llamadas `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` no puede nombrar una fuente compuesta
|
||||
@msg {k}: give a font {k}: indique una fuente
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: indique una fuente
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size debe estar entre {_1} y {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size debe estar entre {_1} y {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline debe estar entre {_1} y {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline debe estar entre {_1} y {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: la escala debe estar entre {_1} y {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: la escala debe estar entre {_1} y {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman y numerals se pueden escalar
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: solo kana, roman y numerals se pueden escalar
|
||||
@msg {k}: only kana can be scaled at the center {k}: solo kana se puede escalar desde el centro
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: solo kana se puede escalar desde el centro
|
||||
@msg no composite font ninguna fuente compuesta
|
||||
Delete Font Eliminar fuente
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5971,6 +6050,7 @@ mark-style Japanese Japonesas
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un efecto 3D en el relleno o el trazo de un grupo no dibuja nada que exportar
|
||||
@msg The 3D object is too complex to export El objeto 3D es demasiado complejo para exportarlo
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) el objeto no tiene ningún efecto 3D (Extrusión y biselado, Revolución, Inflar o Rotar)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` debe ser un objeto que solo contenga `mode`, con el valor shaded o wireframe
|
||||
@msg its 3D effect builds no solid su efecto 3D no construye ningún sólido
|
||||
@msg bevel must be none or one of {_1} bevel debe ser none o uno de estos: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} no puede construir sólidos a partir de esta ilustración: sus instancias de símbolo, fusiones y grupos de recorte contienen demasiados objetos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -523,6 +523,7 @@
|
||||
Rotate (Classic)… Kierrä (perinteinen)…
|
||||
Surface Pinta
|
||||
Wireframe Rautalanka
|
||||
Render Settings Renderöintiasetukset
|
||||
No Shading Ei varjostusta
|
||||
Plastic Muovi
|
||||
Highlight Intensity Kiillon voimakkuus
|
||||
@@ -1637,6 +1638,48 @@ digits Malayalam Malajalam
|
||||
Composite Fonts Yhdistelmäfontit
|
||||
Export Composite Fonts Vie yhdistelmäfontit
|
||||
Import Composite Fonts Tuo yhdistelmäfontit
|
||||
@msg each font must be {name, parts} jokaisen fontin on oltava {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts tuntematon avain `{k}`; fontti ottaa name ja parts
|
||||
@msg each font needs a name jokainen fontti tarvitsee nimen
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ei kelpaa yhdistelmäfontin nimeksi (1–{COMPOSITE_NAME_MAX} merkkiä, ei ohjausmerkkejä)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts on oltava objekti
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: tuntematon luokka `{k}`; luokat ovat kanji, kana, punctuation, symbols, roman ja numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} on oltava objekti
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: tuntematon avain `{k}` osassa {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: anna osa kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts asiakirjassa voi olla enintään {COMPOSITE_FONTS_MAX} yhdistelmäfonttia
|
||||
@msg two composite fonts are named `{_1}` kahden yhdistelmäfontin nimi on `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` on asennetun fontin nimi
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} käyttää fonttia `{font}`, joka on yhdistelmäfontti
|
||||
@msg give fonts: [{name, parts}…] anna fonts: [{name, parts}…]
|
||||
@msg path is empty path on tyhjä
|
||||
@msg fonts must be a list of composite fonts fonts on oltava luettelo yhdistelmäfonteista
|
||||
@msg names must be strings names on oltava merkkijonoja
|
||||
@msg no composite font named `{n}` yhdistelmäfonttia `{n}` ei ole
|
||||
@msg names must be a list of names names on oltava luettelo nimistä
|
||||
@msg no composite fonts to export ei vietäviä yhdistelmäfontteja
|
||||
@msg not a composite fonts file: {e} ei yhdistelmäfonttitiedosto: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ei yhdistelmäfonttitiedosto: luettelo compositeFonts puuttuu
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts tiedostossa on yli {COMPOSITE_FONTS_MAX} yhdistelmäfonttia
|
||||
@msg the file is larger than 4 MB tiedosto on suurempi kuin 4 Mt
|
||||
@msg dataBase64 is not base64 dataBase64 ei ole base64-muotoa
|
||||
@msg give one of path, data or dataBase64 anna path, data tai dataBase64
|
||||
@msg the file holds no composite fonts tiedostossa ei ole yhdistelmäfontteja
|
||||
@msg the file holds two composite fonts named `{_1}` tiedostossa on kaksi yhdistelmäfonttia nimeltä `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` ei kelpaa yhdistelmäfontin nimeksi
|
||||
@msg {k}: give a font {k}: anna fontti
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: anna fontti
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size on oltava {_1}–{_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size on oltava {_1}–{_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline on oltava {_1}–{_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline on oltava {_1}–{_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: skaalauksen on oltava {_1}–{_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skaalauksen on oltava {_1}–{_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: vain kana, roman ja numerals voidaan skaalata
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: vain kana, roman ja numerals voidaan skaalata
|
||||
@msg {k}: only kana can be scaled at the center {k}: vain kana voidaan skaalata keskeltä
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: vain kana voidaan skaalata keskeltä
|
||||
@msg no composite font ei yhdistelmäfonttia
|
||||
Delete Font Poista fontti
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -2874,6 +2917,7 @@ digits Malayalam Malajalam
|
||||
Limit to Library: Rajoita kirjastoon:
|
||||
Link harmony colors Linkitä harmoniavärit
|
||||
Merge colors into one row Yhdistä värit yhdelle riville
|
||||
New Row Uusi rivi
|
||||
No colors to recolor (preserved colors are left out). Ei uudelleenväritettäviä värejä (säilytetyt värit jätetään pois).
|
||||
Preserve: Säilytä:
|
||||
Randomly change color order Muuta värijärjestystä satunnaisesti
|
||||
@@ -3568,6 +3612,40 @@ theme Appearance Ulkoasu
|
||||
Create Graph Luo kaavio
|
||||
Graph Data Kaavion tiedot
|
||||
Graph Type Kaavion tyyppi
|
||||
Graph Options Kaavion asetukset
|
||||
Left Axis Vasen akseli
|
||||
Right Axis Oikea akseli
|
||||
Category Axis Luokka-akseli
|
||||
Separate Scales Erilliset asteikot
|
||||
Add Drop Shadow Lisää pudotusvarjo
|
||||
Add Legend Across Top Lisää selite ylös
|
||||
First Row in Front Ensimmäinen rivi edessä
|
||||
First Column in Front Ensimmäinen sarake edessä
|
||||
Legend Selite
|
||||
Column Width Pylvään leveys
|
||||
Bar Width Palkin leveys
|
||||
Cluster Width Ryhmän leveys
|
||||
Mark Data Points Merkitse arvopisteet
|
||||
Connect Data Points Yhdistä arvopisteet
|
||||
Edge-to-Edge Lines Viivat reunasta reunaan
|
||||
Draw Filled Lines Piirrä täytetyt viivat
|
||||
Tick Values Asteikon arvot
|
||||
Override Calculated Values Ohita lasketut arvot
|
||||
Divisions Jaot
|
||||
Tick Marks Asteikkomerkit
|
||||
Tick Marks per Division Asteikkomerkkejä jakoa kohden
|
||||
Draw Tick Marks Between Labels Piirrä asteikkomerkit nimien väliin
|
||||
Add Labels Lisää nimet
|
||||
Maximum must be more than Minimum. Suurimman on oltava pienintä suurempi.
|
||||
Column Graph Pylväskaavio
|
||||
Stacked Column Graph Pinottu pylväskaavio
|
||||
Bar Graph Palkkikaavio
|
||||
Stacked Bar Graph Pinottu palkkikaavio
|
||||
Line Graph Viivakaavio
|
||||
Area Graph Aluekaavio
|
||||
Scatter Graph Hajontakaavio
|
||||
Pie Graph Ympyräkaavio
|
||||
Radar Graph Säteittäinen kaavio
|
||||
Vector Halftone Vektorirasteri
|
||||
spot spotti
|
||||
Text Teksti
|
||||
@@ -4157,6 +4235,7 @@ axis Both Molemmat
|
||||
Replace: Korvaa:
|
||||
Type on a Path Options Polulla olevan tekstin asetukset
|
||||
Highlight Variable Korosta muuttuja
|
||||
Sort Variables Lajittele muuttujat
|
||||
New Variable Uusi muuttuja
|
||||
List Variables Luettele muuttujat
|
||||
Bind Variable Sido muuttuja
|
||||
@@ -5913,6 +5992,7 @@ mark-style Japanese Japanilainen
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ryhmän täytön tai viivan 3D-tehoste ei piirrä mitään vietävää
|
||||
@msg The 3D object is too complex to export 3D-objekti on liian monimutkainen vietäväksi
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objektilla ei ole 3D-tehostetta (Pursota ja viistä, Pyöräytä, Täytä ilmalla tai Kierrä)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` on oltava objekti, jossa on vain `mode` arvolla shaded tai wireframe
|
||||
@msg its 3D effect builds no solid sen 3D-tehoste ei muodosta kappaletta
|
||||
@msg bevel must be none or one of {_1} bevel-arvon on oltava none tai jokin seuraavista: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} ei voi muodostaa kappaleita tästä kuvituksesta: sen symbolien esiintymät, sulautukset ja leikkausryhmät sisältävät liikaa objekteja
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1183,6 +1183,7 @@ font-technology Variable Variables
|
||||
Rotate (Classic)… Rotation (classique)…
|
||||
Surface Surface
|
||||
Wireframe Fil de fer
|
||||
Render Settings Paramètres de rendu
|
||||
No Shading Sans ombrage
|
||||
Plastic Plastique
|
||||
Highlight Intensity Intensité de la brillance
|
||||
@@ -1416,6 +1417,40 @@ font-technology Variable Variables
|
||||
Graph Graphe
|
||||
Graph Data Données du graphe
|
||||
Graph Type Type de graphe
|
||||
Graph Options Options du graphe
|
||||
Left Axis Axe gauche
|
||||
Right Axis Axe droit
|
||||
Category Axis Axe des catégories
|
||||
Separate Scales Échelles distinctes
|
||||
Add Drop Shadow Ajouter une ombre portée
|
||||
Add Legend Across Top Ajouter la légende en haut
|
||||
First Row in Front Première ligne au premier plan
|
||||
First Column in Front Première colonne au premier plan
|
||||
Legend Légende
|
||||
Column Width Largeur des colonnes
|
||||
Bar Width Largeur des barres
|
||||
Cluster Width Largeur des groupes
|
||||
Mark Data Points Marquer les points de données
|
||||
Connect Data Points Relier les points de données
|
||||
Edge-to-Edge Lines Lignes bord à bord
|
||||
Draw Filled Lines Tracer des lignes pleines
|
||||
Tick Values Valeurs des graduations
|
||||
Override Calculated Values Remplacer les valeurs calculées
|
||||
Divisions Divisions
|
||||
Tick Marks Graduations
|
||||
Tick Marks per Division Graduations par division
|
||||
Draw Tick Marks Between Labels Tracer les graduations entre les étiquettes
|
||||
Add Labels Ajouter des étiquettes
|
||||
Maximum must be more than Minimum. Le maximum doit être supérieur au minimum.
|
||||
Column Graph Graphe à colonnes
|
||||
Stacked Column Graph Graphe à colonnes empilées
|
||||
Bar Graph Graphe à barres
|
||||
Stacked Bar Graph Graphe à barres empilées
|
||||
Line Graph Graphe linéaire
|
||||
Area Graph Graphe de zones
|
||||
Scatter Graph Graphe de dispersion
|
||||
Pie Graph Graphe à secteurs
|
||||
Radar Graph Graphe radar
|
||||
Graph Design Conception de graphe
|
||||
Graph Marker Marqueur de graphe
|
||||
New Design Nouvelle conception
|
||||
@@ -1536,6 +1571,7 @@ font-technology Variable Variables
|
||||
Highlight anchors on mouse over Mettre en évidence les points d’ancrage au survol
|
||||
Highlight Rows Surligner les lignes
|
||||
Highlight Variable Surligner la variable
|
||||
Sort Variables Trier les variables
|
||||
Highlight Substituted Fonts Surligner les polices remplacées
|
||||
Highlight Substituted Glyphs Surligner les glyphes remplacés
|
||||
History Historique
|
||||
@@ -2013,6 +2049,7 @@ font-technology Variable Variables
|
||||
Menu Dialog Boîte de dialogue du menu
|
||||
Merge Fusionner
|
||||
Merge colors into one row Fusionner les couleurs sur une ligne
|
||||
New Row Nouvelle ligne
|
||||
Merge Graphic Styles Fusionner les styles graphiques
|
||||
Merge Graphic Styles… Fusionner les styles graphiques…
|
||||
Merge Layers Fusionner les calques
|
||||
@@ -3697,6 +3734,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Polices composites
|
||||
Export Composite Fonts Exporter des polices composites
|
||||
Import Composite Fonts Importer des polices composites
|
||||
@msg each font must be {name, parts} chaque police doit être {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts clé inconnue `{k}` ; une police prend name et parts
|
||||
@msg each font needs a name chaque police doit avoir un nom
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ne peut pas nommer une police composite (1 à {COMPOSITE_NAME_MAX} caractères, sans caractères de contrôle)
|
||||
@msg `{name}`: parts must be an object `{name}` : parts doit être un objet
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}` : classe inconnue `{k}` ; les classes sont kanji, kana, punctuation, symbols, roman et numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}` : {_1} doit être un objet
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}` : clé inconnue `{k}` dans {_1}
|
||||
@msg `{name}`: give the kanji part `{name}` : indiquez la partie kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un document contient au plus {COMPOSITE_FONTS_MAX} polices composites
|
||||
@msg two composite fonts are named `{_1}` deux polices composites s’appellent `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` est le nom d’une police installée
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}` : {_2} utilise `{font}`, une police composite
|
||||
@msg give fonts: [{name, parts}…] indiquez fonts : [{name, parts}…]
|
||||
@msg path is empty path est vide
|
||||
@msg fonts must be a list of composite fonts fonts doit être une liste de polices composites
|
||||
@msg names must be strings names doit contenir des chaînes
|
||||
@msg no composite font named `{n}` aucune police composite nommée `{n}`
|
||||
@msg names must be a list of names names doit être une liste de noms
|
||||
@msg no composite fonts to export aucune police composite à exporter
|
||||
@msg not a composite fonts file: {e} ce n’est pas un fichier de polices composites : {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ce n’est pas un fichier de polices composites : pas de liste compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts le fichier contient plus de {COMPOSITE_FONTS_MAX} polices composites
|
||||
@msg the file is larger than 4 MB le fichier dépasse 4 Mo
|
||||
@msg dataBase64 is not base64 dataBase64 n’est pas du base64
|
||||
@msg give one of path, data or dataBase64 indiquez path, data ou dataBase64
|
||||
@msg the file holds no composite fonts le fichier ne contient aucune police composite
|
||||
@msg the file holds two composite fonts named `{_1}` le fichier contient deux polices composites nommées `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` ne peut pas nommer une police composite
|
||||
@msg {k}: give a font {k} : indiquez une police
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k} : indiquez une police
|
||||
@msg {k}: size must be {_1} to {_2}% {k} : size doit être compris entre {_1} et {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k} : size doit être compris entre {_1} et {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k} : baseline doit être compris entre {_1} et {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k} : baseline doit être compris entre {_1} et {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k} : l’échelle doit être comprise entre {_1} et {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k} : l’échelle doit être comprise entre {_1} et {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k} : seuls kana, roman et numerals peuvent être mis à l’échelle
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k} : seuls kana, roman et numerals peuvent être mis à l’échelle
|
||||
@msg {k}: only kana can be scaled at the center {k} : seul kana peut être mis à l’échelle depuis le centre
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k} : seul kana peut être mis à l’échelle depuis le centre
|
||||
@msg no composite font aucune police composite
|
||||
Delete Font Supprimer la police
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5596,6 +5675,7 @@ lighting Light Lumière
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un effet 3D sur le fond ou le contour d'un groupe ne dessine rien à exporter
|
||||
@msg The 3D object is too complex to export L'objet 3D est trop complexe pour être exporté
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l'objet n'a aucun effet 3D (Extrusion et biseautage, Révolution, Gonfler ou Rotation)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` doit être un objet ne contenant que `mode`, avec la valeur shaded ou wireframe
|
||||
@msg its 3D effect builds no solid son effet 3D ne construit aucun volume
|
||||
@msg bevel must be none or one of {_1} bevel doit valoir none ou l’une de ces valeurs : {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} ne peut pas construire de volumes à partir de cette illustration : ses occurrences de symbole, dégradés de formes et groupes de découpe contiennent trop d'objets
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1540,6 +1540,40 @@
|
||||
Graph Design ગ્રાફ ડિઝાઇન
|
||||
Graph Marker ગ્રાફ માર્કર
|
||||
Graph Type ગ્રાફ પ્રકાર
|
||||
Graph Options ગ્રાફ વિકલ્પો
|
||||
Left Axis ડાબી અક્ષ
|
||||
Right Axis જમણી અક્ષ
|
||||
Category Axis શ્રેણી અક્ષ
|
||||
Separate Scales અલગ સ્કેલ
|
||||
Add Drop Shadow ડ્રોપ શેડો ઉમેરો
|
||||
Add Legend Across Top ઉપર લેજન્ડ ઉમેરો
|
||||
First Row in Front પ્રથમ પંક્તિ આગળ
|
||||
First Column in Front પ્રથમ કૉલમ આગળ
|
||||
Legend લેજન્ડ
|
||||
Column Width કૉલમ પહોળાઈ
|
||||
Bar Width બાર પહોળાઈ
|
||||
Cluster Width જૂથ પહોળાઈ
|
||||
Mark Data Points ડેટા પોઇન્ટ ચિહ્નિત કરો
|
||||
Connect Data Points ડેટા પોઇન્ટ જોડો
|
||||
Edge-to-Edge Lines કિનારીથી કિનારી સુધી રેખાઓ
|
||||
Draw Filled Lines ભરેલી રેખાઓ દોરો
|
||||
Tick Values ટિક મૂલ્યો
|
||||
Override Calculated Values ગણતરી કરેલાં મૂલ્યો બદલો
|
||||
Divisions વિભાગો
|
||||
Tick Marks ટિક ચિહ્નો
|
||||
Tick Marks per Division વિભાગ દીઠ ટિક ચિહ્નો
|
||||
Draw Tick Marks Between Labels લેબલ વચ્ચે ટિક ચિહ્નો દોરો
|
||||
Add Labels લેબલ ઉમેરો
|
||||
Maximum must be more than Minimum. મહત્તમ ન્યૂનતમ કરતાં વધુ હોવું જોઈએ.
|
||||
Column Graph કૉલમ ગ્રાફ
|
||||
Stacked Column Graph સ્ટેક્ડ કૉલમ ગ્રાફ
|
||||
Bar Graph બાર ગ્રાફ
|
||||
Stacked Bar Graph સ્ટેક્ડ બાર ગ્રાફ
|
||||
Line Graph રેખા ગ્રાફ
|
||||
Area Graph ક્ષેત્ર ગ્રાફ
|
||||
Scatter Graph સ્કેટર ગ્રાફ
|
||||
Pie Graph પાઇ ગ્રાફ
|
||||
Radar Graph રડાર ગ્રાફ
|
||||
Graphic Pen… ગ્રાફિક પેન…
|
||||
Graphic Style ગ્રાફિક સ્ટાઇલ
|
||||
Graphic Style Libraries ગ્રાફિક સ્ટાઇલ લાઇબ્રેરીઓ
|
||||
@@ -1644,6 +1678,7 @@
|
||||
Highlight Substituted Glyphs બદલાયેલા ગ્લિફ હાઇલાઇટ કરો
|
||||
Highlight Tracking ટ્રેકિંગ હાઇલાઇટ કરો
|
||||
Highlight Variable વેરિએબલ હાઇલાઇટ કરો
|
||||
Sort Variables વેરિએબલનો ક્રમ લગાવો
|
||||
Highlight anchors on mouse over માઉસ ફેરવતાં એન્કર હાઇલાઇટ કરો
|
||||
Highlight: હાઇલાઇટ:
|
||||
Hindi હિન્દી
|
||||
@@ -2107,6 +2142,7 @@
|
||||
Merge Shared Edge શેર કરેલી કિનાર મર્જ કરો
|
||||
Merge Swatches સ્વેચ મર્જ કરો
|
||||
Merge colors into one row રંગોને એક હરોળમાં મર્જ કરો
|
||||
New Row નવી હરોળ
|
||||
Merge only with selection માત્ર પસંદગી સાથે મર્જ કરો
|
||||
Merged PRs મર્જ થયેલા PR
|
||||
Mesh Point Color મેશ બિંદુનો રંગ
|
||||
@@ -4179,6 +4215,7 @@
|
||||
Width: પહોળાઈ:
|
||||
Window વિન્ડો
|
||||
Wireframe વાયરફ્રેમ
|
||||
Render Settings રેન્ડર સેટિંગ્સ
|
||||
Word Spacing: શબ્દ અંતર:
|
||||
Words Longer Than: આનાથી લાંબા શબ્દો:
|
||||
Working CMYK કાર્યકારી CMYK
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 72 and column 5.
|
@@ -1827,6 +1827,7 @@ antiAlias Sharp शार्प
|
||||
Rotate (Classic)… घुमाव (क्लासिक)…
|
||||
Surface सतह
|
||||
Wireframe वायरफ़्रेम
|
||||
Render Settings रेंडर सेटिंग्स
|
||||
No Shading कोई शेडिंग नहीं
|
||||
Plastic प्लास्टिक
|
||||
Highlight Intensity हाइलाइट तीव्रता
|
||||
@@ -2116,6 +2117,40 @@ antiAlias Sharp शार्प
|
||||
Graph Design ग्राफ़ डिजाइन
|
||||
Graph Marker ग्राफ़ मार्कर
|
||||
Graph Type ग्राफ़ टाइप
|
||||
Graph Options ग्राफ़ विकल्प
|
||||
Left Axis बायाँ एक्सिस
|
||||
Right Axis दायाँ एक्सिस
|
||||
Category Axis श्रेणी एक्सिस
|
||||
Separate Scales अलग स्केल
|
||||
Add Drop Shadow ड्रॉप शैडो जोड़ें
|
||||
Add Legend Across Top ऊपर लेजेंड जोड़ें
|
||||
First Row in Front पहली पंक्ति आगे
|
||||
First Column in Front पहला कॉलम आगे
|
||||
Legend लेजेंड
|
||||
Column Width कॉलम चौड़ाई
|
||||
Bar Width बार चौड़ाई
|
||||
Cluster Width समूह चौड़ाई
|
||||
Mark Data Points डेटा पॉइंट चिह्नित करें
|
||||
Connect Data Points डेटा पॉइंट जोड़ें
|
||||
Edge-to-Edge Lines किनारे से किनारे तक रेखाएँ
|
||||
Draw Filled Lines भरी हुई रेखाएँ बनाएँ
|
||||
Tick Values टिक मान
|
||||
Override Calculated Values परिकलित मानों को बदलें
|
||||
Divisions विभाजन
|
||||
Tick Marks टिक चिह्न
|
||||
Tick Marks per Division प्रति विभाजन टिक चिह्न
|
||||
Draw Tick Marks Between Labels लेबलों के बीच टिक चिह्न बनाएँ
|
||||
Add Labels लेबल जोड़ें
|
||||
Maximum must be more than Minimum. अधिकतम, न्यूनतम से अधिक होना चाहिए।
|
||||
Column Graph कॉलम ग्राफ़
|
||||
Stacked Column Graph स्टैक्ड कॉलम ग्राफ़
|
||||
Bar Graph बार ग्राफ़
|
||||
Stacked Bar Graph स्टैक्ड बार ग्राफ़
|
||||
Line Graph लाइन ग्राफ़
|
||||
Area Graph एरिया ग्राफ़
|
||||
Scatter Graph स्कैटर ग्राफ़
|
||||
Pie Graph पाई ग्राफ़
|
||||
Radar Graph रडार ग्राफ़
|
||||
Graphic ग्राफिक
|
||||
Graphic Pen ग्राफ़िक पेन
|
||||
Graphic Pen… ग्राफ़िक पेन…
|
||||
@@ -2271,6 +2306,7 @@ antiAlias Sharp शार्प
|
||||
Highlight Substituted Glyphs बदले हुए ग्लिफ़ हाइलाइट करें
|
||||
Highlight Tracking हाइलाइट ट्रैकिंग
|
||||
Highlight Variable वेरिएबल हाइलाइट करें
|
||||
Sort Variables वेरिएबल क्रमबद्ध करें
|
||||
Highlight: हाइलाइट:
|
||||
Hindi हिन्दी
|
||||
History हिस्ट्री
|
||||
@@ -2819,6 +2855,7 @@ antiAlias Sharp शार्प
|
||||
Menu Dialog मेन्यू डायलॉग
|
||||
Merge मर्ज करें
|
||||
Merge colors into one row रंगों को एक पंक्ति में मिलाएं
|
||||
New Row नई पंक्ति
|
||||
Merge Graphic Styles मर्ज ग्राफिक स्टाइल्स
|
||||
Merge Graphic Styles… मर्ज ग्राफिक स्टाइल्स…
|
||||
Merge Layers लेयर्स मर्ज करें
|
||||
@@ -5075,6 +5112,48 @@ antiAlias Sharp शार्प
|
||||
Composite Fonts कंपोज़िट फ़ॉन्ट
|
||||
Export Composite Fonts कंपोज़िट फ़ॉन्ट एक्सपोर्ट करें
|
||||
Import Composite Fonts कंपोज़िट फ़ॉन्ट इम्पोर्ट करें
|
||||
@msg each font must be {name, parts} हर फ़ॉन्ट {name, parts} होना चाहिए
|
||||
@msg unknown key `{k}`; a font takes name and parts अज्ञात कुंजी `{k}`; फ़ॉन्ट name और parts लेता है
|
||||
@msg each font needs a name हर फ़ॉन्ट का एक नाम होना चाहिए
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` कंपोज़िट फ़ॉन्ट का नाम नहीं हो सकता (1 से {COMPOSITE_NAME_MAX} अक्षर, कोई नियंत्रण अक्षर नहीं)
|
||||
@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}`: {_1} must be an object `{name}`: {_1} एक ऑब्जेक्ट होना चाहिए
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} में अज्ञात कुंजी `{k}`
|
||||
@msg `{name}`: give the kanji part `{name}`: kanji भाग दें
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts एक दस्तावेज़ में अधिकतम {COMPOSITE_FONTS_MAX} कंपोज़िट फ़ॉन्ट रहते हैं
|
||||
@msg two composite fonts are named `{_1}` दो कंपोज़िट फ़ॉन्ट का नाम `{_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 give fonts: [{name, parts}…] fonts दें: [{name, parts}…]
|
||||
@msg path is empty path खाली है
|
||||
@msg fonts must be a list of composite fonts fonts कंपोज़िट फ़ॉन्ट की सूची होनी चाहिए
|
||||
@msg names must be strings names स्ट्रिंग होने चाहिए
|
||||
@msg no composite font named `{n}` `{n}` नाम का कोई कंपोज़िट फ़ॉन्ट नहीं है
|
||||
@msg names must be a list of names names नामों की सूची होनी चाहिए
|
||||
@msg no composite fonts to export एक्सपोर्ट करने के लिए कोई कंपोज़िट फ़ॉन्ट नहीं है
|
||||
@msg not a composite fonts file: {e} यह कंपोज़िट फ़ॉन्ट फ़ाइल नहीं है: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list यह कंपोज़िट फ़ॉन्ट फ़ाइल नहीं है: compositeFonts सूची नहीं है
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts फ़ाइल में {COMPOSITE_FONTS_MAX} से अधिक कंपोज़िट फ़ॉन्ट हैं
|
||||
@msg the file is larger than 4 MB फ़ाइल 4 MB से बड़ी है
|
||||
@msg dataBase64 is not base64 dataBase64 base64 नहीं है
|
||||
@msg give one of path, data or dataBase64 path, data या dataBase64 में से एक दें
|
||||
@msg the file holds no composite fonts फ़ाइल में कोई कंपोज़िट फ़ॉन्ट नहीं है
|
||||
@msg the file holds two composite fonts named `{_1}` फ़ाइल में `{_1}` नाम के दो कंपोज़िट फ़ॉन्ट हैं
|
||||
@msg `{_1}` can't name a composite font `{_1}` कंपोज़िट फ़ॉन्ट का नाम नहीं हो सकता
|
||||
@msg {k}: give a font {k}: एक फ़ॉन्ट दें
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: एक फ़ॉन्ट दें
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size {_1} से {_2}% तक होना चाहिए
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size {_1} से {_2}% तक होना चाहिए
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline {_1} से {_2}% तक होना चाहिए
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline {_1} से {_2}% तक होना चाहिए
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: स्केल {_1} से {_2}% तक होना चाहिए
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: स्केल {_1} से {_2}% तक होना चाहिए
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: केवल kana, roman और numerals को स्केल किया जा सकता है
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: केवल kana, roman और numerals को स्केल किया जा सकता है
|
||||
@msg {k}: only kana can be scaled at the center {k}: केंद्र से केवल kana को स्केल किया जा सकता है
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: केंद्र से केवल kana को स्केल किया जा सकता है
|
||||
@msg no composite font कोई कंपोज़िट फ़ॉन्ट नहीं
|
||||
Delete Font फ़ॉन्ट हटाएँ
|
||||
Kanji कांजी
|
||||
Kana काना
|
||||
@@ -8049,6 +8128,7 @@ antiAlias Sharp शार्प
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ग्रुप के फ़िल या स्ट्रोक पर 3D इफ़ेक्ट एक्सपोर्ट करने लायक कुछ नहीं बनाता
|
||||
@msg The 3D object is too complex to export 3D ऑब्जेक्ट एक्सपोर्ट करने के लिए बहुत जटिल है
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) ऑब्जेक्ट पर कोई 3D इफ़ेक्ट नहीं है (एक्सट्रूडेड और बेवेल, रिवॉल्व, फुलाएँ या घुमाव)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` केवल `mode` वाला ऑब्जेक्ट होना चाहिए, जिसका मान shaded या wireframe हो
|
||||
@msg its 3D effect builds no solid इसका 3D इफ़ेक्ट कोई ठोस नहीं बनाता
|
||||
@msg bevel must be none or one of {_1} bevel को none या {_1} में से एक होना चाहिए
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} इस आर्ट से ठोस नहीं बना सकता: इसके सिंबल इंस्टेंस, ब्लेंड और क्लिपिंग ग्रुप में बहुत अधिक ऑब्जेक्ट हैं
|
||||
@@ -8950,6 +9030,124 @@ theme Appearance अपीयरेंस
|
||||
Grommets आईलेट
|
||||
Grommet Inset किनारे से आईलेट की दूरी
|
||||
Diameter व्यास
|
||||
|
||||
# Messages of the effects, 3D, image export, color profiles, swatch exchange files, color books and DXF import.
|
||||
@msg 3D effects require finite coordinates within 100 million points 3D इफ़ेक्ट्स के लिए 100 मिलियन पॉइंट के भीतर परिमित निर्देशांक चाहिए
|
||||
@msg 3D effects require one to ten lights 3D इफ़ेक्ट्स के लिए एक से दस लाइटें चाहिए
|
||||
@msg BMP depth {_1}: 1, 4, 8, 16, 24 or 32 bits per pixel BMP डेप्थ {_1}: प्रति पिक्सेल 1, 4, 8, 16, 24 या 32 बिट
|
||||
@msg BMP encoding failed: {width} × {height} pixels doesn't fit the format or the pixel buffer BMP एन्कोडिंग विफल: {width} × {height} पिक्सेल न फ़ॉर्मैट में फ़िट होते हैं, न पिक्सेल बफ़र में
|
||||
@msg GIF encoding failed: the palette image is malformed GIF एन्कोडिंग विफल: पैलेट इमेज विकृत है
|
||||
@msg GIF encoding failed: {e} GIF एन्कोडिंग विफल: {e}
|
||||
@msg JPEG colour profile: {e} JPEG कलर प्रोफ़ाइल: {e}
|
||||
@msg JPEG encoding failed: pixel buffer doesn't match the image size JPEG एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता
|
||||
@msg JPEG encoding failed: {e} JPEG एन्कोडिंग विफल: {e}
|
||||
@msg PNG encoding failed: pixel buffer doesn't match the image size PNG एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता
|
||||
@msg PNG encoding failed: the palette image is malformed PNG एन्कोडिंग विफल: पैलेट इमेज विकृत है
|
||||
@msg PNG encoding failed: {e} PNG एन्कोडिंग विफल: {e}
|
||||
@msg PNG encoding failed: {width} × {height} pixels is not a valid image size PNG एन्कोडिंग विफल: {width} × {height} पिक्सेल मान्य इमेज आकार नहीं है
|
||||
@msg PSD colour profile: {e} PSD कलर प्रोफ़ाइल: {e}
|
||||
@msg PSD encoding failed: the pixel buffer doesn't match the image size PSD एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता
|
||||
@msg Revolve profile contains invalid coordinates रिवॉल्व प्रोफ़ाइल में अमान्य निर्देशांक हैं
|
||||
@msg Revolve profile is too complex रिवॉल्व प्रोफ़ाइल बहुत जटिल है
|
||||
@msg Revolve projection is not finite रिवॉल्व प्रोजेक्शन परिमित नहीं है
|
||||
@msg Revolve requires finite profile coordinates within 100 million points रिवॉल्व के लिए 100 मिलियन पॉइंट के भीतर परिमित प्रोफ़ाइल निर्देशांक चाहिए
|
||||
@msg Revolve surface is too complex; reduce Segments or simplify the profile रिवॉल्व सतह बहुत जटिल है; सेगमेंट कम करें या प्रोफ़ाइल सरल करें
|
||||
@msg Revolve surface is too complex; simplify the profile रिवॉल्व सतह बहुत जटिल है; प्रोफ़ाइल सरल करें
|
||||
@msg Revolve visibility cannot split the projected boundary रिवॉल्व दृश्यता प्रोजेक्ट की गई सीमा को विभाजित नहीं कर सकती
|
||||
@msg Revolve visibility creates too many fragments रिवॉल्व दृश्यता बहुत अधिक टुकड़े बनाती है
|
||||
@msg Revolve visibility creates too many vector anchors रिवॉल्व दृश्यता बहुत अधिक वेक्टर एंकर बनाती है
|
||||
@msg Revolve visibility exceeds its geometry work limit रिवॉल्व दृश्यता अपनी ज्यामिति गणना की सीमा से आगे निकल जाती है
|
||||
@msg Revolve visibility exceeds its spatial index limit रिवॉल्व दृश्यता अपनी स्पेशियल इंडेक्स सीमा से आगे निकल जाती है
|
||||
@msg Revolve visibility has too many boundary crossings रिवॉल्व दृश्यता में सीमा के बहुत अधिक क्रॉसिंग हैं
|
||||
@msg Revolve visibility has too many input anchors रिवॉल्व दृश्यता में बहुत अधिक इनपुट एंकर हैं
|
||||
@msg Revolve visibility requires convex cap sectors रिवॉल्व दृश्यता के लिए उत्तल कैप सेक्टर चाहिए
|
||||
@msg Revolve visibility requires finite projected faces रिवॉल्व दृश्यता के लिए परिमित प्रोजेक्ट किए गए फ़ेस चाहिए
|
||||
@msg SVG options: {e} SVG ऑप्शन्स: {e}
|
||||
@msg Save for Web settings: {e} वेब के लिए सेव करें की सेटिंग्स: {e}
|
||||
@msg TIFF colour profile: {e} TIFF कलर प्रोफ़ाइल: {e}
|
||||
@msg TIFF encoding failed: the pixel buffer doesn't match the image size TIFF एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता
|
||||
@msg TIFF encoding failed: {e} TIFF एन्कोडिंग विफल: {e}
|
||||
@msg Targa depth {_1}: 16, 24 or 32 bits per pixel Targa डेप्थ {_1}: प्रति पिक्सेल 16, 24 या 32 बिट
|
||||
@msg Targa encoding failed: the pixel buffer doesn't match the image size Targa एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता
|
||||
@msg The 3D projection is not finite 3D प्रोजेक्शन परिमित नहीं है
|
||||
@msg The 3D shadow is too complex; turn Shadows off or simplify the art 3D छाया बहुत जटिल है; छायाएँ बंद करें या आर्टवर्क सरल करें
|
||||
@msg The shape contains invalid coordinates शेप में अमान्य निर्देशांक हैं
|
||||
@msg Too many Revolve faces for visibility evaluation दृश्यता की गणना के लिए रिवॉल्व के फ़ेस बहुत अधिक हैं
|
||||
@msg VectorCraft reads at most {ASE_MAX_ENTRIES} colors and groups from a swatch exchange file, and this library has {entries} VectorCraft स्वॉच एक्सचेंज फ़ाइल से अधिकतम {ASE_MAX_ENTRIES} रंग और ग्रुप पढ़ता है, और इस लाइब्रेरी में {entries} हैं
|
||||
@msg WebP encoding failed: {e} WebP एन्कोडिंग विफल: {e}
|
||||
@msg [{j}] `{_1}`: {why} [{j}] `{_1}`: {why}
|
||||
@msg `index` must be a whole number `index` पूरी संख्या होनी चाहिए
|
||||
@msg `view` is smallThumb, mediumThumb, largeThumb, smallList or largeList `view` smallThumb, mediumThumb, largeThumb, smallList या largeList होता है
|
||||
@msg a line can't run from a point to itself कोई लाइन किसी पॉइंट से उसी पॉइंट तक नहीं जा सकती
|
||||
@msg a line needs at least two points लाइन के लिए कम से कम दो पॉइंट चाहिए
|
||||
@msg a number cannot be read एक संख्या पढ़ी नहीं जा सकती
|
||||
@msg a swatch exchange block is too long स्वॉच एक्सचेंज का एक ब्लॉक बहुत लंबा है
|
||||
@msg a swatch exchange name is too long स्वॉच एक्सचेंज का एक नाम बहुत लंबा है
|
||||
@msg a {sw} × {sh} image can't be resized to {width} × {height} pixels {sw} × {sh} इमेज का आकार बदलकर {width} × {height} पिक्सेल नहीं किया जा सकता
|
||||
@msg a {width} × {height} image is too large {width} × {height} इमेज बहुत बड़ी है
|
||||
@msg a {width} × {height} image: {e} {width} × {height} इमेज: {e}
|
||||
@msg an image could not be written: {e} एक इमेज लिखी नहीं जा सकी: {e}
|
||||
@msg an image that couldn't be read was left out जो इमेज पढ़ी नहीं जा सकी, उसे छोड़ दिया गया
|
||||
@msg can't open “{name}”: {e} “{name}” ओपन नहीं किया जा सकता: {e}
|
||||
@msg cannot align zip entries to {align} bytes ज़िप एंट्री को {align} बाइट पर अलाइन नहीं किया जा सकता
|
||||
@msg cannot open the automation {kind} root `{_1}`: {e} ऑटोमेशन {kind} रूट `{_1}` ओपन नहीं किया जा सकता: {e}
|
||||
@msg cannot read profile: {0} प्रोफ़ाइल पढ़ी नहीं जा सकती: {0}
|
||||
@msg color book version {version} isn't supported (only 1) कलर बुक वर्ज़न {version} समर्थित नहीं है (केवल 1)
|
||||
@msg color books in color space {other} aren't supported (only RGB, CMYK and Lab) कलर स्पेस {other} वाली कलर बुक समर्थित नहीं हैं (केवल RGB, CMYK और Lab)
|
||||
@msg file name too long to zip: {_1} ज़िप करने के लिए फ़ाइल का नाम बहुत लंबा है: {_1}
|
||||
@msg file name too long to zip: {name} ज़िप करने के लिए फ़ाइल का नाम बहुत लंबा है: {name}
|
||||
@msg freeform line {i}: {e} फ्रीफॉर्म लाइन {i}: {e}
|
||||
@msg freeform point {i}: {e} फ्रीफॉर्म पॉइंट {i}: {e}
|
||||
@msg give `{key}` or `{other}`, not both `{key}` या `{other}` दें, दोनों नहीं
|
||||
@msg grayscale profiles can't be working spaces yet ग्रेस्केल प्रोफ़ाइल अभी वर्किंग स्पेस नहीं हो सकतीं
|
||||
@msg input contains NaN or infinite coordinates इनपुट में NaN या अनंत निर्देशांक हैं
|
||||
@msg input path could not be closed इनपुट पाथ बंद नहीं किया जा सका
|
||||
@msg invalid ICC profile: {0} अमान्य ICC प्रोफ़ाइल: {0}
|
||||
@msg invalid scale {scale} अमान्य स्केल {scale}
|
||||
@msg its localized names take more than {_1} KB इसके स्थानीयकृत नाम {_1} KB से अधिक जगह लेते हैं
|
||||
@msg its objects are nested too deep ({e}) इसके ऑब्जेक्ट बहुत गहराई तक नेस्ट किए गए हैं ({e})
|
||||
@msg its tag table lists the `{_1}` tag more than once इसकी टैग तालिका में `{_1}` टैग एक से अधिक बार है
|
||||
@msg knockout groups with brushed strokes over other translucent elements are written as PDF knockout groups: their translucent elements knock out what they cover fully जिन नॉकआउट ग्रुप में ब्रश स्ट्रोक दूसरे पारदर्शी एलिमेंट के ऊपर हैं, वे PDF नॉकआउट ग्रुप के रूप में लिखे जाते हैं: उनके पारदर्शी एलिमेंट अपने नीचे का पूरा हिस्सा नॉकआउट करते हैं
|
||||
@msg knockout groups with too many overlapping translucent elements are written as PDF knockout groups: their translucent elements knock out what they cover fully बहुत अधिक ओवरलैप करने वाले पारदर्शी एलिमेंट वाले नॉकआउट ग्रुप PDF नॉकआउट ग्रुप के रूप में लिखे जाते हैं: उनके पारदर्शी एलिमेंट अपने नीचे का पूरा हिस्सा नॉकआउट करते हैं
|
||||
@msg not a graphic style library: {e} यह ग्राफ़िक स्टाइल लाइब्रेरी नहीं है: {e}
|
||||
@msg not a swatch library (.vcswatches or .gpl) यह स्वॉच लाइब्रेरी नहीं है (.vcswatches या .gpl)
|
||||
@msg not a swatch library (format `{_1}`) यह स्वॉच लाइब्रेरी नहीं है (फ़ॉर्मैट `{_1}`)
|
||||
@msg not a swatch library: {e} यह स्वॉच लाइब्रेरी नहीं है: {e}
|
||||
@msg not an action set file ({e}) यह एक्शन सेट फ़ाइल नहीं है ({e})
|
||||
@msg profile colour space {other} is not RGB, CMYK or Gray प्रोफ़ाइल का कलर स्पेस {other} RGB, CMYK या Gray नहीं है
|
||||
@msg regular expression too large रेगुलर एक्सप्रेशन बहुत बड़ा है
|
||||
@msg script file is not UTF-8 text: {value} स्क्रिप्ट फ़ाइल UTF-8 टेक्स्ट नहीं है: {value}
|
||||
@msg stop {i}: {e} स्टॉप {i}: {e}
|
||||
@msg swatch exchange version {major}.{minor} isn't supported (only 1.x) स्वॉच एक्सचेंज वर्ज़न {major}.{minor} समर्थित नहीं है (केवल 1.x)
|
||||
@msg the color book holds more than {ASE_MAX_ENTRIES} colors कलर बुक में {ASE_MAX_ENTRIES} से अधिक रंग हैं
|
||||
@msg the color book is cut short कलर बुक अधूरी है
|
||||
@msg the color book's names take more than {_1} MB कलर बुक के नाम {_1} MB से अधिक जगह लेते हैं
|
||||
@msg the image would be {w} × {h} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {_1} megapixels: lower the scale or resolution इमेज {w} × {h} पिक्सेल की होगी; रास्टर एक्सपोर्ट हर तरफ़ अधिकतम {MAX_RASTER_SIDE} पिक्सेल और {_1} मेगापिक्सेल तक सीमित हैं: स्केल या रिज़ॉल्यूशन कम करें
|
||||
@msg the optimised image can't be read back: {e} ऑप्टिमाइज़ की गई इमेज वापस पढ़ी नहीं जा सकती: {e}
|
||||
@msg the pixel buffer doesn't match the image size पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता
|
||||
@msg the preview: {e} प्रीव्यू: {e}
|
||||
@msg the request may not read the New Document profiles folder अनुरोध नया डॉक्यूमेंट प्रोफ़ाइल फ़ोल्डर नहीं पढ़ सकता
|
||||
@msg the shapes are too degenerate to combine शेप इतने विकृत हैं कि उन्हें मिलाया नहीं जा सकता
|
||||
@msg the shapes cross themselves and each other too often to combine शेप खुद को और एक-दूसरे को इतनी बार काटते हैं कि उन्हें मिलाया नहीं जा सकता
|
||||
@msg the swatch exchange file holds more than {ASE_MAX_ENTRIES} colors and groups स्वॉच एक्सचेंज फ़ाइल में {ASE_MAX_ENTRIES} से अधिक रंग और ग्रुप हैं
|
||||
@msg the swatch exchange file is cut short स्वॉच एक्सचेंज फ़ाइल अधूरी है
|
||||
@msg the {_1} plate can't be read: {e} {_1} प्लेट पढ़ी नहीं जा सकती: {e}
|
||||
@msg this binary DXF file is damaged or cut short यह बाइनरी DXF फ़ाइल क्षतिग्रस्त या अधूरी है
|
||||
@msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that यह एक DWG ड्रॉइंग है, जिसे पढ़ा नहीं जा सकता: इसे CAD ऐप से DXF के रूप में सेव करें और वह फ़ाइल ओपन करें
|
||||
@msg too many classes बहुत अधिक क्लास
|
||||
@msg too many groups बहुत अधिक ग्रुप
|
||||
@msg too many layers for PSD (at most 32767) PSD के लिए बहुत अधिक लेयर्स (अधिकतम 32767)
|
||||
@msg too many swatch exchange blocks स्वॉच एक्सचेंज के बहुत अधिक ब्लॉक
|
||||
@msg unknown colour profile: {0} अज्ञात कलर प्रोफ़ाइल: {0}
|
||||
@msg {_1} clashes with a built-in profile name {_1} किसी अंतर्निहित प्रोफ़ाइल के नाम से टकराता है
|
||||
@msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} पिक्सेल GIF के लिए बहुत बड़ा है (हर तरफ़ अधिकतम 65535 पिक्सेल): रिज़ॉल्यूशन कम करें
|
||||
@msg {_1}: no usable transform to/from sRGB {_1}: sRGB में या sRGB से बदलने का कोई उपयोगी ट्रांसफ़ॉर्म नहीं है
|
||||
@msg {name} is not a CMYK profile {name} CMYK प्रोफ़ाइल नहीं है
|
||||
@msg {name} is not an RGB profile {name} RGB प्रोफ़ाइल नहीं है
|
||||
@msg {path}: could not be read {path}: पढ़ा नहीं जा सका
|
||||
@msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} पिक्सेल JPEG के लिए बहुत बड़ा है (हर तरफ़ अधिकतम 65535 पिक्सेल): रिज़ॉल्यूशन कम करें
|
||||
@msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} पिक्सेल Targa के लिए बहुत बड़ा है (हर तरफ़ अधिकतम 65535 पिक्सेल): रिज़ॉल्यूशन कम करें
|
||||
@msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} पिक्सेल PSD के लिए बहुत बड़ा है (हर तरफ़ अधिकतम {MAX_SIDE} पिक्सेल): रिज़ॉल्यूशन कम करें
|
||||
Nest Shapes… आकृतियाँ सटाकर रखें…
|
||||
Nest Shapes आकृतियाँ सटाकर रखें
|
||||
Roll रोल
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1482,6 +1482,7 @@ font-technology Variable Változó
|
||||
Rotate (Classic)… Elforgatás (klasszikus)…
|
||||
Surface Felület
|
||||
Wireframe Drótváz
|
||||
Render Settings Renderelési beállítások
|
||||
No Shading Nincs árnyékolás
|
||||
Plastic Műanyag
|
||||
Highlight Intensity Csillanás erőssége
|
||||
@@ -1767,6 +1768,40 @@ file-filter Templates Sablonok
|
||||
Graph Diagram
|
||||
Graph Data Diagramadatok
|
||||
Graph Type Diagramtípus
|
||||
Graph Options Diagrambeállítások
|
||||
Left Axis Bal tengely
|
||||
Right Axis Jobb tengely
|
||||
Category Axis Kategóriatengely
|
||||
Separate Scales Külön skálák
|
||||
Add Drop Shadow Vetett árnyék hozzáadása
|
||||
Add Legend Across Top Jelmagyarázat felül
|
||||
First Row in Front Első sor elöl
|
||||
First Column in Front Első oszlop elöl
|
||||
Legend Jelmagyarázat
|
||||
Column Width Oszlopszélesség
|
||||
Bar Width Sávszélesség
|
||||
Cluster Width Csoportszélesség
|
||||
Mark Data Points Adatpontok jelölése
|
||||
Connect Data Points Adatpontok összekötése
|
||||
Edge-to-Edge Lines Széltől szélig érő vonalak
|
||||
Draw Filled Lines Kitöltött vonalak rajzolása
|
||||
Tick Values Osztásértékek
|
||||
Override Calculated Values Számított értékek felülírása
|
||||
Divisions Osztások
|
||||
Tick Marks Osztásjelek
|
||||
Tick Marks per Division Osztásjelek osztásonként
|
||||
Draw Tick Marks Between Labels Osztásjelek a címkék között
|
||||
Add Labels Címkék hozzáadása
|
||||
Maximum must be more than Minimum. A maximumnak nagyobbnak kell lennie a minimumnál.
|
||||
Column Graph Oszlopdiagram
|
||||
Stacked Column Graph Halmozott oszlopdiagram
|
||||
Bar Graph Sávdiagram
|
||||
Stacked Bar Graph Halmozott sávdiagram
|
||||
Line Graph Vonaldiagram
|
||||
Area Graph Területdiagram
|
||||
Scatter Graph Pontdiagram
|
||||
Pie Graph Kördiagram
|
||||
Radar Graph Sugárdiagram
|
||||
Select Segments Szakaszok kijelölése
|
||||
Artistic Művészi
|
||||
Film Grain… Filmszemcse…
|
||||
@@ -1953,6 +1988,7 @@ file-filter Templates Sablonok
|
||||
Highlight Substituted Fonts Helyettesített betűtípusok kiemelése
|
||||
Highlight Substituted Glyphs Helyettesített glifák kiemelése
|
||||
Highlight Variable Változó kiemelése
|
||||
Sort Variables Változók rendezése
|
||||
Hindi Hindi
|
||||
History Előzmények
|
||||
History Options… Előzmények beállításai…
|
||||
@@ -2531,6 +2567,7 @@ file-filter Templates Sablonok
|
||||
Menu Dialog Menü párbeszédablaka
|
||||
Merge Összeolvasztás
|
||||
Merge colors into one row Színek egyesítése egy sorba
|
||||
New Row Új sor
|
||||
Merge Graphic Styles Grafikai stílusok egyesítése
|
||||
Merge Graphic Styles… Grafikai stílusok egyesítése…
|
||||
Merge Layers Rétegek egyesítése
|
||||
@@ -4373,6 +4410,48 @@ mark-style Japanese Japán
|
||||
Composite Fonts Összetett betűtípusok
|
||||
Export Composite Fonts Összetett betűtípusok exportálása
|
||||
Import Composite Fonts Összetett betűtípusok importálása
|
||||
@msg each font must be {name, parts} minden betűtípusnak {name, parts} alakúnak kell lennie
|
||||
@msg unknown key `{k}`; a font takes name and parts ismeretlen kulcs: `{k}`; egy betűtípus name és parts kulcsot fogad
|
||||
@msg each font needs a name minden betűtípusnak kell egy név
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nem lehet összetett betűtípus neve (1–{COMPOSITE_NAME_MAX} karakter, vezérlőkarakterek nélkül)
|
||||
@msg `{name}`: parts must be an object `{name}`: a parts értékének objektumnak kell lennie
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: ismeretlen osztály: `{k}`; az osztályok: kanji, kana, punctuation, symbols, roman és numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: a(z) {_1} értékének objektumnak kell lennie
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: ismeretlen kulcs: `{k}` itt: {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: adja meg a kanji részt
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts egy dokumentum legfeljebb {COMPOSITE_FONTS_MAX} összetett betűtípust tartalmazhat
|
||||
@msg two composite fonts are named `{_1}` két összetett betűtípus neve is `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` egy telepített betűtípus neve
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: a(z) {_2} a(z) `{font}` összetett betűtípust használja
|
||||
@msg give fonts: [{name, parts}…] adja meg: fonts: [{name, parts}…]
|
||||
@msg path is empty a path üres
|
||||
@msg fonts must be a list of composite fonts a fonts értékének összetett betűtípusok listájának kell lennie
|
||||
@msg names must be strings a names elemeinek karakterláncoknak kell lenniük
|
||||
@msg no composite font named `{n}` nincs `{n}` nevű összetett betűtípus
|
||||
@msg names must be a list of names a names értékének nevek listájának kell lennie
|
||||
@msg no composite fonts to export nincs exportálható összetett betűtípus
|
||||
@msg not a composite fonts file: {e} nem összetettbetűtípus-fájl: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list nem összetettbetűtípus-fájl: nincs compositeFonts lista
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts a fájl több mint {COMPOSITE_FONTS_MAX} összetett betűtípust tartalmaz
|
||||
@msg the file is larger than 4 MB a fájl nagyobb 4 MB-nál
|
||||
@msg dataBase64 is not base64 a dataBase64 nem base64
|
||||
@msg give one of path, data or dataBase64 adja meg a path, data vagy dataBase64 egyikét
|
||||
@msg the file holds no composite fonts a fájl nem tartalmaz összetett betűtípust
|
||||
@msg the file holds two composite fonts named `{_1}` a fájl két `{_1}` nevű összetett betűtípust tartalmaz
|
||||
@msg `{_1}` can't name a composite font `{_1}` nem lehet összetett betűtípus neve
|
||||
@msg {k}: give a font {k}: adjon meg egy betűtípust
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: adjon meg egy betűtípust
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: a size értéke {_1}–{_2}% lehet
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: a size értéke {_1}–{_2}% lehet
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: a baseline értéke {_1}–{_2}% lehet
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: a baseline értéke {_1}–{_2}% lehet
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: a méretezés {_1}–{_2}% lehet
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: a méretezés {_1}–{_2}% lehet
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: csak a kana, roman és numerals méretezhető
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: csak a kana, roman és numerals méretezhető
|
||||
@msg {k}: only kana can be scaled at the center {k}: csak a kana méretezhető a középpontból
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: csak a kana méretezhető a középpontból
|
||||
@msg no composite font nincs összetett betűtípus
|
||||
Delete Font Betűtípus törlése
|
||||
Kanji Kandzsi
|
||||
Kana Kana
|
||||
@@ -6011,6 +6090,7 @@ imageTrace Outline Körvonal
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export a csoport kitöltésén vagy körvonalán lévő 3D effektus nem rajzol semmi exportálhatót
|
||||
@msg The 3D object is too complex to export A 3D objektum túl összetett az exportáláshoz
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) az objektumnak nincs 3D effektusa (Kihúzás és élletörés, Forgatás tengely körül, Felfújás vagy Elforgatás)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe a `render` csak `mode` mezőt tartalmazó objektum lehet, shaded vagy wireframe értékkel
|
||||
@msg its 3D effect builds no solid a 3D effektusa nem hoz létre testet
|
||||
@msg bevel must be none or one of {_1} a bevel értéke none vagy ezek egyike lehet: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: ebből a rajzból nem hozható létre test: a szimbólumpéldányai, összemosásai és vágócsoportjai túl sok objektumot tartalmaznak
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -480,6 +480,7 @@
|
||||
Rotate (Classic)… Putar (klasik)…
|
||||
Surface Permukaan
|
||||
Wireframe Kerangka kawat
|
||||
Render Settings Pengaturan render
|
||||
No Shading Tanpa pengarsiran
|
||||
Plastic Plastik
|
||||
Highlight Intensity Intensitas kilau
|
||||
@@ -2712,6 +2713,7 @@ digits Malayalam Malayalam
|
||||
Limit to Library: Batasi ke pustaka:
|
||||
Link harmony colors Tautkan warna harmoni
|
||||
Merge colors into one row Gabungkan warna ke dalam satu baris
|
||||
New Row Baris baru
|
||||
No colors to recolor (preserved colors are left out). Tidak ada warna untuk diwarnai ulang (warna yang dipertahankan dikesampingkan).
|
||||
Preserve: Pertahankan:
|
||||
Randomly change color order Ubah urutan warna secara acak
|
||||
@@ -3361,6 +3363,40 @@ theme Appearance Tampilan
|
||||
Create Graph Buat grafik
|
||||
Graph Data Data grafik
|
||||
Graph Type Jenis grafik
|
||||
Graph Options Opsi grafik
|
||||
Left Axis Sumbu kiri
|
||||
Right Axis Sumbu kanan
|
||||
Category Axis Sumbu kategori
|
||||
Separate Scales Skala terpisah
|
||||
Add Drop Shadow Tambahkan bayangan jatuh
|
||||
Add Legend Across Top Tambahkan legenda di atas
|
||||
First Row in Front Baris pertama di depan
|
||||
First Column in Front Kolom pertama di depan
|
||||
Legend Legenda
|
||||
Column Width Lebar kolom
|
||||
Bar Width Lebar batang
|
||||
Cluster Width Lebar kelompok
|
||||
Mark Data Points Tandai titik data
|
||||
Connect Data Points Hubungkan titik data
|
||||
Edge-to-Edge Lines Garis dari tepi ke tepi
|
||||
Draw Filled Lines Gambar garis terisi
|
||||
Tick Values Nilai tanda skala
|
||||
Override Calculated Values Timpa nilai terhitung
|
||||
Divisions Pembagian
|
||||
Tick Marks Tanda skala
|
||||
Tick Marks per Division Tanda skala per pembagian
|
||||
Draw Tick Marks Between Labels Gambar tanda skala di antara label
|
||||
Add Labels Tambahkan label
|
||||
Maximum must be more than Minimum. Maksimum harus lebih besar dari minimum.
|
||||
Column Graph Grafik kolom
|
||||
Stacked Column Graph Grafik kolom bertumpuk
|
||||
Bar Graph Grafik batang
|
||||
Stacked Bar Graph Grafik batang bertumpuk
|
||||
Line Graph Grafik garis
|
||||
Area Graph Grafik area
|
||||
Scatter Graph Grafik sebar
|
||||
Pie Graph Grafik pai
|
||||
Radar Graph Grafik radar
|
||||
Vector Halftone Halftone vektor
|
||||
spot spot
|
||||
Text Teks
|
||||
@@ -3948,6 +3984,7 @@ axis Both Keduanya
|
||||
Replace: Ganti:
|
||||
Type on a Path Options Opsi teks pada jalur
|
||||
Highlight Variable Sorot variabel
|
||||
Sort Variables Urutkan variabel
|
||||
New Variable Variabel baru
|
||||
List Variables Daftar variabel
|
||||
Bind Variable Ikat variabel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2461 and column 5.
|
@@ -1195,6 +1195,7 @@ font-technology Variable Variabili
|
||||
Rotate (Classic)… Ruota (classico)…
|
||||
Surface Superficie
|
||||
Wireframe Reticolo
|
||||
Render Settings Impostazioni di rendering
|
||||
No Shading Nessuna ombreggiatura
|
||||
Plastic Plastica
|
||||
Highlight Intensity Intensità lucentezza
|
||||
@@ -1441,6 +1442,40 @@ file-filter Templates Modelli
|
||||
Graph Grafico
|
||||
Graph Data Dati grafico
|
||||
Graph Type Tipo di grafico
|
||||
Graph Options Opzioni grafico
|
||||
Left Axis Asse sinistro
|
||||
Right Axis Asse destro
|
||||
Category Axis Asse delle categorie
|
||||
Separate Scales Scale separate
|
||||
Add Drop Shadow Aggiungi ombra esterna
|
||||
Add Legend Across Top Aggiungi legenda in alto
|
||||
First Row in Front Prima riga in primo piano
|
||||
First Column in Front Prima colonna in primo piano
|
||||
Legend Legenda
|
||||
Column Width Larghezza colonne
|
||||
Bar Width Larghezza barre
|
||||
Cluster Width Larghezza gruppi
|
||||
Mark Data Points Contrassegna punti dati
|
||||
Connect Data Points Collega punti dati
|
||||
Edge-to-Edge Lines Linee da bordo a bordo
|
||||
Draw Filled Lines Disegna linee piene
|
||||
Tick Values Valori delle tacche
|
||||
Override Calculated Values Sostituisci valori calcolati
|
||||
Divisions Divisioni
|
||||
Tick Marks Tacche
|
||||
Tick Marks per Division Tacche per divisione
|
||||
Draw Tick Marks Between Labels Disegna tacche tra le etichette
|
||||
Add Labels Aggiungi etichette
|
||||
Maximum must be more than Minimum. Il massimo deve essere maggiore del minimo.
|
||||
Column Graph Grafico a colonne
|
||||
Stacked Column Graph Grafico a colonne in pila
|
||||
Bar Graph Grafico a barre
|
||||
Stacked Bar Graph Grafico a barre in pila
|
||||
Line Graph Grafico a linee
|
||||
Area Graph Grafico ad area
|
||||
Scatter Graph Grafico a dispersione
|
||||
Pie Graph Grafico a torta
|
||||
Radar Graph Grafico radar
|
||||
Graph Design Design del grafico
|
||||
Graph Marker Indicatore del grafico
|
||||
New Design Nuovo design
|
||||
@@ -1561,6 +1596,7 @@ file-filter Templates Modelli
|
||||
Highlight anchors on mouse over Evidenzia ancoraggi al passaggio del mouse
|
||||
Highlight Rows Evidenzia righe
|
||||
Highlight Variable Evidenzia variabile
|
||||
Sort Variables Ordina variabili
|
||||
Highlight Substituted Fonts Evidenzia font sostituiti
|
||||
Highlight Substituted Glyphs Evidenzia glifi sostituiti
|
||||
History Cronologia
|
||||
@@ -2055,6 +2091,7 @@ file-filter Templates Modelli
|
||||
Menu Dialog Finestra di dialogo menu
|
||||
Merge Fondi
|
||||
Merge colors into one row Unisci colori in una riga
|
||||
New Row Nuova riga
|
||||
Merge Graphic Styles Unisci stili grafici
|
||||
Merge Graphic Styles… Unisci stili grafici…
|
||||
Merge Layers Unisci livelli
|
||||
@@ -3751,6 +3788,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Font compositi
|
||||
Export Composite Fonts Esporta font compositi
|
||||
Import Composite Fonts Importa font compositi
|
||||
@msg each font must be {name, parts} ogni font deve essere {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts chiave sconosciuta `{k}`; un font accetta name e parts
|
||||
@msg each font needs a name ogni font deve avere un nome
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` non può essere il nome di un font composito (da 1 a {COMPOSITE_NAME_MAX} caratteri, senza caratteri di controllo)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts deve essere un oggetto
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe sconosciuta `{k}`; le classi sono kanji, kana, punctuation, symbols, roman e numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} deve essere un oggetto
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: chiave sconosciuta `{k}` in {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: indica la parte kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un documento contiene al massimo {COMPOSITE_FONTS_MAX} font compositi
|
||||
@msg two composite fonts are named `{_1}` due font compositi si chiamano `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` è il nome di un font installato
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, un font composito
|
||||
@msg give fonts: [{name, parts}…] indica fonts: [{name, parts}…]
|
||||
@msg path is empty path è vuoto
|
||||
@msg fonts must be a list of composite fonts fonts deve essere un elenco di font compositi
|
||||
@msg names must be strings names deve contenere stringhe
|
||||
@msg no composite font named `{n}` nessun font composito di nome `{n}`
|
||||
@msg names must be a list of names names deve essere un elenco di nomi
|
||||
@msg no composite fonts to export nessun font composito da esportare
|
||||
@msg not a composite fonts file: {e} non è un file di font compositi: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list non è un file di font compositi: manca l’elenco compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts il file contiene più di {COMPOSITE_FONTS_MAX} font compositi
|
||||
@msg the file is larger than 4 MB il file supera 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 non è base64
|
||||
@msg give one of path, data or dataBase64 indica path, data o dataBase64
|
||||
@msg the file holds no composite fonts il file non contiene font compositi
|
||||
@msg the file holds two composite fonts named `{_1}` il file contiene due font compositi di nome `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` non può essere il nome di un font composito
|
||||
@msg {k}: give a font {k}: indica un font
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: indica un font
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size deve essere tra {_1} e {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size deve essere tra {_1} e {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline deve essere tra {_1} e {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline deve essere tra {_1} e {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: la scala deve essere tra {_1} e {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: la scala deve essere tra {_1} e {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman e numerals si possono ridimensionare
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: solo kana, roman e numerals si possono ridimensionare
|
||||
@msg {k}: only kana can be scaled at the center {k}: solo kana si può ridimensionare dal centro
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: solo kana si può ridimensionare dal centro
|
||||
@msg no composite font nessun font composito
|
||||
Delete Font Elimina font
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5644,6 +5723,7 @@ lighting Light Luce
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un effetto 3D sul riempimento o sulla traccia di un gruppo non disegna nulla da esportare
|
||||
@msg The 3D object is too complex to export L'oggetto 3D è troppo complesso per essere esportato
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l'oggetto non ha alcun effetto 3D (Estrusione e smusso, Rivoluzione, Gonfia o Ruota)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` deve essere un oggetto che contiene solo `mode`, con valore shaded o wireframe
|
||||
@msg its 3D effect builds no solid il suo effetto 3D non costruisce alcun solido
|
||||
@msg bevel must be none or one of {_1} bevel deve essere none o uno dei seguenti: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} non può costruire solidi da questo disegno: le sue istanze di simbolo, fusioni e gruppi di ritaglio contengono troppi oggetti
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -570,6 +570,7 @@ digits Malayalam マラヤーラム数字
|
||||
Rotate (Classic)… 回転(クラシック)…
|
||||
Surface 表面
|
||||
Wireframe ワイヤーフレーム
|
||||
Render Settings レンダリング設定
|
||||
No Shading 陰影なし
|
||||
Plastic プラスチック
|
||||
Highlight Intensity ハイライトの強さ
|
||||
@@ -1945,6 +1946,7 @@ antiAlias Sharp シャープ
|
||||
Highlight Alternate Glyphs 代替字形をハイライト
|
||||
Highlight Rows 行をハイライト
|
||||
Highlight Variable 変数をハイライト表示
|
||||
Sort Variables 変数を並べ替え
|
||||
Highlight Substituted Fonts 置換フォントをハイライト
|
||||
Highlight Substituted Glyphs 置換字形をハイライト
|
||||
Highlight anchors on mouse over マウスオーバー時にアンカーをハイライト
|
||||
@@ -2324,6 +2326,7 @@ antiAlias Sharp シャープ
|
||||
Merge Selected 選択レイヤーを結合
|
||||
Merge Swatches スウォッチを統合
|
||||
Merge colors into one row カラーを 1 行に統合
|
||||
New Row 新規行
|
||||
Mesh Tool メッシュツール
|
||||
Message: メッセージ:
|
||||
Metadata: メタデータ:
|
||||
@@ -3859,6 +3862,40 @@ file-filter Templates テンプレート
|
||||
Gradient (. or Shift+.) グラデーション (. または Shift+.)
|
||||
Graph Data グラフデータ
|
||||
Graph Type グラフの種類
|
||||
Graph Options グラフオプション
|
||||
Left Axis 左軸
|
||||
Right Axis 右軸
|
||||
Category Axis 項目軸
|
||||
Separate Scales 目盛りを別にする
|
||||
Add Drop Shadow ドロップシャドウを追加
|
||||
Add Legend Across Top 凡例を上に表示
|
||||
First Row in Front 最初の行を前面に
|
||||
First Column in Front 最初の列を前面に
|
||||
Legend 凡例
|
||||
Column Width 棒の幅
|
||||
Bar Width 横棒の幅
|
||||
Cluster Width グループの幅
|
||||
Mark Data Points データポイントにマーカーを表示
|
||||
Connect Data Points データポイントを線でつなぐ
|
||||
Edge-to-Edge Lines 端から端までの線
|
||||
Draw Filled Lines 線を塗りつぶして描く
|
||||
Tick Values 目盛りの値
|
||||
Override Calculated Values 計算値を上書き
|
||||
Divisions 分割数
|
||||
Tick Marks 目盛り
|
||||
Tick Marks per Division 分割ごとの目盛り
|
||||
Draw Tick Marks Between Labels ラベルの間に目盛りを描く
|
||||
Add Labels ラベルを追加
|
||||
Maximum must be more than Minimum. 最大値は最小値より大きくしてください。
|
||||
Column Graph 棒グラフ
|
||||
Stacked Column Graph 積み上げ棒グラフ
|
||||
Bar Graph 横向き棒グラフ
|
||||
Stacked Bar Graph 横向き積み上げ棒グラフ
|
||||
Line Graph 折れ線グラフ
|
||||
Area Graph 面グラフ
|
||||
Scatter Graph 散布図
|
||||
Pie Graph 円グラフ
|
||||
Radar Graph レーダーチャート
|
||||
Graph Design グラフのデザイン
|
||||
Graph Marker グラフのマーカー
|
||||
New Design 新規デザイン
|
||||
@@ -4079,6 +4116,48 @@ file-filter Templates テンプレート
|
||||
Composite Fonts 合成フォント
|
||||
Export Composite Fonts 合成フォントを書き出し
|
||||
Import Composite Fonts 合成フォントを読み込み
|
||||
@msg each font must be {name, parts} 各フォントは {name, parts} の形にしてください
|
||||
@msg unknown key `{k}`; a font takes name and parts `{k}` は不明なキーです。フォントに使えるのは name と parts です
|
||||
@msg each font needs a name 各フォントには名前が必要です
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` は合成フォントの名前にできません (1~{COMPOSITE_NAME_MAX} 文字、制御文字なし)
|
||||
@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}`: {_1} must be an object `{name}`: {_1} はオブジェクトにしてください
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} の `{k}` は不明なキーです
|
||||
@msg `{name}`: give the kanji part `{name}`: kanji の部分を指定してください
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 1 つのドキュメントに置ける合成フォントは {COMPOSITE_FONTS_MAX} 個までです
|
||||
@msg two composite fonts are named `{_1}` `{_1}` という名前の合成フォントが 2 つあります
|
||||
@msg `{_1}` is the name of an installed font `{_1}` はインストール済みのフォントの名前です
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} に合成フォントの `{font}` が設定されています
|
||||
@msg give fonts: [{name, parts}…] fonts を指定してください: [{name, parts}…]
|
||||
@msg path is empty path が空です
|
||||
@msg fonts must be a list of composite fonts fonts は合成フォントのリストにしてください
|
||||
@msg names must be strings names は文字列にしてください
|
||||
@msg no composite font named `{n}` `{n}` という合成フォントはありません
|
||||
@msg names must be a list of names names は名前のリストにしてください
|
||||
@msg no composite fonts to export 書き出す合成フォントがありません
|
||||
@msg not a composite fonts file: {e} 合成フォントのファイルではありません: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list 合成フォントのファイルではありません: compositeFonts のリストがありません
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ファイルに {COMPOSITE_FONTS_MAX} 個を超える合成フォントがあります
|
||||
@msg the file is larger than 4 MB ファイルが 4 MB を超えています
|
||||
@msg dataBase64 is not base64 dataBase64 が base64 ではありません
|
||||
@msg give one of path, data or dataBase64 path、data、dataBase64 のいずれかを指定してください
|
||||
@msg the file holds no composite fonts ファイルに合成フォントがありません
|
||||
@msg the file holds two composite fonts named `{_1}` ファイルに `{_1}` という名前の合成フォントが 2 つあります
|
||||
@msg `{_1}` can't name a composite font `{_1}` は合成フォントの名前にできません
|
||||
@msg {k}: give a font {k}: フォントを指定してください
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: フォントを指定してください
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size は {_1}~{_2}% にしてください
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size は {_1}~{_2}% にしてください
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline は {_1}~{_2}% にしてください
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline は {_1}~{_2}% にしてください
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: 比率は {_1}~{_2}% にしてください
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: 比率は {_1}~{_2}% にしてください
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: 比率を変更できるのは kana、roman、numerals だけです
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: 比率を変更できるのは kana、roman、numerals だけです
|
||||
@msg {k}: only kana can be scaled at the center {k}: 中央を基準に比率を変更できるのは kana だけです
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: 中央を基準に比率を変更できるのは kana だけです
|
||||
@msg no composite font 合成フォントがありません
|
||||
Delete Font フォントを削除
|
||||
Kanji 漢字
|
||||
Kana かな
|
||||
@@ -5686,6 +5765,7 @@ lighting Light ライト
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export グループの塗りまたは線にある 3D 効果には書き出せる描画がありません
|
||||
@msg The 3D object is too complex to export 3D オブジェクトが複雑すぎるため書き出せません
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) オブジェクトに 3D 効果 (押し出しとベベル、回転体、膨張、回転) がありません
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` は `mode` だけを持ち、その値が shaded または wireframe のオブジェクトにしてください
|
||||
@msg its 3D effect builds no solid 3D 効果からソリッドが作成されません
|
||||
@msg bevel must be none or one of {_1} bevel には none または {_1} のいずれかを指定してください
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} はこのアートからソリッドを作成できません。シンボルインスタンス、ブレンド、クリッピンググループに含まれるオブジェクトが多すぎます
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -452,6 +452,7 @@
|
||||
Rotate (Classic)… შემობრუნება (კლასიკური)…
|
||||
Surface ზედაპირი
|
||||
Wireframe მავთულის ჩარჩო
|
||||
Render Settings რენდერის პარამეტრები
|
||||
No Shading დაჩრდილვის გარეშე
|
||||
Plastic პლასტიკური
|
||||
Highlight Intensity ელვარების ინტენსივობა
|
||||
@@ -1508,6 +1509,48 @@ digits Malayalam მალაიალამური
|
||||
Composite Fonts კომპოზიტური შრიფტები
|
||||
Export Composite Fonts კომპოზიტური შრიფტების ექსპორტი
|
||||
Import Composite Fonts კომპოზიტური შრიფტების იმპორტი
|
||||
@msg each font must be {name, parts} თითოეული შრიფტი უნდა იყოს {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts უცნობი გასაღები `{k}`; შრიფტი იღებს name-სა და parts-ს
|
||||
@msg each font needs a name თითოეულ შრიფტს სახელი სჭირდება
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ვერ იქნება კომპოზიტური შრიფტის სახელი (1-დან {COMPOSITE_NAME_MAX} სიმბოლომდე, მმართველი სიმბოლოების გარეშე)
|
||||
@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}`: {_1} must be an object `{name}`: {_1} უნდა იყოს ობიექტი
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: უცნობი გასაღები `{k}` ნაწილში {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: მიუთითეთ ნაწილი kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts დოკუმენტი შეიცავს არაუმეტეს {COMPOSITE_FONTS_MAX} კომპოზიტურ შრიფტს
|
||||
@msg two composite fonts are named `{_1}` ორ კომპოზიტურ შრიფტს ჰქვია `{_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 give fonts: [{name, parts}…] მიუთითეთ fonts: [{name, parts}…]
|
||||
@msg path is empty path ცარიელია
|
||||
@msg fonts must be a list of composite fonts fonts უნდა იყოს კომპოზიტური შრიფტების სია
|
||||
@msg names must be strings names უნდა იყოს სტრიქონები
|
||||
@msg no composite font named `{n}` კომპოზიტური შრიფტი სახელით `{n}` არ არსებობს
|
||||
@msg names must be a list of names names უნდა იყოს სახელების სია
|
||||
@msg no composite fonts to export საექსპორტო კომპოზიტური შრიფტები არ არის
|
||||
@msg not a composite fonts file: {e} ეს არ არის კომპოზიტური შრიფტების ფაილი: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ეს არ არის კომპოზიტური შრიფტების ფაილი: სია compositeFonts არ არის
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ფაილი შეიცავს {COMPOSITE_FONTS_MAX}-ზე მეტ კომპოზიტურ შრიფტს
|
||||
@msg the file is larger than 4 MB ფაილი 4 MB-ზე დიდია
|
||||
@msg dataBase64 is not base64 dataBase64 არ არის base64
|
||||
@msg give one of path, data or dataBase64 მიუთითეთ path, data ან dataBase64
|
||||
@msg the file holds no composite fonts ფაილი კომპოზიტურ შრიფტებს არ შეიცავს
|
||||
@msg the file holds two composite fonts named `{_1}` ფაილი შეიცავს ორ კომპოზიტურ შრიფტს სახელით `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` ვერ იქნება კომპოზიტური შრიფტის სახელი
|
||||
@msg {k}: give a font {k}: მიუთითეთ შრიფტი
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: მიუთითეთ შრიფტი
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: მასშტაბი უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: მასშტაბი უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: მასშტაბირება შეიძლება მხოლოდ kana, roman და numerals ნაწილებში
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: მასშტაბირება შეიძლება მხოლოდ kana, roman და numerals ნაწილებში
|
||||
@msg {k}: only kana can be scaled at the center {k}: ცენტრიდან მასშტაბირება შეიძლება მხოლოდ kana ნაწილში
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: ცენტრიდან მასშტაბირება შეიძლება მხოლოდ kana ნაწილში
|
||||
@msg no composite font კომპოზიტური შრიფტი არ არის
|
||||
Delete Font შრიფტის წაშლა
|
||||
Kanji კანძი
|
||||
Kana კანა
|
||||
@@ -2628,6 +2671,7 @@ digits Malayalam მალაიალამური
|
||||
Limit to Library: ბიბლიოთეკით შეზღუდვა:
|
||||
Link harmony colors ჰარმონიის ფერების დაკავშირება
|
||||
Merge colors into one row ფერების ერთ სტრიქონში შერწყმა
|
||||
New Row ახალი სტრიქონი
|
||||
No colors to recolor (preserved colors are left out). ხელახლა შესაფერადებელი ფერები არ არის (შენარჩუნებული ფერები გამოტოვებულია).
|
||||
Preserve: შენარჩუნება:
|
||||
Randomly change color order ფერების რიგის შემთხვევითი შეცვლა
|
||||
@@ -3207,6 +3251,40 @@ theme Appearance იერსახე
|
||||
Create Graph გრაფიკის შექმნა
|
||||
Graph Data გრაფიკის მონაცემები
|
||||
Graph Type გრაფიკის ტიპი
|
||||
Graph Options გრაფიკის პარამეტრები
|
||||
Left Axis მარცხენა ღერძი
|
||||
Right Axis მარჯვენა ღერძი
|
||||
Category Axis კატეგორიების ღერძი
|
||||
Separate Scales ცალკე სკალები
|
||||
Add Drop Shadow ჩრდილის დამატება
|
||||
Add Legend Across Top ლეგენდის დამატება ზემოთ
|
||||
First Row in Front პირველი მწკრივი წინ
|
||||
First Column in Front პირველი სვეტი წინ
|
||||
Legend ლეგენდა
|
||||
Column Width სვეტის სიგანე
|
||||
Bar Width ზოლის სიგანე
|
||||
Cluster Width ჯგუფის სიგანე
|
||||
Mark Data Points მონაცემთა წერტილების მონიშვნა
|
||||
Connect Data Points მონაცემთა წერტილების შეერთება
|
||||
Edge-to-Edge Lines ხაზები კიდიდან კიდემდე
|
||||
Draw Filled Lines შევსებული ხაზების დახატვა
|
||||
Tick Values დანაყოფების მნიშვნელობები
|
||||
Override Calculated Values გამოთვლილი მნიშვნელობების შეცვლა
|
||||
Divisions დანაყოფები
|
||||
Tick Marks დანაყოფის ნიშნები
|
||||
Tick Marks per Division ნიშნები თითო დანაყოფზე
|
||||
Draw Tick Marks Between Labels ნიშნების დახატვა წარწერებს შორის
|
||||
Add Labels წარწერების დამატება
|
||||
Maximum must be more than Minimum. მაქსიმუმი მინიმუმზე მეტი უნდა იყოს.
|
||||
Column Graph სვეტოვანი გრაფიკი
|
||||
Stacked Column Graph დაწყობილი სვეტოვანი გრაფიკი
|
||||
Bar Graph ზოლოვანი გრაფიკი
|
||||
Stacked Bar Graph დაწყობილი ზოლოვანი გრაფიკი
|
||||
Line Graph ხაზოვანი გრაფიკი
|
||||
Area Graph არეების გრაფიკი
|
||||
Scatter Graph გაფანტული გრაფიკი
|
||||
Pie Graph წრიული გრაფიკი
|
||||
Radar Graph რადარული გრაფიკი
|
||||
Vector Halftone ვექტორული ნახევარტონი
|
||||
spot სპოტ
|
||||
Text ტექსტი
|
||||
@@ -3769,6 +3847,7 @@ axis Both ორივე
|
||||
Replace: ჩანაცვლება:
|
||||
Type on a Path Options ტრაექტორიაზე ტექსტის პარამეტრები
|
||||
Highlight Variable ცვლადის გამოკვეთა
|
||||
Sort Variables ცვლადების დალაგება
|
||||
New Variable ახალი ცვლადი
|
||||
List Variables ცვლადების სია
|
||||
Bind Variable ცვლადის მიბმა
|
||||
@@ -5311,6 +5390,7 @@ font-technology Variable ცვლადი
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ჯგუფის შევსებაზე ან კონტურზე 3D ეფექტი ექსპორტისთვის არაფერს ხატავს
|
||||
@msg The 3D object is too complex to export 3D ობიექტი ზედმეტად რთულია ექსპორტისთვის
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) ობიექტს არ აქვს 3D ეფექტი (ამოწევა და წაკვეთა, ბრუნვა, გაბერვა ან შემობრუნება)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` უნდა იყოს ობიექტი, რომელიც შეიცავს მხოლოდ `mode`-ს, მნიშვნელობით shaded ან wireframe
|
||||
@msg its 3D effect builds no solid მისი 3D ეფექტი სხეულს არ ქმნის
|
||||
@msg bevel must be none or one of {_1} bevel უნდა იყოს none ან ერთ-ერთი: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: ამ ნახატიდან სხეულების შექმნა შეუძლებელია: მისი სიმბოლოს ეგზემპლარები, გადასვლები და მომჭრელი ჯგუფები ზედმეტად ბევრ ობიექტს შეიცავს
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -563,6 +563,7 @@ digits Malayalam 말라얄람어
|
||||
Rotate (Classic)… 회전(클래식)…
|
||||
Surface 표면
|
||||
Wireframe 와이어프레임
|
||||
Render Settings 렌더링 설정
|
||||
No Shading 셰이딩 없음
|
||||
Plastic 플라스틱
|
||||
Highlight Intensity 하이라이트 강도
|
||||
@@ -1935,6 +1936,7 @@ antiAlias Sharp 날카롭게
|
||||
Highlight Alternate Glyphs 대체 글리프 강조 표시
|
||||
Highlight Rows 행 강조 표시
|
||||
Highlight Variable 가변 요소 강조 표시
|
||||
Sort Variables 가변 요소 정렬
|
||||
Highlight Substituted Fonts 대체된 글꼴 강조 표시
|
||||
Highlight Substituted Glyphs 대체된 글리프 강조 표시
|
||||
Highlight anchors on mouse over 마우스를 올리면 앵커 강조 표시
|
||||
@@ -2314,6 +2316,7 @@ antiAlias Sharp 날카롭게
|
||||
Merge Selected 선택 항목 병합
|
||||
Merge Swatches 견본 병합
|
||||
Merge colors into one row 색상을 한 행으로 병합
|
||||
New Row 새 행
|
||||
Mesh Tool 메시 도구
|
||||
Message: 메시지:
|
||||
Metadata: 메타데이터:
|
||||
@@ -3847,6 +3850,40 @@ file-filter Templates 템플릿
|
||||
Gradient (. or Shift+.) 그레이디언트 (. 또는 Shift+.)
|
||||
Graph Data 그래프 데이터
|
||||
Graph Type 그래프 유형
|
||||
Graph Options 그래프 옵션
|
||||
Left Axis 왼쪽 축
|
||||
Right Axis 오른쪽 축
|
||||
Category Axis 범주 축
|
||||
Separate Scales 별도 눈금
|
||||
Add Drop Shadow 드롭 섀도 추가
|
||||
Add Legend Across Top 위쪽에 범례 추가
|
||||
First Row in Front 첫 행을 앞에
|
||||
First Column in Front 첫 열을 앞에
|
||||
Legend 범례
|
||||
Column Width 세로 막대 폭
|
||||
Bar Width 가로 막대 폭
|
||||
Cluster Width 묶음 폭
|
||||
Mark Data Points 데이터 요소 표시
|
||||
Connect Data Points 데이터 요소 연결
|
||||
Edge-to-Edge Lines 가장자리 간 선
|
||||
Draw Filled Lines 채워진 선 그리기
|
||||
Tick Values 눈금 값
|
||||
Override Calculated Values 계산된 값 재정의
|
||||
Divisions 구간
|
||||
Tick Marks 눈금 표시
|
||||
Tick Marks per Division 구간당 눈금 표시
|
||||
Draw Tick Marks Between Labels 레이블 사이에 눈금 표시 그리기
|
||||
Add Labels 레이블 추가
|
||||
Maximum must be more than Minimum. 최대값은 최소값보다 커야 합니다.
|
||||
Column Graph 세로 막대 그래프
|
||||
Stacked Column Graph 누적 세로 막대 그래프
|
||||
Bar Graph 가로 막대 그래프
|
||||
Stacked Bar Graph 누적 가로 막대 그래프
|
||||
Line Graph 선 그래프
|
||||
Area Graph 영역 그래프
|
||||
Scatter Graph 분산형 그래프
|
||||
Pie Graph 원형 그래프
|
||||
Radar Graph 방사형 그래프
|
||||
Graph Design 그래프 디자인
|
||||
Graph Marker 그래프 마커
|
||||
New Design 새 디자인
|
||||
@@ -4067,6 +4104,48 @@ file-filter Templates 템플릿
|
||||
Composite Fonts 합성 글꼴
|
||||
Export Composite Fonts 합성 글꼴 내보내기
|
||||
Import Composite Fonts 합성 글꼴 가져오기
|
||||
@msg each font must be {name, parts} 각 글꼴은 {name, parts} 형식이어야 합니다
|
||||
@msg unknown key `{k}`; a font takes name and parts 알 수 없는 키 `{k}`: 글꼴에는 name과 parts만 쓸 수 있습니다
|
||||
@msg each font needs a name 각 글꼴에는 이름이 필요합니다
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}`은(는) 합성 글꼴 이름으로 쓸 수 없습니다(1~{COMPOSITE_NAME_MAX}자, 제어 문자 없음)
|
||||
@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}`: {_1} must be an object `{name}`: {_1}은(는) 객체여야 합니다
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1}에 알 수 없는 키 `{k}`이(가) 있습니다
|
||||
@msg `{name}`: give the kanji part `{name}`: kanji 부분을 지정하세요
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 문서에는 합성 글꼴을 최대 {COMPOSITE_FONTS_MAX}개까지 둘 수 있습니다
|
||||
@msg two composite fonts are named `{_1}` 이름이 `{_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 give fonts: [{name, parts}…] fonts를 지정하세요: [{name, parts}…]
|
||||
@msg path is empty path가 비어 있습니다
|
||||
@msg fonts must be a list of composite fonts fonts는 합성 글꼴 목록이어야 합니다
|
||||
@msg names must be strings names는 문자열이어야 합니다
|
||||
@msg no composite font named `{n}` 이름이 `{n}`인 합성 글꼴이 없습니다
|
||||
@msg names must be a list of names names는 이름 목록이어야 합니다
|
||||
@msg no composite fonts to export 내보낼 합성 글꼴이 없습니다
|
||||
@msg not a composite fonts file: {e} 합성 글꼴 파일이 아닙니다: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list 합성 글꼴 파일이 아닙니다: compositeFonts 목록이 없습니다
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts 파일에 합성 글꼴이 {COMPOSITE_FONTS_MAX}개보다 많습니다
|
||||
@msg the file is larger than 4 MB 파일이 4MB보다 큽니다
|
||||
@msg dataBase64 is not base64 dataBase64가 base64가 아닙니다
|
||||
@msg give one of path, data or dataBase64 path, data, dataBase64 중 하나를 지정하세요
|
||||
@msg the file holds no composite fonts 파일에 합성 글꼴이 없습니다
|
||||
@msg the file holds two composite fonts named `{_1}` 파일에 이름이 `{_1}`인 합성 글꼴이 두 개 있습니다
|
||||
@msg `{_1}` can't name a composite font `{_1}`은(는) 합성 글꼴 이름으로 쓸 수 없습니다
|
||||
@msg {k}: give a font {k}: 글꼴을 지정하세요
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: 글꼴을 지정하세요
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size는 {_1}~{_2}%여야 합니다
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size는 {_1}~{_2}%여야 합니다
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline은 {_1}~{_2}%여야 합니다
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline은 {_1}~{_2}%여야 합니다
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: 비율은 {_1}~{_2}%여야 합니다
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: 비율은 {_1}~{_2}%여야 합니다
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: 비율을 바꿀 수 있는 것은 kana, roman, numerals뿐입니다
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: 비율을 바꿀 수 있는 것은 kana, roman, numerals뿐입니다
|
||||
@msg {k}: only kana can be scaled at the center {k}: 가운데를 기준으로 비율을 바꿀 수 있는 것은 kana뿐입니다
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: 가운데를 기준으로 비율을 바꿀 수 있는 것은 kana뿐입니다
|
||||
@msg no composite font 합성 글꼴이 없습니다
|
||||
Delete Font 글꼴 삭제
|
||||
Kanji 한자
|
||||
Kana 가나
|
||||
@@ -5605,6 +5684,7 @@ lighting Light 광원
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 그룹의 칠이나 획에 있는 3D 효과는 내보낼 것을 그리지 않습니다
|
||||
@msg The 3D object is too complex to export 3D 개체가 너무 복잡하여 내보낼 수 없습니다
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) 개체에 3D 효과(돌출 및 경사, 회전체, 부풀리기 또는 회전)가 없습니다
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render`는 값이 shaded 또는 wireframe인 `mode`만 있는 객체여야 합니다
|
||||
@msg its 3D effect builds no solid 3D 효과가 입체를 만들지 않습니다
|
||||
@msg bevel must be none or one of {_1} bevel은 none 또는 {_1} 중 하나여야 합니다
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}은(는) 이 아트로 입체를 만들 수 없습니다. 심볼 인스턴스, 블렌드, 클리핑 그룹에 개체가 너무 많습니다
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -998,6 +998,7 @@
|
||||
Rotate (Classic)… Roteren (klassiek)…
|
||||
Surface Oppervlak
|
||||
Wireframe Draadmodel
|
||||
Render Settings Renderinstellingen
|
||||
No Shading Geen arcering
|
||||
Plastic Plastic
|
||||
Highlight Intensity Glansintensiteit
|
||||
@@ -2088,6 +2089,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Samengestelde lettertypen
|
||||
Export Composite Fonts Samengestelde lettertypen exporteren
|
||||
Import Composite Fonts Samengestelde lettertypen importeren
|
||||
@msg each font must be {name, parts} elk lettertype moet {name, parts} zijn
|
||||
@msg unknown key `{k}`; a font takes name and parts onbekende sleutel `{k}`; een lettertype neemt name en parts
|
||||
@msg each font needs a name elk lettertype heeft een naam nodig
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan geen naam van een samengesteld lettertype zijn (1 tot {COMPOSITE_NAME_MAX} tekens, geen besturingstekens)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts moet een object zijn
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: onbekende klasse `{k}`; de klassen zijn kanji, kana, punctuation, symbols, roman en numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} moet een object zijn
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: onbekende sleutel `{k}` in {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: geef het deel kanji op
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts een document bevat hoogstens {COMPOSITE_FONTS_MAX} samengestelde lettertypen
|
||||
@msg two composite fonts are named `{_1}` twee samengestelde lettertypen heten `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` is de naam van een geïnstalleerd lettertype
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} gebruikt `{font}`, een samengesteld lettertype
|
||||
@msg give fonts: [{name, parts}…] geef fonts op: [{name, parts}…]
|
||||
@msg path is empty path is leeg
|
||||
@msg fonts must be a list of composite fonts fonts moet een lijst van samengestelde lettertypen zijn
|
||||
@msg names must be strings names moeten tekenreeksen zijn
|
||||
@msg no composite font named `{n}` geen samengesteld lettertype met de naam `{n}`
|
||||
@msg names must be a list of names names moet een lijst van namen zijn
|
||||
@msg no composite fonts to export geen samengestelde lettertypen om te exporteren
|
||||
@msg not a composite fonts file: {e} geen bestand met samengestelde lettertypen: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list geen bestand met samengestelde lettertypen: geen lijst compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts het bestand bevat meer dan {COMPOSITE_FONTS_MAX} samengestelde lettertypen
|
||||
@msg the file is larger than 4 MB het bestand is groter dan 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 is geen base64
|
||||
@msg give one of path, data or dataBase64 geef path, data of dataBase64 op
|
||||
@msg the file holds no composite fonts het bestand bevat geen samengestelde lettertypen
|
||||
@msg the file holds two composite fonts named `{_1}` het bestand bevat twee samengestelde lettertypen met de naam `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` kan geen naam van een samengesteld lettertype zijn
|
||||
@msg {k}: give a font {k}: geef een lettertype op
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: geef een lettertype op
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size moet tussen {_1} en {_2}% liggen
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size moet tussen {_1} en {_2}% liggen
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline moet tussen {_1} en {_2}% liggen
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline moet tussen {_1} en {_2}% liggen
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: de schaal moet tussen {_1} en {_2}% liggen
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: de schaal moet tussen {_1} en {_2}% liggen
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: alleen kana, roman en numerals kunnen worden geschaald
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: alleen kana, roman en numerals kunnen worden geschaald
|
||||
@msg {k}: only kana can be scaled at the center {k}: alleen kana kan vanuit het midden worden geschaald
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: alleen kana kan vanuit het midden worden geschaald
|
||||
@msg no composite font geen samengesteld lettertype
|
||||
Delete Font Lettertype verwijderen
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -3230,6 +3273,7 @@ digits Malayalam Malayalam
|
||||
Limit to Library: Beperken tot bibliotheek:
|
||||
Link harmony colors Harmoniekleuren koppelen
|
||||
Merge colors into one row Kleuren samenvoegen in één rij
|
||||
New Row Nieuwe rij
|
||||
No colors to recolor (preserved colors are left out). Geen kleuren om opnieuw te kleuren (behouden kleuren zijn weggelaten).
|
||||
Preserve: Behouden:
|
||||
Randomly change color order Kleurvolgorde willekeurig wijzigen
|
||||
@@ -3879,6 +3923,40 @@ theme Appearance Vormgeving
|
||||
Create Graph Diagram maken
|
||||
Graph Data Diagramgegevens
|
||||
Graph Type Diagramtype
|
||||
Graph Options Diagramopties
|
||||
Left Axis Linkeras
|
||||
Right Axis Rechteras
|
||||
Category Axis Categorie-as
|
||||
Separate Scales Aparte schalen
|
||||
Add Drop Shadow Slagschaduw toevoegen
|
||||
Add Legend Across Top Legenda bovenaan toevoegen
|
||||
First Row in Front Eerste rij vooraan
|
||||
First Column in Front Eerste kolom vooraan
|
||||
Legend Legenda
|
||||
Column Width Kolombreedte
|
||||
Bar Width Staafbreedte
|
||||
Cluster Width Groepbreedte
|
||||
Mark Data Points Gegevenspunten markeren
|
||||
Connect Data Points Gegevenspunten verbinden
|
||||
Edge-to-Edge Lines Lijnen van rand tot rand
|
||||
Draw Filled Lines Gevulde lijnen tekenen
|
||||
Tick Values Maatstreepwaarden
|
||||
Override Calculated Values Berekende waarden negeren
|
||||
Divisions Verdelingen
|
||||
Tick Marks Maatstreepjes
|
||||
Tick Marks per Division Maatstreepjes per verdeling
|
||||
Draw Tick Marks Between Labels Maatstreepjes tussen labels tekenen
|
||||
Add Labels Labels toevoegen
|
||||
Maximum must be more than Minimum. Het maximum moet groter zijn dan het minimum.
|
||||
Column Graph Kolomdiagram
|
||||
Stacked Column Graph Gestapeld kolomdiagram
|
||||
Bar Graph Staafdiagram
|
||||
Stacked Bar Graph Gestapeld staafdiagram
|
||||
Line Graph Lijndiagram
|
||||
Area Graph Vlakdiagram
|
||||
Scatter Graph Spreidingsdiagram
|
||||
Pie Graph Cirkeldiagram
|
||||
Radar Graph Radardiagram
|
||||
Vector Halftone Vectorhalftoon
|
||||
spot spot
|
||||
Text Tekst
|
||||
@@ -4466,6 +4544,7 @@ axis Both Beide
|
||||
Replace: Vervangen:
|
||||
Type on a Path Options Opties voor tekst op pad
|
||||
Highlight Variable Variabele markeren
|
||||
Sort Variables Variabelen sorteren
|
||||
New Variable Nieuwe variabele
|
||||
List Variables Variabelen weergeven
|
||||
Bind Variable Variabele binden
|
||||
@@ -7786,6 +7865,7 @@ mark-style Japanese Japans
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export een 3D-effect op de vulling of contour van een groep tekent niets om te exporteren
|
||||
@msg The 3D object is too complex to export Het 3D-object is te complex om te exporteren
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) het object heeft geen 3D-effect (Extruderen en afschuinen, Draaien, Opblazen of Roteren)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` moet een object zijn met alleen `mode`, ingesteld op shaded of wireframe
|
||||
@msg its 3D effect builds no solid het 3D-effect ervan maakt geen massief object
|
||||
@msg bevel must be none or one of {_1} bevel moet none of een van {_1} zijn
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kan van deze illustratie geen massieve objecten maken: de symboolinstanties, vermengingen en knipgroepen ervan bevatten te veel objecten
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -485,6 +485,7 @@
|
||||
Rotate (Classic)… Obróć (klasyczny)…
|
||||
Surface Powierzchnia
|
||||
Wireframe Szkielet
|
||||
Render Settings Ustawienia renderowania
|
||||
No Shading Bez cieniowania
|
||||
Plastic Plastik
|
||||
Highlight Intensity Intensywność połysku
|
||||
@@ -1576,6 +1577,48 @@ digits Malayalam Malajalam
|
||||
Composite Fonts Fonty złożone
|
||||
Export Composite Fonts Eksportuj fonty złożone
|
||||
Import Composite Fonts Importuj fonty złożone
|
||||
@msg each font must be {name, parts} każdy font musi mieć postać {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts nieznany klucz `{k}`; font przyjmuje name i parts
|
||||
@msg each font needs a name każdy font musi mieć nazwę
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nie może być nazwą fontu złożonego (od 1 do {COMPOSITE_NAME_MAX} znaków, bez znaków sterujących)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts musi być obiektem
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: nieznana klasa `{k}`; klasy to kanji, kana, punctuation, symbols, roman i numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} musi być obiektem
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: nieznany klucz `{k}` w {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: podaj część kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts dokument może zawierać najwyżej {COMPOSITE_FONTS_MAX} fontów złożonych
|
||||
@msg two composite fonts are named `{_1}` dwa fonty złożone mają nazwę `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` to nazwa zainstalowanego fontu
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} używa `{font}`, który jest fontem złożonym
|
||||
@msg give fonts: [{name, parts}…] podaj fonts: [{name, parts}…]
|
||||
@msg path is empty path jest puste
|
||||
@msg fonts must be a list of composite fonts fonts musi być listą fontów złożonych
|
||||
@msg names must be strings names muszą być ciągami znaków
|
||||
@msg no composite font named `{n}` brak fontu złożonego o nazwie `{n}`
|
||||
@msg names must be a list of names names musi być listą nazw
|
||||
@msg no composite fonts to export brak fontów złożonych do eksportu
|
||||
@msg not a composite fonts file: {e} to nie jest plik fontów złożonych: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list to nie jest plik fontów złożonych: brak listy compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts plik zawiera więcej niż {COMPOSITE_FONTS_MAX} fontów złożonych
|
||||
@msg the file is larger than 4 MB plik jest większy niż 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 nie jest w formacie base64
|
||||
@msg give one of path, data or dataBase64 podaj path, data lub dataBase64
|
||||
@msg the file holds no composite fonts plik nie zawiera fontów złożonych
|
||||
@msg the file holds two composite fonts named `{_1}` plik zawiera dwa fonty złożone o nazwie `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` nie może być nazwą fontu złożonego
|
||||
@msg {k}: give a font {k}: podaj font
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: podaj font
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size musi wynosić od {_1} do {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size musi wynosić od {_1} do {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline musi wynosić od {_1} do {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline musi wynosić od {_1} do {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: skala musi wynosić od {_1} do {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skala musi wynosić od {_1} do {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: skalować można tylko kana, roman i numerals
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: skalować można tylko kana, roman i numerals
|
||||
@msg {k}: only kana can be scaled at the center {k}: od środka można skalować tylko kana
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: od środka można skalować tylko kana
|
||||
@msg no composite font brak fontu złożonego
|
||||
Delete Font Usuń font
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -2839,6 +2882,7 @@ digits Malayalam Malajalam
|
||||
Limit to Library: Ogranicz do biblioteki:
|
||||
Link harmony colors Połącz kolory harmonii
|
||||
Merge colors into one row Scal kolory w jeden wiersz
|
||||
New Row Nowy wiersz
|
||||
No colors to recolor (preserved colors are left out). Brak kolorów do zmiany (zachowane kolory są pomijane).
|
||||
Preserve: Zachowaj:
|
||||
Randomly change color order Losowo zmień kolejność kolorów
|
||||
@@ -3509,6 +3553,40 @@ theme Appearance Wygląd
|
||||
Create Graph Utwórz wykres
|
||||
Graph Data Dane wykresu
|
||||
Graph Type Typ wykresu
|
||||
Graph Options Opcje wykresu
|
||||
Left Axis Lewa oś
|
||||
Right Axis Prawa oś
|
||||
Category Axis Oś kategorii
|
||||
Separate Scales Oddzielne skale
|
||||
Add Drop Shadow Dodaj cień
|
||||
Add Legend Across Top Dodaj legendę u góry
|
||||
First Row in Front Pierwszy wiersz z przodu
|
||||
First Column in Front Pierwsza kolumna z przodu
|
||||
Legend Legenda
|
||||
Column Width Szerokość kolumny
|
||||
Bar Width Szerokość słupka
|
||||
Cluster Width Szerokość grupy
|
||||
Mark Data Points Oznacz punkty danych
|
||||
Connect Data Points Połącz punkty danych
|
||||
Edge-to-Edge Lines Linie od krawędzi do krawędzi
|
||||
Draw Filled Lines Rysuj wypełnione linie
|
||||
Tick Values Wartości podziałki
|
||||
Override Calculated Values Zastąp obliczone wartości
|
||||
Divisions Podziały
|
||||
Tick Marks Znaczniki podziałki
|
||||
Tick Marks per Division Znaczniki na podział
|
||||
Draw Tick Marks Between Labels Rysuj znaczniki między etykietami
|
||||
Add Labels Dodaj etykiety
|
||||
Maximum must be more than Minimum. Maksimum musi być większe niż minimum.
|
||||
Column Graph Wykres kolumnowy
|
||||
Stacked Column Graph Skumulowany wykres kolumnowy
|
||||
Bar Graph Wykres słupkowy
|
||||
Stacked Bar Graph Skumulowany wykres słupkowy
|
||||
Line Graph Wykres liniowy
|
||||
Area Graph Wykres warstwowy
|
||||
Scatter Graph Wykres punktowy
|
||||
Pie Graph Wykres kołowy
|
||||
Radar Graph Wykres radarowy
|
||||
Vector Halftone Półton wektorowy
|
||||
spot dodatkowy
|
||||
Text Tekst
|
||||
@@ -4080,6 +4158,7 @@ axis Both Obie
|
||||
Replace: Zastąp:
|
||||
Type on a Path Options Opcje tekstu na ścieżce
|
||||
Highlight Variable Wyróżnij zmienną
|
||||
Sort Variables Sortuj zmienne
|
||||
New Variable Nowa zmienna
|
||||
List Variables Wyświetl zmienne
|
||||
Bind Variable Powiąż zmienną
|
||||
@@ -5859,6 +5938,7 @@ mark-style Japanese Japońskie
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export efekt 3D na wypełnieniu lub obrysie grupy nie rysuje niczego do eksportu
|
||||
@msg The 3D object is too complex to export Obiekt 3D jest zbyt złożony, aby go wyeksportować
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) obiekt nie ma efektu 3D (Wyciągnięcie i fazowanie, Obrót, Nadmuchanie ani Obróć)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` musi być obiektem zawierającym tylko `mode` o wartości shaded lub wireframe
|
||||
@msg its 3D effect builds no solid jego efekt 3D nie tworzy żadnej bryły
|
||||
@msg bevel must be none or one of {_1} bevel musi mieć wartość none lub jedną z: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} nie może utworzyć brył z tego rysunku: jego wystąpienia symboli, przejścia i grupy przycinające zawierają zbyt wiele obiektów
|
||||
|
||||
|
Can't render this file because it is too large.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user