mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge main into fix/a-rasterize-effect-before-a-scaling-transform
This commit is contained in:
@@ -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,
|
||||
|
||||
+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() });
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -899,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")];
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -694,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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -84,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;
|
||||
@@ -94,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};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1366,6 +1366,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 Нов дизайн
|
||||
@@ -1485,6 +1519,7 @@ font-technology Variable Променливи
|
||||
Highlight anchors on mouse over Маркиране на опорните точки при задържане на мишката
|
||||
Highlight Rows Маркиране на редовете
|
||||
Highlight Variable Маркиране на променливи
|
||||
Sort Variables Сортиране на променливите
|
||||
Highlight Substituted Fonts Маркиране на заменени шрифтове
|
||||
Highlight Substituted Glyphs Маркиране на заменени глифи
|
||||
History История
|
||||
@@ -1947,6 +1982,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 contains an unexpected character in line 2 and column 73.
|
@@ -2893,6 +2893,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
|
||||
@@ -3575,6 +3576,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
|
||||
@@ -4181,6 +4216,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.
|
@@ -1899,6 +1899,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…
|
||||
@@ -2040,6 +2074,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
|
||||
@@ -2561,6 +2596,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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2971,6 +2971,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
|
||||
@@ -3671,6 +3672,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
|
||||
@@ -4279,6 +4314,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
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3363,6 +3363,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
|
||||
@@ -4012,6 +4013,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
|
||||
@@ -4599,6 +4634,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.
|
@@ -1411,6 +1411,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 +2057,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
|
||||
@@ -5401,6 +5436,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
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1341,6 +1341,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 Νέο σχέδιο
|
||||
@@ -1460,6 +1494,7 @@ font-technology Variable Μεταβλητές
|
||||
Highlight anchors on mouse over Επισήμανση σημείων αγκύρωσης με το ποντίκι
|
||||
Highlight Rows Επισήμανση γραμμών
|
||||
Highlight Variable Επισήμανση μεταβλητής
|
||||
Sort Variables Ταξινόμηση μεταβλητών
|
||||
Highlight Substituted Fonts Επισήμανση υποκατεστημένων γραμματοσειρών
|
||||
Highlight Substituted Glyphs Επισήμανση υποκατεστημένων γλύφων
|
||||
History Ιστορικό
|
||||
@@ -3000,6 +3035,7 @@ theme Appearance Εμφάνιση
|
||||
Menu Dialog Παράθυρο μενού
|
||||
Merge Συγχώνευση
|
||||
Merge colors into one row Συγχώνευση χρωμάτων σε μία γραμμή
|
||||
New Row Νέα γραμμή
|
||||
Merge Graphic Styles Συγχώνευση στυλ γραφικών
|
||||
Merge Graphic Styles… Συγχώνευση στυλ γραφικών…
|
||||
Merge Selected Συγχώνευση επιλεγμένων
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1420,6 +1420,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
|
||||
@@ -1540,6 +1574,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
|
||||
@@ -2017,6 +2052,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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2915,6 +2915,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
|
||||
@@ -3609,6 +3610,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
|
||||
@@ -4198,6 +4233,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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1414,6 +1414,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
|
||||
@@ -1534,6 +1568,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
|
||||
@@ -2011,6 +2046,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
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1541,6 +1541,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 ગ્રાફિક સ્ટાઇલ લાઇબ્રેરીઓ
|
||||
@@ -1645,6 +1679,7 @@
|
||||
Highlight Substituted Glyphs બદલાયેલા ગ્લિફ હાઇલાઇટ કરો
|
||||
Highlight Tracking ટ્રેકિંગ હાઇલાઇટ કરો
|
||||
Highlight Variable વેરિએબલ હાઇલાઇટ કરો
|
||||
Sort Variables વેરિએબલનો ક્રમ લગાવો
|
||||
Highlight anchors on mouse over માઉસ ફેરવતાં એન્કર હાઇલાઇટ કરો
|
||||
Highlight: હાઇલાઇટ:
|
||||
Hindi હિન્દી
|
||||
@@ -2108,6 +2143,7 @@
|
||||
Merge Shared Edge શેર કરેલી કિનાર મર્જ કરો
|
||||
Merge Swatches સ્વેચ મર્જ કરો
|
||||
Merge colors into one row રંગોને એક હરોળમાં મર્જ કરો
|
||||
New Row નવી હરોળ
|
||||
Merge only with selection માત્ર પસંદગી સાથે મર્જ કરો
|
||||
Merged PRs મર્જ થયેલા PR
|
||||
Mesh Point Color મેશ બિંદુનો રંગ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 72 and column 5.
|
@@ -2116,6 +2116,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 +2305,7 @@ antiAlias Sharp शार्प
|
||||
Highlight Substituted Glyphs बदले हुए ग्लिफ़ हाइलाइट करें
|
||||
Highlight Tracking हाइलाइट ट्रैकिंग
|
||||
Highlight Variable वेरिएबल हाइलाइट करें
|
||||
Sort Variables वेरिएबल क्रमबद्ध करें
|
||||
Highlight: हाइलाइट:
|
||||
Hindi हिन्दी
|
||||
History हिस्ट्री
|
||||
@@ -2819,6 +2854,7 @@ antiAlias Sharp शार्प
|
||||
Menu Dialog मेन्यू डायलॉग
|
||||
Merge मर्ज करें
|
||||
Merge colors into one row रंगों को एक पंक्ति में मिलाएं
|
||||
New Row नई पंक्ति
|
||||
Merge Graphic Styles मर्ज ग्राफिक स्टाइल्स
|
||||
Merge Graphic Styles… मर्ज ग्राफिक स्टाइल्स…
|
||||
Merge Layers लेयर्स मर्ज करें
|
||||
@@ -8992,6 +9028,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.
|
@@ -1765,6 +1765,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…
|
||||
@@ -1951,6 +1985,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…
|
||||
@@ -2529,6 +2564,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
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2711,6 +2711,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
|
||||
@@ -3360,6 +3361,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
|
||||
@@ -3947,6 +3982,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 2459 and column 5.
|
@@ -1441,6 +1441,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 +1595,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 +2090,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
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1943,6 +1943,7 @@ antiAlias Sharp シャープ
|
||||
Highlight Alternate Glyphs 代替字形をハイライト
|
||||
Highlight Rows 行をハイライト
|
||||
Highlight Variable 変数をハイライト表示
|
||||
Sort Variables 変数を並べ替え
|
||||
Highlight Substituted Fonts 置換フォントをハイライト
|
||||
Highlight Substituted Glyphs 置換字形をハイライト
|
||||
Highlight anchors on mouse over マウスオーバー時にアンカーをハイライト
|
||||
@@ -2322,6 +2323,7 @@ antiAlias Sharp シャープ
|
||||
Merge Selected 選択レイヤーを結合
|
||||
Merge Swatches スウォッチを統合
|
||||
Merge colors into one row カラーを 1 行に統合
|
||||
New Row 新規行
|
||||
Mesh Tool メッシュツール
|
||||
Message: メッセージ:
|
||||
Metadata: メタデータ:
|
||||
@@ -3858,6 +3860,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 新規デザイン
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2669,6 +2669,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 ფერების რიგის შემთხვევითი შეცვლა
|
||||
@@ -3248,6 +3249,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 ტექსტი
|
||||
@@ -3810,6 +3845,7 @@ axis Both ორივე
|
||||
Replace: ჩანაცვლება:
|
||||
Type on a Path Options ტრაექტორიაზე ტექსტის პარამეტრები
|
||||
Highlight Variable ცვლადის გამოკვეთა
|
||||
Sort Variables ცვლადების დალაგება
|
||||
New Variable ახალი ცვლადი
|
||||
List Variables ცვლადების სია
|
||||
Bind Variable ცვლადის მიბმა
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1933,6 +1933,7 @@ antiAlias Sharp 날카롭게
|
||||
Highlight Alternate Glyphs 대체 글리프 강조 표시
|
||||
Highlight Rows 행 강조 표시
|
||||
Highlight Variable 가변 요소 강조 표시
|
||||
Sort Variables 가변 요소 정렬
|
||||
Highlight Substituted Fonts 대체된 글꼴 강조 표시
|
||||
Highlight Substituted Glyphs 대체된 글리프 강조 표시
|
||||
Highlight anchors on mouse over 마우스를 올리면 앵커 강조 표시
|
||||
@@ -2312,6 +2313,7 @@ antiAlias Sharp 날카롭게
|
||||
Merge Selected 선택 항목 병합
|
||||
Merge Swatches 견본 병합
|
||||
Merge colors into one row 색상을 한 행으로 병합
|
||||
New Row 새 행
|
||||
Mesh Tool 메시 도구
|
||||
Message: 메시지:
|
||||
Metadata: 메타데이터:
|
||||
@@ -3846,6 +3848,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 새 디자인
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3271,6 +3271,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
|
||||
@@ -3920,6 +3921,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
|
||||
@@ -4507,6 +4542,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
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2880,6 +2880,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
|
||||
@@ -3550,6 +3551,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
|
||||
@@ -4121,6 +4156,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ą
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1727,6 +1727,7 @@ antiAlias Sharp Nítido
|
||||
Merge Layers Mesclar Camadas
|
||||
Merge Swatches Mesclar Amostras
|
||||
Merge colors into one row Mesclar cores em uma linha
|
||||
New Row Nova linha
|
||||
Mesh Tool Ferramenta Malha
|
||||
Message: Mensagem:
|
||||
Metadata: Metadados:
|
||||
@@ -3615,6 +3616,7 @@ font-technology Variable Variáveis
|
||||
Graphics Gráficos
|
||||
Highlight Rows Destacar linhas
|
||||
Highlight Variable Destacar variável
|
||||
Sort Variables Ordenar variáveis
|
||||
Palette: Paleta:
|
||||
Limited Limitada
|
||||
Full Tone Tom completo
|
||||
@@ -4970,6 +4972,40 @@ axis Both Ambos
|
||||
Gradient (. or Shift+.) Gradiente (. ou Shift+.)
|
||||
Graph Data Dados do Gráfico
|
||||
Graph Type Tipo de Gráfico
|
||||
Graph Options Opções do Gráfico
|
||||
Left Axis Eixo Esquerdo
|
||||
Right Axis Eixo Direito
|
||||
Category Axis Eixo de Categorias
|
||||
Separate Scales Escalas Separadas
|
||||
Add Drop Shadow Adicionar Sombra Projetada
|
||||
Add Legend Across Top Adicionar Legenda na Parte Superior
|
||||
First Row in Front Primeira Linha na Frente
|
||||
First Column in Front Primeira Coluna na Frente
|
||||
Legend Legenda
|
||||
Column Width Largura da Coluna
|
||||
Bar Width Largura da Barra
|
||||
Cluster Width Largura do Grupo
|
||||
Mark Data Points Marcar Pontos de Dados
|
||||
Connect Data Points Conectar Pontos de Dados
|
||||
Edge-to-Edge Lines Linhas de Borda a Borda
|
||||
Draw Filled Lines Desenhar Linhas Preenchidas
|
||||
Tick Values Valores das Marcas
|
||||
Override Calculated Values Substituir Valores Calculados
|
||||
Divisions Divisões
|
||||
Tick Marks Marcas de Escala
|
||||
Tick Marks per Division Marcas por Divisão
|
||||
Draw Tick Marks Between Labels Desenhar Marcas entre Rótulos
|
||||
Add Labels Adicionar Rótulos
|
||||
Maximum must be more than Minimum. O máximo deve ser maior que o mínimo.
|
||||
Column Graph Gráfico de Colunas
|
||||
Stacked Column Graph Gráfico de Colunas Empilhadas
|
||||
Bar Graph Gráfico de Barras
|
||||
Stacked Bar Graph Gráfico de Barras Empilhadas
|
||||
Line Graph Gráfico de Linhas
|
||||
Area Graph Gráfico de Áreas
|
||||
Scatter Graph Gráfico de Dispersão
|
||||
Pie Graph Gráfico de Pizza
|
||||
Radar Graph Gráfico de Radar
|
||||
New Design Novo Design
|
||||
Delete Design Excluir Design
|
||||
Paste Design Colar Design
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1430,6 +1430,40 @@ file-filter Templates Шаблоны
|
||||
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 Новый дизайн
|
||||
@@ -1550,6 +1584,7 @@ file-filter Templates Шаблоны
|
||||
Highlight anchors on mouse over Выделять опорные точки при наведении
|
||||
Highlight Rows Выделять строки
|
||||
Highlight Variable Подсветить переменную
|
||||
Sort Variables Сортировать переменные
|
||||
Highlight Substituted Fonts Выделять заменённые шрифты
|
||||
Highlight Substituted Glyphs Выделять заменённые глифы
|
||||
History История
|
||||
@@ -2044,6 +2079,7 @@ file-filter Templates Шаблоны
|
||||
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.
|
@@ -1379,6 +1379,40 @@ font-technology Variable Variabla
|
||||
Graph Diagram
|
||||
Graph Data Diagramdata
|
||||
Graph Type Diagramtyp
|
||||
Graph Options Diagramalternativ
|
||||
Left Axis Vänster axel
|
||||
Right Axis Höger axel
|
||||
Category Axis Kategoriaxel
|
||||
Separate Scales Separata skalor
|
||||
Add Drop Shadow Lägg till slagskugga
|
||||
Add Legend Across Top Lägg till förklaring överst
|
||||
First Row in Front Första raden längst fram
|
||||
First Column in Front Första kolumnen längst fram
|
||||
Legend Förklaring
|
||||
Column Width Stapelbredd
|
||||
Bar Width Liggande stapelbredd
|
||||
Cluster Width Gruppbredd
|
||||
Mark Data Points Markera datapunkter
|
||||
Connect Data Points Förbind datapunkter
|
||||
Edge-to-Edge Lines Linjer från kant till kant
|
||||
Draw Filled Lines Rita fyllda linjer
|
||||
Tick Values Skalstrecksvärden
|
||||
Override Calculated Values Åsidosätt beräknade värden
|
||||
Divisions Indelningar
|
||||
Tick Marks Skalstreck
|
||||
Tick Marks per Division Skalstreck per indelning
|
||||
Draw Tick Marks Between Labels Rita skalstreck mellan etiketter
|
||||
Add Labels Lägg till etiketter
|
||||
Maximum must be more than Minimum. Maximum måste vara större än minimum.
|
||||
Column Graph Stapeldiagram
|
||||
Stacked Column Graph Staplat stapeldiagram
|
||||
Bar Graph Liggande stapeldiagram
|
||||
Stacked Bar Graph Staplat liggande stapeldiagram
|
||||
Line Graph Linjediagram
|
||||
Area Graph Ytdiagram
|
||||
Scatter Graph Punktdiagram
|
||||
Pie Graph Cirkeldiagram
|
||||
Radar Graph Polärdiagram
|
||||
Graph Design Diagramdesign
|
||||
Graph Marker Diagrammarkör
|
||||
New Design Ny design
|
||||
@@ -1498,6 +1532,7 @@ font-technology Variable Variabla
|
||||
Highlight anchors on mouse over Framhäv ankarpunkter när musen är över dem
|
||||
Highlight Rows Framhäv rader
|
||||
Highlight Variable Framhäv variabel
|
||||
Sort Variables Sortera variabler
|
||||
Highlight Substituted Fonts Framhäv ersatta typsnitt
|
||||
Highlight Substituted Glyphs Framhäv ersatta glyfer
|
||||
History Historik
|
||||
@@ -1958,6 +1993,7 @@ font-technology Variable Variabla
|
||||
Menu Dialog Menydialog
|
||||
Merge Sammanfoga
|
||||
Merge colors into one row Sammanfoga färger till en rad
|
||||
New Row Ny rad
|
||||
Merge Graphic Styles Sammanfoga grafikstilar
|
||||
Merge Graphic Styles… Sammanfoga grafikstilar…
|
||||
Merge Layers Sammanfoga lager
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -158,6 +158,25 @@ fn hindi_rows_keep_the_meaning_of_their_english() {
|
||||
"पेज थंबनेल RGB पिक्सल का होना चाहिए, हर तरफ़ 1 से 256 (मिला 0 × 4)"
|
||||
);
|
||||
assert_eq!(message(hi, "invalid angle"), "अमान्य कोण");
|
||||
let limits = message(
|
||||
hi,
|
||||
"the image would be 9000 × 9000 pixels; raster exports are limited to 8192 pixels per side and 64 megapixels: lower the scale or resolution",
|
||||
);
|
||||
assert!(
|
||||
limits.contains("9000 × 9000 पिक्सेल") && limits.contains("8192 पिक्सेल और 64 मेगापिक्सेल") && limits.contains("स्केल या रिज़ॉल्यूशन कम करें"),
|
||||
"{limits}"
|
||||
);
|
||||
assert_eq!(
|
||||
message(hi, "color books in color space HSB aren't supported (only RGB, CMYK and Lab)"),
|
||||
"कलर स्पेस HSB वाली कलर बुक समर्थित नहीं हैं (केवल RGB, CMYK और Lab)"
|
||||
);
|
||||
assert_eq!(message(hi, "`index` must be a whole number"), "`index` पूरी संख्या होनी चाहिए");
|
||||
let knockout = message(
|
||||
hi,
|
||||
"knockout groups with too many overlapping translucent elements are written as PDF knockout groups: \
|
||||
their translucent elements knock out what they cover fully",
|
||||
);
|
||||
assert!(knockout.starts_with("बहुत अधिक ओवरलैप") && knockout.contains("PDF नॉकआउट ग्रुप"), "{knockout}");
|
||||
assert_eq!(tr(hi, "Desired"), "वांछित");
|
||||
let range = "न्यूनतम, वांछित से अधिक नहीं होना चाहिए और वांछित, अधिकतम से अधिक नहीं होना चाहिए";
|
||||
assert_eq!(tr(hi, "Minimum must not exceed Desired, nor Desired Maximum."), format!("{range}।"));
|
||||
|
||||
@@ -1353,6 +1353,40 @@ font-technology Variable Değişken
|
||||
Graph Grafik
|
||||
Graph Data Grafik Verileri
|
||||
Graph Type Grafik Türü
|
||||
Graph Options Grafik Seçenekleri
|
||||
Left Axis Sol Eksen
|
||||
Right Axis Sağ Eksen
|
||||
Category Axis Kategori Ekseni
|
||||
Separate Scales Ayrı Ölçekler
|
||||
Add Drop Shadow Alt Gölge Ekle
|
||||
Add Legend Across Top Göstergeyi Üste Ekle
|
||||
First Row in Front İlk Satır Önde
|
||||
First Column in Front İlk Sütun Önde
|
||||
Legend Gösterge
|
||||
Column Width Sütun Genişliği
|
||||
Bar Width Çubuk Genişliği
|
||||
Cluster Width Küme Genişliği
|
||||
Mark Data Points Veri Noktalarını İşaretle
|
||||
Connect Data Points Veri Noktalarını Bağla
|
||||
Edge-to-Edge Lines Kenardan Kenara Çizgiler
|
||||
Draw Filled Lines Dolgulu Çizgiler Çiz
|
||||
Tick Values Onay İşareti Değerleri
|
||||
Override Calculated Values Hesaplanan Değerleri Geçersiz Kıl
|
||||
Divisions Bölümler
|
||||
Tick Marks Ölçek İşaretleri
|
||||
Tick Marks per Division Bölüm Başına Ölçek İşareti
|
||||
Draw Tick Marks Between Labels Etiketler Arasına Ölçek İşaretleri Çiz
|
||||
Add Labels Etiket Ekle
|
||||
Maximum must be more than Minimum. En çok, en azdan büyük olmalıdır.
|
||||
Column Graph Sütun Grafik
|
||||
Stacked Column Graph Yığılmış Sütun Grafik
|
||||
Bar Graph Çubuk Grafik
|
||||
Stacked Bar Graph Yığılmış Çubuk Grafik
|
||||
Line Graph Çizgi Grafik
|
||||
Area Graph Alan Grafik
|
||||
Scatter Graph Dağılım Grafik
|
||||
Pie Graph Pasta Grafik
|
||||
Radar Graph Radar Grafik
|
||||
Graph Design Grafik Tasarımı
|
||||
Graph Marker Grafik İşaretleyici
|
||||
New Design Yeni Tasarım
|
||||
@@ -1462,6 +1496,7 @@ font-technology Variable Değişken
|
||||
Highlight anchors on mouse over Fareyle üzerine gelindiğinde bağlantı noktalarını vurgulayın
|
||||
Highlight Rows Satırları Vurgula
|
||||
Highlight Variable Değişkeni Vurgula
|
||||
Sort Variables Değişkenleri Sırala
|
||||
Highlight Substituted Fonts Değiştirilen Yazı Tiplerini Vurgula
|
||||
Highlight Substituted Glyphs Değiştirilen Glifleri Vurgula
|
||||
History Geçmiş
|
||||
@@ -1849,6 +1884,7 @@ font-technology Variable Değişken
|
||||
Menu Dialog Menü İletişim Kutusu
|
||||
Merge Birleştir
|
||||
Merge colors into one row Renkleri tek satırda birleştirme
|
||||
New Row Yeni satır
|
||||
Merge Graphic Styles Grafik Stillerini Birleştir
|
||||
Merge Graphic Styles… Grafik Stillerini Birleştir…
|
||||
Merge Layers Katmanları Birleştir
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1435,6 +1435,40 @@ file-filter Templates Шаблони
|
||||
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 Новий дизайн
|
||||
@@ -1555,6 +1589,7 @@ file-filter Templates Шаблони
|
||||
Highlight anchors on mouse over Виділяти опорні точки при наведенні
|
||||
Highlight Rows Виділяти рядки
|
||||
Highlight Variable Підсвітити змінну
|
||||
Sort Variables Сортувати змінні
|
||||
Highlight Substituted Fonts Виділяти замінені шрифти
|
||||
Highlight Substituted Glyphs Виділяти замінені гліфи
|
||||
History Історія
|
||||
@@ -2049,6 +2084,7 @@ file-filter Templates Шаблони
|
||||
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.
|
@@ -1685,6 +1685,40 @@ file-filter Templates Mẫu
|
||||
Chop Design Cắt thiết kế
|
||||
Scale Design Co giãn thiết kế
|
||||
Graph Type Loại biểu đồ
|
||||
Graph Options Tùy chọn biểu đồ
|
||||
Left Axis Trục trái
|
||||
Right Axis Trục phải
|
||||
Category Axis Trục danh mục
|
||||
Separate Scales Thang đo riêng
|
||||
Add Drop Shadow Thêm đổ bóng
|
||||
Add Legend Across Top Thêm chú giải ở trên cùng
|
||||
First Row in Front Hàng đầu tiên ở trước
|
||||
First Column in Front Cột đầu tiên ở trước
|
||||
Legend Chú giải
|
||||
Column Width Độ rộng cột
|
||||
Bar Width Độ rộng thanh
|
||||
Cluster Width Độ rộng nhóm
|
||||
Mark Data Points Đánh dấu điểm dữ liệu
|
||||
Connect Data Points Nối các điểm dữ liệu
|
||||
Edge-to-Edge Lines Đường từ mép đến mép
|
||||
Draw Filled Lines Vẽ đường tô đầy
|
||||
Tick Values Giá trị vạch chia
|
||||
Override Calculated Values Ghi đè giá trị đã tính
|
||||
Divisions Số khoảng chia
|
||||
Tick Marks Vạch chia
|
||||
Tick Marks per Division Vạch chia mỗi khoảng
|
||||
Draw Tick Marks Between Labels Vẽ vạch chia giữa các nhãn
|
||||
Add Labels Thêm nhãn
|
||||
Maximum must be more than Minimum. Giá trị tối đa phải lớn hơn giá trị tối thiểu.
|
||||
Column Graph Biểu đồ cột
|
||||
Stacked Column Graph Biểu đồ cột chồng
|
||||
Bar Graph Biểu đồ thanh
|
||||
Stacked Bar Graph Biểu đồ thanh chồng
|
||||
Line Graph Biểu đồ đường
|
||||
Area Graph Biểu đồ vùng
|
||||
Scatter Graph Biểu đồ phân tán
|
||||
Pie Graph Biểu đồ tròn
|
||||
Radar Graph Biểu đồ radar
|
||||
Graphic Đồ họa
|
||||
Graphic Style Kiểu đồ họa
|
||||
Graphic Style Libraries Thư viện kiểu đồ họa
|
||||
@@ -1788,6 +1822,7 @@ file-filter Templates Mẫu
|
||||
Highlight Substituted Fonts Đánh dấu phông thay thế
|
||||
Highlight Substituted Glyphs Đánh dấu Glyph bị thay thế
|
||||
Highlight Variable Đánh dấu biến
|
||||
Sort Variables Sắp xếp biến
|
||||
Hindi Tiếng Hindi
|
||||
digits Hindi Hindi
|
||||
History Lịch sử
|
||||
@@ -2354,6 +2389,7 @@ digits Hindi Hindi
|
||||
Menu Dialog Hộp thoại menu
|
||||
Merge Hợp nhất
|
||||
Merge colors into one row Hợp nhất màu vào một hàng
|
||||
New Row Hàng mới
|
||||
Merge Graphic Styles Hợp nhất kiểu đồ họa
|
||||
Merge Graphic Styles… Hợp nhất kiểu đồ họa…
|
||||
Merge Layers Hợp nhất lớp
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -1370,6 +1370,40 @@ file-filter Templates 模板
|
||||
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 新建设计
|
||||
@@ -1926,6 +1960,7 @@ file-filter Templates 模板
|
||||
Menu Dialog 菜单对话框
|
||||
Merge 合并
|
||||
Merge colors into one row 将颜色合并为一行
|
||||
New Row 新建行
|
||||
Merge Graphic Styles 合并图形样式
|
||||
Merge Graphic Styles… 合并图形样式…
|
||||
Merge Layers 合并图层
|
||||
@@ -5476,6 +5511,7 @@ font-technology Variable 可变
|
||||
High Performance (discrete) 高性能 (独立显卡)
|
||||
Highlight Rows 突出显示行
|
||||
Highlight Variable 突出显示变量
|
||||
Sort Variables 对变量排序
|
||||
Lock Others 锁定其他
|
||||
Merge Selected 合并所选图层
|
||||
Move Rows 移动行
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1405,6 +1405,40 @@ file-filter Templates 範本
|
||||
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 新增設計
|
||||
@@ -1526,6 +1560,7 @@ file-filter Templates 範本
|
||||
Highlight anchors on mouse over 滑鼠移過時醒目提示錨點
|
||||
Highlight Rows 反白顯示列
|
||||
Highlight Variable 突出顯示變數
|
||||
Sort Variables 排序變數
|
||||
Highlight Substituted Fonts 醒目提示被取代的字體
|
||||
Highlight Substituted Glyphs 醒目提示被取代的字符
|
||||
History 步驟記錄
|
||||
@@ -3113,6 +3148,7 @@ file-filter Templates 範本
|
||||
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.
|
@@ -594,6 +594,9 @@ fn write_plan(
|
||||
})
|
||||
};
|
||||
let failed = target.clone().unwrap_or_default();
|
||||
// The status line before the save starts: on the worker, the save sets one line ("<label>…")
|
||||
// right after this; written right away, it sets none before `then` runs.
|
||||
let (sets, started_line) = (app.ui.status_sets, format!("{label}…"));
|
||||
let then = move |app: &mut VectorcraftApp, r: Result<Value, String>| {
|
||||
if r.is_err()
|
||||
&& location_refused.load(std::sync::atomic::Ordering::Relaxed)
|
||||
@@ -623,7 +626,10 @@ fn write_plan(
|
||||
let beside: Vec<String> = beside.filter(|p| *p != path).map(str::to_string).collect();
|
||||
app.picks.written_beside.insert(uid, beside);
|
||||
}
|
||||
report_saved(app, &path, &r);
|
||||
// A line set while the file was written (another command's, another job's) stays.
|
||||
if background::status_unchanged(app, sets, &started_line) {
|
||||
report_saved(app, &path, &r);
|
||||
}
|
||||
Ok(r)
|
||||
};
|
||||
let mut r = background::run(app, enabled, label, Some(uid), work, then)?;
|
||||
|
||||
@@ -133,9 +133,7 @@ pub(crate) fn graph_dialog(app: &mut VectorcraftApp, id: &str) -> Result<Value,
|
||||
if id == "graph.setData" {
|
||||
return crate::dialogs::graph_data::open(app);
|
||||
}
|
||||
let v = app.session.execute(id, &json!({})).map_err(|e| e.to_string())?;
|
||||
let (label, fields) = ("Graph Type", v);
|
||||
app.run("ui.paramDialog", json!({"command": id, "label": label, "params": fields})).map(|_| json!({ "dialog": "command" }))
|
||||
crate::dialogs::graph_type::open(app)
|
||||
}
|
||||
|
||||
pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,17 +8,117 @@
|
||||
//! the document then no longer matches the active dataset, and "Update Data Set" writes the
|
||||
//! art back into it.
|
||||
|
||||
use egui::Ui;
|
||||
use egui::{Rect, Response, Sense, Ui, pos2, vec2};
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{NodeId, NodeKind, VariableKind};
|
||||
use vectorcraft_engine::DocState;
|
||||
use vectorcraft_engine::cmd::{VariableSort, VariableSortKey, variable_order};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets;
|
||||
|
||||
/// The panel id, as the dock, the Window menu and `window.panel` name it.
|
||||
pub const ID: &str = "variables";
|
||||
|
||||
/// The width of the variable list's type column, which holds each row's kind icon.
|
||||
const TYPE_COLUMN: f32 = 22.0;
|
||||
|
||||
/// The width of the Variables column in a list `width` wide; the Objects column takes the rest.
|
||||
fn name_column(width: f32) -> f32 {
|
||||
((width - TYPE_COLUMN) / 2.0).max(0.0)
|
||||
}
|
||||
|
||||
/// The sort a click on `key`'s header asks for: that column in ascending order, or the reverse
|
||||
/// of the order the list has when it is already sorted by that column.
|
||||
fn clicked_sort(sort: VariableSort, key: VariableSortKey) -> VariableSort {
|
||||
VariableSort { by: key, descending: sort.by == key && !sort.descending }
|
||||
}
|
||||
|
||||
/// The arrow a header shows while the list is sorted by its column.
|
||||
fn sort_arrow(sort: VariableSort, key: VariableSortKey) -> Option<&'static str> {
|
||||
(sort.by == key).then_some(if sort.descending { "▼" } else { "▲" })
|
||||
}
|
||||
|
||||
/// Draw the list's three column headers (type, Variables, Objects); a click on one returns the
|
||||
/// sort it asks for.
|
||||
fn headers(ui: &mut Ui, sort: VariableSort) -> Option<VariableSort> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let h = ui.spacing().interact_size.y;
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::hover());
|
||||
let name_w = name_column(rect.width());
|
||||
let name_left = rect.left() + TYPE_COLUMN;
|
||||
let cells = [
|
||||
(VariableSortKey::Type, tl!("Type"), Rect::from_min_max(rect.min, pos2(name_left, rect.bottom()))),
|
||||
(VariableSortKey::Name, tl!("Variables"), Rect::from_min_max(pos2(name_left, rect.top()), pos2(name_left + name_w, rect.bottom()))),
|
||||
(VariableSortKey::Object, tl!("Objects"), Rect::from_min_max(pos2(name_left + name_w, rect.top()), rect.max)),
|
||||
];
|
||||
let font = egui::TextStyle::Body.resolve(ui.style());
|
||||
let mut clicked = None;
|
||||
for (key, label, cell) in cells {
|
||||
let r = ui.interact(cell, ui.scope_id().with(("variables-sort", key.id())), Sense::click());
|
||||
if r.hovered() {
|
||||
ui.painter().rect_filled(cell, 0.0, t.hover);
|
||||
}
|
||||
let arrow = sort_arrow(sort, key);
|
||||
let p = ui.painter().with_clip_rect(cell);
|
||||
if key == VariableSortKey::Type {
|
||||
// The type column is too narrow for a word: its icon, or the arrow once it sorts.
|
||||
match arrow {
|
||||
Some(a) => {
|
||||
p.text(cell.center(), egui::Align2::CENTER_CENTER, a, font.clone(), t.text_dim);
|
||||
}
|
||||
None => crate::icons::paint(ui, "arrow-up-down", Rect::from_center_size(cell.center(), vec2(14.0, 14.0)), t.icon),
|
||||
}
|
||||
} else {
|
||||
p.text(cell.left_center() + vec2(4.0, 0.0), egui::Align2::LEFT_CENTER, label, font.clone(), t.text_strong);
|
||||
if let Some(a) = arrow {
|
||||
p.text(cell.right_center() - vec2(4.0, 0.0), egui::Align2::RIGHT_CENTER, a, font.clone(), t.text_dim);
|
||||
}
|
||||
}
|
||||
r.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, arrow.is_some(), label));
|
||||
if r.on_hover_text(label).clicked() {
|
||||
clicked = Some(clicked_sort(sort, key));
|
||||
}
|
||||
}
|
||||
clicked
|
||||
}
|
||||
|
||||
/// Draw one variable row across the three columns: its kind's icon, its name and the object it
|
||||
/// drives. Each column clips its text to its own width.
|
||||
fn variable_row(ui: &mut Ui, row: &VarRow, selected: bool) -> Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let h = ui.spacing().interact_size.y;
|
||||
let (rect, r) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click());
|
||||
if selected {
|
||||
ui.painter().rect_filled(rect, 0.0, t.row_selected);
|
||||
} else if r.hovered() {
|
||||
ui.painter().rect_filled(rect, 0.0, t.hover);
|
||||
}
|
||||
// The kind's own icon, so a text variable and a visibility one read apart.
|
||||
let icon = match row.kind {
|
||||
VariableKind::Text => "type",
|
||||
VariableKind::Visibility => "eye",
|
||||
VariableKind::Image => "image",
|
||||
VariableKind::Graph => "chart-column",
|
||||
VariableKind::Unknown => "square-dashed",
|
||||
};
|
||||
let icon_rect = Rect::from_center_size(pos2(rect.left() + TYPE_COLUMN / 2.0, rect.center().y), vec2(14.0, 14.0));
|
||||
crate::icons::paint(ui, icon, icon_rect, t.icon);
|
||||
let name_left = rect.left() + TYPE_COLUMN;
|
||||
let name_right = name_left + name_column(rect.width());
|
||||
let font = egui::TextStyle::Body.resolve(ui.style());
|
||||
let (pad, left) = (vec2(4.0, 0.0), egui::Align2::LEFT_CENTER);
|
||||
let name_cell = Rect::from_min_max(pos2(name_left, rect.top()), pos2(name_right - 4.0, rect.bottom()));
|
||||
ui.painter().with_clip_rect(name_cell).text(name_cell.left_center() + pad, left, &row.name, font.clone(), t.text);
|
||||
let object_cell = Rect::from_min_max(pos2(name_right, rect.top()), rect.max);
|
||||
ui.painter().with_clip_rect(object_cell).text(object_cell.left_center() + pad, left, &row.object, font, t.text_dim);
|
||||
let full = if row.object.is_empty() { row.name.clone() } else { format!("{} · {}", row.name, row.object) };
|
||||
r.widget_info(|| egui::WidgetInfo::selected(egui::Role::ListItem, true, selected, &full));
|
||||
// A column clips a long name, so the whole of it shows on hover.
|
||||
r.on_hover_text(full)
|
||||
}
|
||||
|
||||
/// One variable row: its kind's icon, its name, and the object it drives.
|
||||
struct VarRow {
|
||||
name: String,
|
||||
@@ -105,11 +205,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
};
|
||||
// Owned snapshots: the rows below run commands, which borrow the app mutably.
|
||||
let xml_id = app.session.prefs.identify_objects_by == "xmlId";
|
||||
let vars: Vec<VarRow> = st
|
||||
.doc
|
||||
.variables
|
||||
.variables
|
||||
.iter()
|
||||
// In the order `variable.sort` set, which `variable.list` reports too.
|
||||
let all = &st.doc.variables.variables;
|
||||
let vars: Vec<VarRow> = variable_order(st, xml_id)
|
||||
.into_iter()
|
||||
.filter_map(|i| all.get(i))
|
||||
.map(|v| {
|
||||
let bound: Vec<u64> = st.doc.variables.objects_of(&v.name).iter().map(|i| i.0).collect();
|
||||
let object = match bound.as_slice() {
|
||||
@@ -139,6 +239,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let highlighted = highlighted_row(st);
|
||||
// Lock Variables: no variable is defined, deleted or renamed until they are unlocked.
|
||||
let locked = st.doc.variables.locked;
|
||||
let sort = st.variables_sort;
|
||||
|
||||
// The current data set across the top: its name, and the arrows that step through them.
|
||||
ui.horizontal(|ui| {
|
||||
@@ -162,33 +263,24 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
});
|
||||
});
|
||||
|
||||
widgets::subheader(ui, tl!("Variables"));
|
||||
// The list's column headers: a click sorts the list by that column, and a second click
|
||||
// reverses the order.
|
||||
if let Some(next) = headers(ui, sort) {
|
||||
app.run("variable.sort", json!({"by": next.by.id(), "descending": next.descending})).ok();
|
||||
}
|
||||
if vars.is_empty() {
|
||||
widgets::dim_label(ui, tl!("None"));
|
||||
}
|
||||
for row in &vars {
|
||||
ui.horizontal(|ui| {
|
||||
// The kind's own icon, so a text variable and a visibility one read apart.
|
||||
let icon = match row.kind {
|
||||
VariableKind::Text => "type",
|
||||
VariableKind::Visibility => "eye",
|
||||
VariableKind::Image => "image",
|
||||
VariableKind::Graph => "chart-column",
|
||||
VariableKind::Unknown => "square-dashed",
|
||||
};
|
||||
ui.add(egui::Image::new(crate::icons::source(icon)).fit_to_exact_size(egui::vec2(14.0, 14.0)));
|
||||
// The object a variable drives is named beside it, as the reference panel does.
|
||||
let text = if row.object.is_empty() { row.name.clone() } else { format!("{} · {}", row.name, row.object) };
|
||||
let r = ui.selectable_label(highlighted.as_deref() == Some(row.name.as_str()), text);
|
||||
// A click highlights the row and selects what it drives; a double click opens
|
||||
// Variable Options for it.
|
||||
if r.clicked() {
|
||||
click_row(app, &row.name, &row.bound);
|
||||
}
|
||||
if r.double_clicked() && !locked {
|
||||
crate::dialogs::variables::open(app, "variable.rename", Some(&row.name));
|
||||
}
|
||||
});
|
||||
let r = variable_row(ui, row, highlighted.as_deref() == Some(row.name.as_str()));
|
||||
// A click highlights the row and selects what it drives; a double click opens
|
||||
// Variable Options for it.
|
||||
if r.clicked() {
|
||||
click_row(app, &row.name, &row.bound);
|
||||
}
|
||||
if r.double_clicked() && !locked {
|
||||
crate::dialogs::variables::open(app, "variable.rename", Some(&row.name));
|
||||
}
|
||||
}
|
||||
|
||||
widgets::subheader(ui, tl!("Data Sets"));
|
||||
@@ -406,6 +498,69 @@ mod tests {
|
||||
assert!(text.contains("Name") && text.contains("Data Sets") && text.contains("Data Set 1"), "{text}");
|
||||
}
|
||||
|
||||
/// A click on a column header sorts the list by that column, and a second click on it
|
||||
/// reverses the order; the rows are drawn in the order `variable.list` reports.
|
||||
#[test]
|
||||
fn a_header_click_sorts_the_list_and_a_second_click_reverses_it() {
|
||||
use super::super::tests_appearance::{click, frame_events, text_rect};
|
||||
let mut app = app_with_data();
|
||||
let t = app.session.execute("text.create", &json!({"x": 10, "y": 80, "text": "Bob"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.session.execute("layer.setProps", &json!({"id": t, "name": "Aaron"})).unwrap();
|
||||
let alice = app.session.doc().unwrap().doc.variables.objects_of("Name")[0];
|
||||
app.session.execute("layer.setProps", &json!({"id": alice.0, "name": "Bella"})).unwrap();
|
||||
app.session.execute("variable.define", &json!({"name": "age", "kind": "visibility"})).unwrap();
|
||||
app.session.execute("variable.bind", &json!({"variable": "age", "ids": [t]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let drawn = |ctx: &egui::Context, app: &mut VectorcraftApp| -> Vec<String> {
|
||||
let texts = frame_events(ctx, app, vec![], show);
|
||||
let mut rows: Vec<(f32, String)> = ["Name", "age"].iter().map(|n| (text_rect(&texts, n).center().y, n.to_string())).collect();
|
||||
rows.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
rows.into_iter().map(|(_, n)| n).collect()
|
||||
};
|
||||
let listed = |app: &mut VectorcraftApp| -> Vec<String> {
|
||||
let list = app.run("variable.list", json!({})).unwrap();
|
||||
list["variables"].as_array().unwrap().iter().map(|v| v["name"].as_str().unwrap().to_string()).collect()
|
||||
};
|
||||
let sort = |app: &VectorcraftApp| {
|
||||
let s = app.session.doc().unwrap().variables_sort;
|
||||
(s.by, s.descending)
|
||||
};
|
||||
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"], "the order they were defined in");
|
||||
let steps = app.session.doc().unwrap().history.undo.len();
|
||||
|
||||
let header = |ctx: &egui::Context, app: &mut VectorcraftApp, label: &str| text_rect(&frame_events(ctx, app, vec![], show), label).center();
|
||||
let at = header(&ctx, &mut app, "Variables");
|
||||
click(&ctx, &mut app, at, show);
|
||||
assert_eq!(sort(&app), (VariableSortKey::Name, false));
|
||||
assert_eq!(drawn(&ctx, &mut app), ["age", "Name"]);
|
||||
assert_eq!(listed(&mut app), ["age", "Name"]);
|
||||
let at = header(&ctx, &mut app, "Variables");
|
||||
click(&ctx, &mut app, at, show);
|
||||
assert_eq!(sort(&app), (VariableSortKey::Name, true));
|
||||
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"]);
|
||||
|
||||
// Objects: Aaron (age's type) before Bella (Name's), then the reverse.
|
||||
let at = header(&ctx, &mut app, "Objects");
|
||||
click(&ctx, &mut app, at, show);
|
||||
assert_eq!(sort(&app), (VariableSortKey::Object, false));
|
||||
assert_eq!(drawn(&ctx, &mut app), ["age", "Name"]);
|
||||
let at = header(&ctx, &mut app, "Objects");
|
||||
click(&ctx, &mut app, at, show);
|
||||
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"]);
|
||||
|
||||
// The type column's header sits left of the Variables header: text before visibility.
|
||||
let left = text_rect(&frame_events(&ctx, &mut app, vec![], show), "Variables").left() - 4.0 - TYPE_COLUMN / 2.0;
|
||||
let y = header(&ctx, &mut app, "Variables").y;
|
||||
click(&ctx, &mut app, egui::pos2(left, y), show);
|
||||
assert_eq!(sort(&app), (VariableSortKey::Type, false));
|
||||
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"]);
|
||||
click(&ctx, &mut app, egui::pos2(left, y), show);
|
||||
assert_eq!(sort(&app), (VariableSortKey::Type, true));
|
||||
assert_eq!(drawn(&ctx, &mut app), ["age", "Name"]);
|
||||
// Sorting is not an edit.
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps, "no undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_the_art_left_behind_is_in_italics() {
|
||||
let mut app = app_with_data();
|
||||
|
||||
@@ -248,6 +248,122 @@ fn the_status_bar_shows_progress() {
|
||||
assert!(done.iter().any(|t| t == "Exported /out/p.svg") && !done.iter().any(|t| t.contains('…')), "{done:?}");
|
||||
}
|
||||
|
||||
/// An app whose thread-safe writer fails with `e` once `gate` opens.
|
||||
fn failing(gate: Gate, e: &'static str) -> VectorcraftApp {
|
||||
let services = Services {
|
||||
write: Some(Box::new(|_: &str, _: &[u8]| Err("the UI thread must not write here".into()))),
|
||||
write_shared: Some(Arc::new(move |_: &str, _: &[u8]| {
|
||||
gate.wait();
|
||||
Err(e.into())
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
VectorcraftApp::new(Session::new(), services)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_background_save_keeps_a_newer_status_on_success() {
|
||||
let gate = Gate::default();
|
||||
let (mut app, files) = app(gate.clone());
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
app.run("file.save", json!({"path": "/docs/n.vectorcraft"})).unwrap();
|
||||
assert_eq!(app.ui.status, "Saving n.vectorcraft…");
|
||||
io::show_with_notes(&mut app, "Newer".into(), vec!["a note".into()]);
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
assert_eq!(app.ui.status, "Newer");
|
||||
assert_eq!(app.ui.status_notes, Some(("Newer".to_string(), vec!["a note".to_string()])));
|
||||
// The save itself completes all the same.
|
||||
assert_eq!(written(&files), ["/docs/n.vectorcraft"]);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert!(!st.is_dirty());
|
||||
assert_eq!(st.path.as_deref(), Some("/docs/n.vectorcraft"));
|
||||
assert_eq!(app.ui.recent_files.first().map(String::as_str), Some("/docs/n.vectorcraft"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_background_save_keeps_a_newer_status_on_failure() {
|
||||
let gate = Gate::default();
|
||||
let mut app = failing(gate.clone(), "disk full");
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
app.run("file.save", json!({"path": "/docs/f.vectorcraft"})).unwrap();
|
||||
app.status("Newer");
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
assert_eq!(app.ui.status, "Newer");
|
||||
// Not saved, and the write error's dialog still says so.
|
||||
let st = app.session.doc().unwrap();
|
||||
assert!(st.is_dirty() && st.path.is_none(), "not saved");
|
||||
let d = app.ui.dialog.as_ref().expect("the write error's dialog");
|
||||
assert_eq!((d.kind.as_str(), d.str("message").as_str()), (crate::dialogs::confirm::MESSAGE, "Can't save “f.vectorcraft”."));
|
||||
assert!(d.str("detail").contains("disk full"), "{}", d.str("detail"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_background_save_reports_when_its_progress_line_is_current() {
|
||||
// Success and failure, nothing set meanwhile: the save's own result.
|
||||
let gate = Gate::default();
|
||||
let (mut good, _) = app(gate.clone());
|
||||
good.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
good.run("file.save", json!({"path": "/docs/c.vectorcraft"})).unwrap();
|
||||
gate.open();
|
||||
background::wait_all(&mut good);
|
||||
assert_eq!(good.ui.status, "Saved /docs/c.vectorcraft");
|
||||
let gate = Gate::default();
|
||||
let mut bad = failing(gate.clone(), "disk full");
|
||||
bad.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
bad.run("file.save", json!({"path": "/docs/f.vectorcraft"})).unwrap();
|
||||
gate.open();
|
||||
background::wait_all(&mut bad);
|
||||
assert!(bad.ui.status.starts_with("Saving f.vectorcraft failed: ") && bad.ui.status.contains("disk full"), "{}", bad.ui.status);
|
||||
// Another line, then one with the same text as the save's: the later line is not the save's.
|
||||
for ok in [true, false] {
|
||||
let gate = Gate::default();
|
||||
let mut app = if ok { app(gate.clone()).0 } else { failing(gate.clone(), "disk full") };
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
app.run("file.save", json!({"path": "/docs/s.vectorcraft"})).unwrap();
|
||||
app.status("Other");
|
||||
app.status("Saving s.vectorcraft…");
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
assert_eq!(app.ui.status, "Saving s.vectorcraft…", "ok: {ok}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_background_failure_does_not_replace_a_newer_status() {
|
||||
for newer in [true, false] {
|
||||
let gate = Gate::default();
|
||||
let (mut app, _) = app(gate.clone());
|
||||
let ran = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
let (g, seen) = (gate.clone(), ran.clone());
|
||||
let r = background::run(
|
||||
&mut app,
|
||||
true,
|
||||
"Exporting x.svg".into(),
|
||||
None,
|
||||
move |_| {
|
||||
g.wait();
|
||||
Err("broke".into())
|
||||
},
|
||||
move |_, r| {
|
||||
seen.set(true);
|
||||
r
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r["background"], true);
|
||||
if newer {
|
||||
app.status("Newer");
|
||||
}
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
assert!(ran.get(), "the job's callback runs");
|
||||
assert_eq!(app.ui.status, if newer { "Newer" } else { "Exporting x.svg failed: broke" });
|
||||
}
|
||||
}
|
||||
|
||||
/// An app with a control channel: its UI-thread writer records files (or fails with `fail`), and
|
||||
/// its thread-safe writer does the same once `gate` opens.
|
||||
fn controlled(gate: Gate, fail: Option<&'static str>) -> (VectorcraftApp, std::sync::mpsc::Sender<ControlRequest>, Files) {
|
||||
|
||||
@@ -147,9 +147,9 @@ fn more_tools_open_their_options() {
|
||||
assert!(open_options(&mut app, "pieGraph").is_err());
|
||||
assert!(app.ui.dialog.is_none());
|
||||
app.run("graph.create", json!({"type": "column", "x": 0, "y": 0, "width": 200, "height": 150})).unwrap();
|
||||
assert_eq!(open_options(&mut app, "pieGraph").unwrap()["dialog"], "command");
|
||||
assert_eq!(open_options(&mut app, "pieGraph").unwrap()["dialog"], "graphType");
|
||||
let d = app.ui.dialog.take().unwrap();
|
||||
assert_eq!((d.str("__command"), d.str("type")), ("graph.setType".to_string(), "column".to_string()));
|
||||
assert_eq!((d.kind.as_str(), d.str("type")), (crate::dialogs::graph_type::KIND, "column".to_string()));
|
||||
// The Magic Wand panel opens, and stays open on a second double-click.
|
||||
open_options(&mut app, "magicWand").unwrap();
|
||||
assert!(crate::panel_docking::showing(&app.ui, "magicWand"));
|
||||
|
||||
+39
-3
@@ -3418,7 +3418,14 @@ already walks Image Trace. Short agent entry points:
|
||||
without a kind VectorCraft reads is listed with `kind: "unknown"`: it takes no values, and `variable.bind`
|
||||
refuses it. When an opened `.ai`'s layers are read, the objects it binds to its text, visibility and image
|
||||
variables come in bound when they are of the variable's kind, locked and hidden objects included, and
|
||||
`variable.list` counts them in `bindings`.
|
||||
`variable.list` counts them in `bindings`. `variable.sort {by: name|object|type|none, descending?}` sorts the
|
||||
Variables panel's list, as a click on its Variables, Objects or type column header does (a second click on the
|
||||
same header sends `descending: true`): by variable name, by the name of the bound object, or by type in
|
||||
VectorCraft's order (text, image, graph, visibility, then unknown); `none` is the order the variables were
|
||||
defined in. Names compare without regard to case; by object, unbound variables come first and variables bound
|
||||
to several objects last, and ties go by name. Another `by` value, or a `descending` that is not true or false,
|
||||
is an error. The sort is panel state, not saved and not an undo step; `variable.list` lists the variables in
|
||||
that order and reports it as `sort: {by, descending}`.
|
||||
- Data sets: `dataset.list`, `dataset.new {name, values?}`, `dataset.set {name, newName?, values?}` (`newName` also
|
||||
renames the row, in the same undo step; Edit Data Set's Name field sends it), `dataset.rename`,
|
||||
`dataset.delete`, `dataset.capture`, `dataset.update`, `dataset.select {name}`, `dataset.next` /
|
||||
@@ -4432,7 +4439,11 @@ stay tints), `scaleTints` (every colour becomes a tint as light, relative to the
|
||||
darker than the row's average become tints and shades) or `hueShift` (the most saturated colour takes the new colour,
|
||||
the others turn by the same hue). `limitTo` snaps new colours to a swatch library's nearest colour;
|
||||
`recolor.randomize {map, order?, saturationBrightness?, seed?}` shuffles or varies new colours; a row with
|
||||
`exclude: true` keeps its colours. With `limitTo`, a row's `tint` (a percentage, default 100) gives the art
|
||||
`exclude: true` keeps its colours. A row's `from` may be an empty list: the row recolors nothing, adds no library swatch with
|
||||
`limitTo` and gives `group` no color, and `recolor.randomize` and `recolor.weigh` keep it. A row without `from`, or with
|
||||
a current color that is not a color, is refused. With art selected, the Recolor Artwork dialog's New Row button (beside
|
||||
Merge) adds such a row above the first selected row, or at the top, with that row's new color, and selects it; Shift-click another row and Merge to move its colors into it. An
|
||||
empty row does not count toward Colors. With `limitTo`, a row's `tint` (a percentage, default 100) gives the art
|
||||
linked to the global or spot swatch its new color is that share of its tint; `tint` must be a number. `recolor.randomize`
|
||||
moves a row's `tint` with its new color and removes it from a color it changes, and moves a row's `model` with its new color. `swatch.editGroup {group, colors, rename?}` rewrites a colour group in one undo
|
||||
step (art linked to its global swatches follows), and `recolor.apply` with `group` recolours the art and rewrites the
|
||||
@@ -10624,7 +10635,7 @@ drag that places the rings' end point, Space held moves the flare at its current
|
||||
(dialog `artboardOptions`; the Artboard tool's active artboard while it is in use, else the window's; its `x` and `y`
|
||||
are the artboard's top-left corner in document points, while the dialog shows X and Y at the reference point the
|
||||
Transform panel uses, counted from the ruler origin), a graph tool's
|
||||
the selected graph's Graph Type (an error without one), a Symbolism tool's the Symbolism Tools Options (dialog
|
||||
the selected graph's Graph Type (dialog `graphType`, described below; an error without one), a Symbolism tool's the Symbolism Tools Options (dialog
|
||||
`symbolismOptions`: `diameter` in points, `intensity` and `density` 1–10, one brush for the eight tools) and the
|
||||
Magic Wand tool's its panel. `ui.newArtboard` (Alt-click on the Artboards panel's New Artboard) opens Artboard Options
|
||||
for a new artboard, with `new: true` and the name, position and size that `artboard.new {}` would give it. OK runs
|
||||
@@ -10633,6 +10644,31 @@ for a new artboard, with `new: true` and the name, position and size that `artbo
|
||||
default name, "Artboard N" for the Nth artboard.
|
||||
A canvas-click dialog also has `preview` (initially on): tentative settings draw at the remembered click, Reset refreshes that preview, and Cancel discards it without persisting settings. OK accepts one flare as one Undo step. Toolbar-only options draw no artwork.
|
||||
|
||||
Object › Graph › Type… (`graph.setType` without params) and a double-click on a graph tool open Graph Type (dialog
|
||||
`graphType`) for the selected graph, or for the series picked in it with Group Selection. Its fields are the options
|
||||
`graph.setType {}` returns, under the command's names for them. `legend`, `pieLegend`, `piePosition`, `pieSort`,
|
||||
`firstRowInFront`, `firstColumnInFront` and `filledLines` are held for every type, so a type picked in the dialog shows
|
||||
the options it uses. `overrideValues` and `rightOverrideValues` are Override Calculated Values of the left (or only)
|
||||
value axis and of the right one. While one is on, OK sends `axisMin`, `axisMax` and `ticks` (or `rightAxisMin`,
|
||||
`rightAxisMax` and `rightTicks`), and the dialog fills the ones the graph lacks with the values the axis would be
|
||||
calculated with for the type, value axis and Separate Scales the dialog shows.
|
||||
One that was on and is turned off sends `null`, `null` and `0`, and one left off sends none of the three. `__page` is the
|
||||
page the dialog shows: `options` (Graph Options), `valueAxis` (Value Axis, or Left Axis while the value axis is on both
|
||||
sides), `rightAxis` (Right Axis, with the value axis on both sides) or `categoryAxis` (column, stacked column, bar,
|
||||
stacked bar, line and area graphs); a pie graph has the `options` page alone, and a page the type picked lacks shows
|
||||
`options`. With series picked, `type` and `valueAxis` (`left` or `right`) go to those series and the other options to
|
||||
the whole graph, as the command applies them; `type` or `valueAxis` is `null` while the picked series differ in it, and
|
||||
OK then leaves each series as it is. OK runs `graph.setType` with the fields: `legend` for every type but pie,
|
||||
and the three pie options for a pie graph. When the selection changed while the dialog was open, it also sends `id`, and
|
||||
`seriesIndexes` while series are picked and a `type` or `valueAxis` goes to them, so it changes the graph and the series
|
||||
the dialog opened on. A value axis side without a scale of its own (the right side without Separate Scales, or with
|
||||
Separate Scales a side that no series is on) shows the tick values of the scale drawn on it, and its Override Calculated
|
||||
Values is dimmed. A value of `axisMin`, `axisMax` or `ticks` (or of the right axis' three) that is not a number is sent
|
||||
as it is, and `graph.setType` refuses it. With nothing changed it closes without a step of history. An overridden value
|
||||
axis whose maximum is not more than its minimum is the error "Maximum must be more than Minimum." and the dialog stays
|
||||
open. The types the dialog dims for picked series (those that cannot go in the graph), the options it dims for a type
|
||||
and the pages it lists are VectorCraft's own rules.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"tool.setOption","params":{"tool":"flare","values":{"rays":24,"ringsOn":false}}}}
|
||||
```
|
||||
|
||||
@@ -789,6 +789,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Variables: `dataset.select`, `dataset.next` and `dataset.prev` name the area type and type on a path whose text they rewrote and that doesn't fit, in the reply's `overflow` key, as `text.setText` does.
|
||||
- Variables: when no object is bound to a variable that the data set has a value for, for example after Import Data Sets… defines new variables without binding them, choosing the data set in the Variables panel or stepping to it with the panel's arrows shows a status message, and `dataset.select`, `dataset.next` and `dataset.prev` add the message to the reply as `warning`.
|
||||
- Variables: Lock Variables (`variable.lock`, in the Variables panel's bottom bar and menu) stops variables from being defined, deleted or renamed until they are unlocked, while objects can still be bound and unbound and data sets still change and apply; the lock is saved with the document.
|
||||
- Variables: the Variables panel lists its variables in three columns with headers (type, Variables, Objects), and a click on a header sorts the list by variable type, variable name or bound object name, with a second click reversing the order (`variable.sort`; `variable.list` follows the order), as the [Variable panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/variable-panel-overview.html) describes.
|
||||
- Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`.
|
||||
- Batch…: Override Export writes the files of an Export for Screens or Export Assets step into a folder for each source file or data set inside the destination folder, under the names the export gives them (`action.batch`). The folder and file names drop trailing periods and spaces (`a..svg` gives `a`). When two source files would get the same name that way (compared without case), or a later file's folder or file would take the name of an output an earlier file wrote, of a source file in the destination folder or, for data sets, of a file already there, Batch adds `-2`, `-3`… to the later one's name.
|
||||
- Batch…: when a step fails on a file or data set, the error names the step by its place in the action's steps, from 1, counting the Open steps the batch skips (`action.batch`). This numbering is VectorCraft's own rule. Tests `cmd::fileio::action_batch::tests::{a_folder_batch_error_names_the_step_in_the_commands_sent, a_recorded_open_that_fails_is_the_first_step, a_data_set_batch_error_names_the_step_in_the_commands_sent, a_redirected_export_that_fails_keeps_its_place, a_source_that_does_not_open_names_no_step}`.
|
||||
@@ -925,6 +926,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Hindi wording (2026-11-13): rows outside the plug-in messages keep their English's meaning, quantities and clauses, and use the catalog's save, swatch, crop, resolution, path and type terms; a test checks those terms and the point unit in every row; native review remains; scores and estimates unchanged.
|
||||
- Hindi word senses (2026-11-13): 85 labels and messages use the interface sense of their English: Desired is वांछित and the range message keeps both comparisons, rulers are रूलर, text characters कैरेक्टर, vector points पॉइंट, fills फ़िल्स and High Contrast उच्च कंट्रास्ट; the glossary test rejects the unrelated senses in every row; native review remains; scores and estimates unchanged.
|
||||
- Hindi terms (2026-11-13): every row that names a variable (data merge, variable fonts and widths, environment variables) says वेरिएबल, and joining paths, anchor points, point type and lines says जोड़ें instead of शामिल हों, which stays for joining the community; a test checks both terms in every row; scores and estimates unchanged.
|
||||
- Hindi messages its catalog lacked (2026-11-14): 116 status and error messages that the Hindi catalog did not translate, among them the image export, color profile, swatch exchange, color book, DXF, PDF knockout group, Revolve and 3D messages, have Hindi rows; the translations are VectorCraft's own and await native review; scores and estimates unchanged.
|
||||
- Hindi English words (2026-11-13): 264 rows no longer keep English words in their sentences, repeat a parameter's name in Hindi or garble them, among them "unknown align", which now names the alignment; lists of accepted values such as `(left|right|top|bottom)` stay as their English writes them; two tests check every row; native review remains; scores and estimates unchanged.
|
||||
- Message field names and values (2026-11-13): 49 status and error messages in the Hindi, Bulgarian, Danish, Indonesian, Dutch, Brazilian Portuguese, Swedish and Turkish catalogs keep the field names in brackets, such as `[subpath, anchor]`, and the values an `unknown` message lists, such as `(points, lines)`, as their English writes them; a test checks every catalog; scores and estimates unchanged.
|
||||
- Italian messages (2026-11-13): the Italian catalog (`it`) has 1,802 more status and error message rows, and the untranslated report's missing Italian messages go from 1,842 to 40; the translations are VectorCraft's own and await native review; scores and estimates unchanged. See [localization-parity.md](localization-parity.md).
|
||||
@@ -1487,6 +1489,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Image Trace, gray photos:** Color and Grayscale smooth a gray photo within its tones before choosing its colors, as they do a color photo. A picture with at least 95% of its opaque pixels within 8 levels across their channels counts as continuous tone when 8 colors leave an error of more than 6 levels and more than 0.065 of the error of one color, instead of 0.1, unless 16 colors leave at most a tenth of the error of 8 (VectorCraft's own values). That exception keeps gray flat art of up to 16 clean levels on its old route; anti-aliased or noisy gray flat art can now count as continuous tone too. Most gray photos the change reaches, and very noisy gray scans of flat art, trace with fewer specks and fewer staircase edges. Some 16 Colors and High Fidelity Photo palettes of gray photos grow toward the requested count, with more anchors and regions. Some gray illustrations and sketches get more anchors and staircase edges at 16 Colors and High Fidelity Photo, and some 6 Colors traces get more staircase edges. Tests: `tests::a_gray_photo_is_continuous_tone_as_its_color_original_is`, `tests::a_gray_photo_is_smoothed_before_it_is_traced`. Scores and hours unchanged.
|
||||
- **Image Trace, tinted photos:** Color and Grayscale smooth a sepia, single-hue or duotone photo within its tones before choosing its colors, as they do a gray photo. A picture with at least 95% of its opaque pixels within 6 levels of its tone curve counts as continuous tone under the same 0.065 rule as a neutral picture. The tone curve runs through the median color of each 8-level stretch along the direction in which the picture's colors spread the most, and a pixel on it must also lie within 6 levels of the plane the colors spread along most, so art in several unrelated colors is not counted (VectorCraft's own rule and values). The neutral rule's exception for 16 colors leaving at most a tenth of the error of 8 applies to it too. Clean tinted bands of 2, 3 and 9 levels and tinted scans of flat art with up to 8 levels of noise stay on the flat-art route. On the synthetic tinted photos the 16 Colors trace keeps 12 to 15 colors instead of 6 to 13, with 525 to 708 anchors instead of up to 1,402. A duotone with strong noise on each channel can still miss the curve and stay unsmoothed. Tests: `tests::a_tinted_photo_is_continuous_tone_as_a_gray_photo_is`, `tests::small_accents_of_other_colors_leave_a_tinted_photo_continuous_tone`, `tests::tinted_flat_art_stays_flat_art`, `tests::pictures_without_tones_are_not_continuous_tone`, `tests::a_tinted_photo_is_smoothed_before_it_is_traced`. Scores and hours unchanged.
|
||||
- **Image Trace, flat art of 10 to 16 levels:** the exception for 16 colors leaving at most a tenth of the error of 8 also applies to a neutral or tinted picture whose 8-color error is more than 0.1 of the error of one color (VectorCraft's own values), so Color and Grayscale trace clean gray, sepia, single-hue and duotone art of 10 to 16 evenly spaced levels as flat art, without smoothing it first. At 16 Colors, 16 gray or sepia levels crossed by two 1 px lines trace as the 35 regions the lines cut them into instead of 33, and Automatic traces 16 clean sepia levels in 9 colors instead of 4. Tests: `tests::flat_art_of_up_to_sixteen_clean_levels_stays_flat_art`, `tests::thin_lines_on_flat_art_of_up_to_sixteen_clean_levels_are_traced`. Scores and hours unchanged.
|
||||
- **Image Trace, Automatic on clean flat art of up to 16 levels:** when flat art's pixels fall in 2 to 16 histogram bins, each holding at least the share a color needs to be counted, and every two bins lie at least 12 levels apart on some channel (VectorCraft's own value), Automatic counts one color per bin, unless mixed-color merging then finds two of them mixed through each other (dithering, or a thin region beside a close level), in which case it counts colors as before. Median cut kept levels less than 24 apart in one box, so 16 clean gray levels traced in 14 colors and 16 sepia levels in 9; gray, sepia, single-hue and duotone art of 2 to 16 evenly spaced levels now keeps every level. Anti-aliased art whose levels lie more than 20 apart is counted level by level too, because its blended edge pixels are left out of the count. Noisy pictures, anti-aliased art whose levels lie 20 or fewer apart, and pictures of 17 levels or more are counted by median cut as before, and continuous-tone classification is unchanged. Tests: `tests::automatic_keeps_every_level_of_clean_flat_art_of_up_to_sixteen_levels`, `tests::automatic_counts_no_anti_aliased_edge_of_clean_levels_as_a_level`, `tests::automatic_merges_a_dithered_close_level_as_noise`, `quantize::palette::tests::automatic_counts_every_clean_level_of_flat_art`, `quantize::palette::tests::noise_and_anti_aliasing_add_no_clean_levels`. Scores and hours unchanged.
|
||||
- **Image Trace, clean gray photos:** the exception for flat art of up to 16 clean levels now applies when 16 colors leave at most a tenth of the error of 8, instead of a fifth (VectorCraft's own values). On the synthetic gray photo with 0 to 2 levels of noise, 16 colors leave 0.19 of the error of 8, so Color and Grayscale now count it as continuous tone and smooth it before tracing, as they do its color original. Clean gray, sepia, single-hue and duotone art of up to 16 levels, where 16 colors leave no error, stays on the flat-art route. Test: `tests::a_clean_gray_photo_is_continuous_tone_as_its_color_original_is`. Scores and hours unchanged.
|
||||
- **Image Trace, JPEG blocks:** Color, Grayscale and Black and White traces smooth the edges of a JPEG's 8 × 8 blocks before tracing, so a dark ground whose tone changes slowly is no longer traced as staircases along the block edges. The block grid is found in the image itself: the mean step between neighboring pixels must peak at one position in eight, across and down. A cropped JPEG and a JPEG saved in another format are smoothed too. A picture of flat 8 × 8 cells is left as it is, and art made of 8 × 8 cells with slight texture inside each cell is smoothed as a JPEG copy would be. A small step at a block edge, with no edge in the eight pixels around it, is spread over those eight pixels, and larger steps are kept, as the deblocking filters of ITU-T H.263 Annex J and MPEG-4 Part 2 Annex F.3.1 do. Black and White smooths only the smallest steps, and traces of more than 16 colors move every row of a stretch of eight by the same amount (VectorCraft's own rule and values). On a 4000 × 3000 photo saved as JPEG, the straight outline segments that lie on the block grid fall from 52% and 51% (across and down) to 24% and 29% at 16 Colors, and from 54% and 51% to 43% and 42% at High Fidelity Photo; staircase runs fall from 83 to 67 (16 Colors), 11 to 1 (6 Colors), 30 to 13 (Black and White Logo) and 428 to 403 (High Fidelity Photo). A 600 px photo saved as a quality-40 JPEG has 4 staircase runs instead of 2 at 16 Colors. Images without a block grid trace as before. Tests: `tests::a_jpegs_blocks_are_not_traced_as_steps`, `deblock::tests`. Scores and hours unchanged.
|
||||
- **Image Trace, Document Library color groups:** with Document Swatches, the panel's Colors menu and `"colorGroup"` trace with one color group's colors instead of every swatch, as the public panel documentation describes Document Library; code-drawn tests in `crates/engine` and the panel.
|
||||
@@ -1601,6 +1604,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Document Color Mode for effects and graphs:** File › Document Color Mode and `document.open {colorMode}` convert the colors of effects (Drop Shadow, Inner Glow, Outer Glow and the other effects with a color, on objects, their fills and strokes and in graphic styles) and the series paints and label styles that a graph keeps, so a graph drawn again from new data keeps the converted colors; File › Document Color Mode also converts the fill and stroke that new art takes. Undo restores the original new-art fill and stroke and Redo applies the conversion again.
|
||||
- **Recolor Artwork layout (#3416):** the Edit chip list uses the modal scroll helper and its actual remaining height; the tab's minimum-height space scrolls with the chips. `tests_recolor` covers 5/20/200/400 colors, window resizing, 100/125/150/200% UI scaling, scrolling to and editing the final chip, Preview, Cancel, Confirm and Undo, alongside the existing #1733 drag regressions. Scores and estimates unchanged.
|
||||
- **Recolor Artwork Edit displays:** buttons beside the Edit tab's wheel show the colors on a smooth color wheel, a segmented color wheel or as color bars ([Edit colors using Recolor Artwork](https://helpx.adobe.com/illustrator/desktop/manage-colors/modify-colors/edit-colors-using-edit-colors-dialog.html)). The segmented wheel draws 24 hues by 5 saturations as solid patches, and its markers move freely as on the smooth wheel. The bars show one solid bar per recolored color in row order: a click selects a bar for the color sliders, and a drag moves its row without changing a color ([Advanced recolor options overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/learn-color-basics/edit-colors-dialog-overview.html)). The dialog opens on the smooth wheel; `display` (`smooth`, `segmented`, `bars`) is the dialog field. `tests_recolor` covers switching, the dialog height, segmented dragging and Brightness, and bar selection, editing, reordering, scrolling, Preview, Cancel and OK. Scores and estimates unchanged.
|
||||
- **Recolor Artwork New Row:** the Assign tab's New Row button adds a row with no current colors, so current colors can be moved into it and take its new color ([Advanced recolor options overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/learn-color-basics/edit-colors-dialog-overview.html), [Assign new colors to selected artworks](https://helpx.adobe.com/illustrator/desktop/manage-colors/modify-colors/assign-new-colors-to-selected-artwork.html)). VectorCraft's own rules: the row goes above the first selected row (at the top when none is selected) with that row's new color and becomes the selection, and Merge with it as the first selected row moves the other rows' colors into it. The button needs selected art. An empty row recolors nothing, does not count toward Colors, adds no library swatch and gives a rewritten color group no color. Tests: `a_new_row_goes_above_the_selection_and_takes_merged_colors`, `an_empty_row_gives_the_group_no_color` and `colors_merged_into_a_new_row_take_its_new_color` in `crates/ui-egui/src/dialogs/recolor.rs`, `an_empty_row_recolors_nothing` and `an_empty_row_adds_no_swatch_and_no_group_color` in `crates/engine/src/tests/recolor.rs`.
|
||||
- **Edit Colors parameters:** `edit.colors.saturate` needs a number `intensity`, the value from -100% to 100% that Saturate takes ([Adjusting colors](https://web.archive.org/web/20230128232631/https://helpx.adobe.com/illustrator/using/adjusting-colors.html)), and answers "missing number `intensity`" without one; a number outside the range counts as its nearest end. A value that is not a number, such as `"40"`, is refused with `` `intensity` must be a number, not "40" ``, as in every command that needs a number (`tests::params::a_malformed_required_number_is_not_reported_as_missing`). `edit.colors.adjustBalance` refuses a channel and `color.harmony` refuses `steps` or `amount` that is not a number; both read `null` as a value left out. `tests::editcolors` and `tests::colorguide` cover them.
|
||||
- **Convert to Indexed Color:** Edit › Edit Colors › Convert to Indexed Color… (`edit.colors.toIndexed`) stores the embedded images in the selection as indexed-color PNGs of at most 256 colors. The palette is chosen as for a PNG-8 export, as evenly spaced levels of red, green and blue, or from the document's swatches, with diffusion, pattern or noise dithering; pixels under half opacity become transparent. The dialog previews live, and OK is one undo step. This is VectorCraft's own command and counts toward no score. `tests::editcolors` and `encode::tests_palette` cover it. Scores and estimates unchanged.
|
||||
- **Working RGB numeric controls:** the Color panel and Color Picker read RGB, HSB, hex and Web Safe RGB values in the active working RGB space. Editing one RGB channel preserves the other native channels, including colors outside display sRGB. Regressions cover sRGB, Display P3, Wide Gamut RGB and ProPhoto RGB, picker initialization, native numeric round trips, and a slider click with one Undo step. Scores and estimates unchanged.
|
||||
@@ -1668,7 +1672,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Object › Expand Appearance keeps each stroke of a path a stroke (weight, caps, joins, dashes, arrowheads, alignment and width profile) on a copy of the path with the geometry effects baked in, and Object › Expand… is dimmed, with a tooltip, while the selection has appearance attributes, so a stroked line with Zig Zag becomes a stroked zig-zag line and only Expand then outlines it, the two steps the [Expand objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/expand-objects.html) help page describes (#3089). Brushed strokes still become their brush art; `object.expand` still bakes and outlines in one step for agents.
|
||||
- **Masks and clipping:** opacity-mask controls work while the mask is being edited; transparency commands take ids and work without a selection; clipping sets clip by compound paths, even-odd paths, text and groups on screen and in raster export, matching SVG/PDF. Alt-click shows only the mask; SVG masks keep Clip and Invert; the transparency grid is per-document state; a clipping path's own fill and stroke paint behind and over the clipped art; layers can be clipping masks.
|
||||
- **Masks and clipping:** opacity-mask controls work while the mask is being edited; transparency commands take ids and work without a selection; clipping sets clip by compound paths, even-odd paths, text and groups on screen and in raster export, matching SVG/PDF. Alt-click shows only the mask; SVG masks keep Clip and Invert; the transparency grid is per-document view state saved in native files (#1968); a clipping path's own fill and stroke paint behind and over the clipped art; layers can be clipping masks.
|
||||
The geometry effects of a clipping path (Roughen, Transform and the other effects that reshape a path), of its fills and strokes, and of the members of a group used as one, shape the area it clips to on the canvas and in raster export, as in SVG and PDF export: a roughened clipping path clips by its roughened outline. They apply before the geometry effects of the clip group or of a group around it, on the canvas and in every export. When the clipping path has a fill or stroke of its own, the geometry effects of those fills and strokes apply after the group's geometry effects instead. Type used as a clipping path keeps its geometry effects when a group's effect outlines it. Raster effects such as Gaussian Blur leave that area as it is. `effects::tests::clipfx` and the renderer's `tests::clip` cover it.
|
||||
The geometry effects of a clipping path (Roughen, Transform and the other effects that reshape a path), of its fills and strokes, and of the members of a group used as one, shape the area it clips to on the canvas and in raster export, as in SVG and PDF export: a roughened clipping path clips by its roughened outline. They apply before the geometry effects of the clip group or of a group around it, on the canvas and in every export, also when the clipping path paints a fill or stroke of its own: that fill and stroke draw the shapes their effects give them. Type used as a clipping path keeps its geometry effects when a group's effect outlines it. Raster effects such as Gaussian Blur leave that area as it is. `effects::tests::clipfx` and the renderer's `tests::clip` cover it.
|
||||
Clicks, Select Behind, the Unlock submenu, marquees and the Enclosing Lasso inside a clip group or a clipping layer follow that area too: the clipped art is found where the effects moved it and not where they took it away. The clipping path itself is still found by its own outline and anchors. A hidden member of a group used as a clipping path does not clip, and a geometry effect on it does not change where clicks land. Outline view and layers with Preview off draw the plain clipping path, and clicks and marquees there count it. Direct Selection picks and outlines an object of a dynamic symbol's art by the same rules. `doc::hit::tests` and the engine's `tests::clip` cover it.
|
||||
- **Transparency:** three-state Knockout Group (on, neutral, off), Opacity & Mask Define Knockout Shape, Page Isolated Blending and Page Knockout Group, on screen and in SVG/PDF. SVG export writes a knockout group of opaque art plainly, without masks, so shapes meeting edge to edge leave no seam; translucent, blended, masked or brushed art knocks out through masks. Blend modes match the standard formulas on screen, in SVG and in PDF, and non-isolated groups blend with the art below. Flatten Transparency with presets, a dialog and a Flattener Preview panel. Flattening for files without transparency (PDF/X-1a, PDF/X-3) in a CMYK document keeps the inks where it rasterizes, as under an image with see-through pixels: the images it makes hold ink amounts, not screen colours the file would separate again, so a C100 M6 Y41 K3 panel stays C100 M6 Y41 K3 there (#2160, `engine/src/cmd/fileio/tests_pdfx.rs`).
|
||||
On the canvas, the renderer no longer draws the picture of the art below back as an image each time it copies the backdrop of a non-isolated group that blends: it keeps that picture and draws the art that follows over it. At 2880 × 1800, 50 translucent groups with Multiply members take 65 ms a frame instead of 299 ms on 8 threads (`vectorcraft-cli bench`), and the frames have the same pixels.
|
||||
@@ -1732,6 +1736,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Preferences › File Handling › Data Recovery › Folder has a Choose… button on the desktop that picks the folder Data Recovery keeps its copies in ([Illustrator reference, Set Data Recovery preferences](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf)); a picked folder that Data Recovery cannot create its folders in or list is refused with a status message, and the field keeps its value. OK applies the same check to a folder typed into the field (VectorCraft's own rule): when it fails, the dialog stays open, the status bar says why and no preference changes; an empty field (the default folder) and the folder already saved are not checked. Preferences › Plug-ins & Scratch Disks is where additional plug-ins folders are set ([Illustrator preferences help](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/set-app-preferences.html)); its Additional Plug-ins Folder has a Choose… button on the desktop as well, VectorCraft's own, matching Data Recovery's, and VectorCraft's own rule refuses a picked or typed folder whose contents can't be listed. Additional Fonts Folder and Templates Folder stay typed only.
|
||||
`prefs.set` applies the dialog's check to `recoveryFolder` and `pluginsFolder` for every caller (VectorCraft's own rule): a folder that fails returns an error and no preference changes; an empty value and the folder already saved are not checked.
|
||||
When a Data Recovery copy can't be written, the status bar and the log show the error (with the document's title when one document's copy fails), the folder copies go to, whether that is the default recovery folder or the folder set in Preferences, and the preference that picks another folder (`recoveryFolder`; VectorCraft's own message). A copy written in the background reports its result in the status bar only while the status bar still shows that copy's own "Saving recovery data for …" line, so a message shown since then, such as Restore's count with its notes or another copy's line, stays; a failure is still written to the log (VectorCraft's own rule).
|
||||
A file saved in the background reports "Saved …" or its failure in the status bar only while the status bar still shows that save's own "Saving …" line, and a failed background export or print shows "… failed" by the same rule, so a message shown since then stays, also one with the same text as an earlier line; the file is still written, the document still counts as saved, Open Recent still lists it, and a failed write still opens its error dialog (VectorCraft's own rule).
|
||||
After Restore in the dialog Data Recovery shows at launch, the status bar keeps the restored documents' notes (VectorCraft's own rule): it shows the count followed by the first note, and hovering it lists each document's lines about its linked files and its warnings, named with the document's title when more than one was restored. The questions about the documents' missing or modified linked files come one document at a time, with that document made the current one, and a copy that can't be restored shows its error while the hover keeps the count and the restored documents' notes (`tests/recovery.rs`).
|
||||
A name typed in the desktop save panel without the file type's extension gets it, and when a file of that name exists, which the panel did not ask about, Save, Save As, Save a Copy, Save as Template, the exports and every other write through a save panel ask before replacing it (dialog `replaceFile`; Cancel writes nothing).
|
||||
A name returned by the desktop save panel with the extension of a format the save does not write has it replaced with the saved format's: Save As of a new document saves `card.ait` and `card.png` as `card.vectorcraft`, and Save as Template saves `card.pdf` as `card.vctemplate`. An extension that names no format stays part of the name (`notes.v2` is saved as `notes.v2.vectorcraft`).
|
||||
@@ -1759,7 +1764,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
SVG import reads each `<circle>` and `<ellipse>` as the ellipse VectorCraft draws: four smooth anchors on the axes, starting at the right, with handles of the standard length, so an ellipse exported as `<ellipse>` comes back with the anchors it was written from and draws the same (`circles_and_ellipses_import_as_the_ellipse_drawn` in `crates/svg/tests/it/basic_shapes.rs`, `filled_ellipse_on_the_artboard_edge_survives_visually` in `prop_svg.rs`).
|
||||
SVG import keeps attributes in other applications' XML namespaces (`plotter:pen="2"`) on the objects and layers they belong to, as it keeps their `data-*` attributes: copies, duplicates and `.vectorcraft` files keep them, and SVG export writes them back with each namespace declared once on the root (the rest of #522; `crates/svg/tests/it/foreign_attributes.rs`, `tests/attributes.rs`).
|
||||
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps defines every symbol in its editing copy with its art, registration point and place in the Symbols panel's order, and writes its instances as symbol instances, which VectorCraft's `.ai` reader opens as symbols and instances again (`symbols_and_their_instances_are_read_back` in `crates/eps/src/ai_editing/tests.rs`; [file-format-parity.md](file-format-parity.md) lists the instances written as their art).
|
||||
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps keeps envelopes made with a warp, a mesh or a top object editable: its editing copy holds each envelope's contents, shape, Fidelity, Envelope Options and Edit Contents state in the form VectorCraft's `.ai` envelope reader reads, beside its distorted art, so the file opens with the same live envelope, Edit Contents still switching between the contents and the envelope ([Edit the contents of envelopes](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html)). Envelopes of type, envelopes in opacity masks, pattern tiles or another envelope's contents, and envelopes the reader would open with another shape or other settings (a Fidelity that isn't a whole number, bounds margins kept with Distort Appearance off), and envelopes after the first 256 envelopes and images with a Warp in a file are written as their distorted art, with a warning (`envelopes_are_read_back_live` and `envelopes_the_reader_cannot_open_live_are_written_as_their_art` in `crates/eps/src/ai_editing/tests.rs`).
|
||||
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps keeps envelopes made with a warp, a mesh or a top object editable: its editing copy holds each envelope's contents, shape, Fidelity, Envelope Options and Edit Contents state in the form VectorCraft's `.ai` envelope reader reads, beside its distorted art, so the file opens with the same live envelope, Edit Contents still switching between the contents and the envelope ([Edit the contents of envelopes](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html)). Envelopes whose type is threaded, has text wrap options or is written as its outlines, envelopes in opacity masks, pattern tiles or another envelope's contents, and envelopes the reader would open with another shape or other settings (a Fidelity that isn't a whole number, bounds margins kept with Distort Appearance off), and envelopes after the first 256 envelopes and images with a Warp in a file are written as their distorted art, with a warning (`envelopes_are_read_back_live` and `envelopes_the_reader_cannot_open_live_are_written_as_their_art` in `crates/eps/src/ai_editing/tests.rs`).
|
||||
An envelope whose contents hold type keeps the type editable in that `.ai`: the editing copy writes the type as type in the envelope's contents, in the form the `.ai` reader opens as a live envelope of type (`TypedEnvelope` in `crates/eps/src/import/ai/envelope.rs`), so the file opens with the type shown and editable in the envelope's contents, with Edit Contents off or on. An envelope of type in a symbol's art, and one whose type is threaded, has text wrap options or is written as its outlines (mirrored type, for example), is written as its distorted art, as before (`an_envelope_of_type_is_read_back_live_with_its_type`, `an_envelope_of_type_written_as_its_art_leaves_no_story` and `an_envelope_of_type_in_a_symbol_or_a_thread_is_written_as_its_art` in `crates/eps/src/ai_editing/tests.rs`).
|
||||
A top object whose top and bottom sides, or left and right sides, have different numbers of segments is also saved live: the side with fewer segments is split into as many as the side across from it has (each segment into equal spans of its curve, a straight one at even steps), up to the 50 a side the grid holds, so the file opens with the same corners and the same mapping of the contents, its outline holding the added anchors. Where the split side would share out the contents another way, as a side of two curves that aren't one curve can, or a side has more than 50 segments, the envelope is written as its distorted art, with the warning (`a_top_object_with_uneven_sides_is_read_back_live`, `a_top_object_split_to_the_grids_limit_is_read_back_live` and `a_top_object_whose_split_side_would_map_another_way_is_written_as_its_art` in `crates/eps/src/ai_editing/tests.rs`).
|
||||
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps writes eligible paths, compound paths and groups with live effects with an art style when the writer supports their appearance and effects (not a filled path that uses the even-odd rule, and none on a large canvas), which VectorCraft's `.ai` reader opens as the same appearance with its effects live (`objects_with_live_effects_are_read_back` and `every_effect_setting_written_on_an_object_is_read_back` in `crates/eps/src/ai_editing/tests.rs`; [file-format-parity.md](file-format-parity.md) lists the objects written without one and the effects they lose).
|
||||
The same holds for the objects of an opacity mask's art, a mask inside another mask's art and a hidden art root included, so a mask whose art has Offset Path opens drawing the image it masks as it did, with the effect live; geometry effects that no art style holds on a mask's art, such as Roughen, are written applied to the art, which opens drawing what it drew (`a_masked_image_with_live_offset_path_round_trips`, `hidden_and_nested_mask_art_keep_live_offset_path` and `a_masks_art_with_an_effect_no_style_holds_is_written_as_it_draws` in `crates/eps/src/ai_editing/tests.rs`).
|
||||
|
||||
@@ -394,6 +394,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Selected Grid Repeat shows row, column and corner handles and spacing sliders; bounding-box handles take priority where they meet | done | #2691, `tools/src/repeat_grid.rs`; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html); drags preview and commit `object.repeat.options` as one undo step, Escape cancels; Radial Repeat has no handles yet |
|
||||
| Grid Repeat Options (Object › Repeat › Options…) has a Preview checkbox: the repeat on the canvas follows the fields while the dialog is open, Cancel puts the original back without a history step, OK keeps the previewed values as one undo step | done | #2651; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html) lists the Preview option for the grid options only, so Radial and Mirror Repeat Options have none; `dialogs/command.rs` tests |
|
||||
| Type on a Path Options (Type › Type on a Path › Type on a Path Options…, or a double-click on the Type on a Path tools) has a Preview checkbox, off when it opens: the selected type on a path follows Effect, Flip, Align to Path and Spacing while the dialog is open, Cancel puts it back without a history step, OK keeps the previewed values as one undo step | done | #5360; the [public page](https://helpx.adobe.com/illustrator/using/creating-type-path.html) lists the dialog's options, and public tutorials of the dialog show its Preview checkbox; `tests/pathtype.rs` |
|
||||
| Graph Type (Object › Graph › Type…, or a double-click on a graph tool) is a dialog of its own: a menu at the top shows Graph Options or the page of an axis; Graph Options has a button per graph type, Value Axis with Separate Scales, the Style checkboxes (Add Drop Shadow, Add Legend Across Top, First Row in Front, First Column in Front) and the options of the type picked (Column or Bar Width and Cluster Width; Mark Data Points, Connect Data Points, Edge-to-Edge Lines and Draw Filled Lines; a pie graph's Legend, Position and Sort); the Value Axis page (Left Axis and Right Axis with the value axis on both sides) has Tick Values with Override Calculated Values, Tick Marks and Add Labels, and the Category Axis page has Tick Marks with Draw Tick Marks Between Labels; with series picked by Group Selection the type and value axis go to those series | done | Public pages: [graph types](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/change-graph-types.html), [value axes](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/change-graph-value-axes.html), [columns, bars and lines](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/format-columns-bars-and-lines.html), [pie graphs](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/format-pie-graphs.html), [separate scales](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/assign-different-scales-to-value-axes.html), [combined types](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/combine-different-graph-types.html). The options dimmed for a type, the pages listed and the Legend checkbox are VectorCraft's own rules. Dialog `graphType`; `dialogs/graph_type.rs` and its tests |
|
||||
| Selected Mirror Repeat shows a prominent axis with move and rotation handles; bounding-box handles take priority over the axis line | done | #2877, `tools/src/mirror.rs`; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-mirror-repeat-options.html) |
|
||||
| Object › Expand on radial, grid and mirror repeats makes groups of their instances | done | #1765 (#1756) |
|
||||
| Object › Expand… is dimmed while the selection has appearance attributes (effects, several fills or strokes, a fill or stroke with its own transparency or effects, a brush); its tooltip and the status bar say to use Object › Expand Appearance first, which leaves a stroked line with Zig Zag a stroked zig-zag line that Expand then outlines | done | #3089; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/expand-objects.html); `dialogs/expand.rs` tests, engine `tests_objexpand`. `object.expand` (agents, scripts) still bakes effects in one step |
|
||||
|
||||
Reference in New Issue
Block a user