mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Libraries: gradients and patterns can be added to a library and applied from it
Squashed from #4442 onto main.
This commit is contained in:
@@ -7,14 +7,20 @@
|
||||
//! merged into the document's). Items can be put in user-named groups, and a library can be
|
||||
//! exported to a `.vclibrary` file and imported from one (#926). Items and groups are kept in the
|
||||
//! order the Libraries panel shows them, which dragging them there changes.
|
||||
//!
|
||||
//! A gradient is kept without the placement it had on its art, and fits each object it paints. A
|
||||
//! pattern is kept as a native document holding one swatch's worth of the pattern (its
|
||||
//! definition and the resources its art uses) and a thumbnail; using it adds the pattern to the
|
||||
//! document, as its swatch, and paints the selection with it.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::TextStyleDef;
|
||||
use vectorcraft_color::{Color, GradientPaint, Paint};
|
||||
use vectorcraft_doc::{Appearance, Node, NodeId, TextStyleDef};
|
||||
|
||||
use super::clipboard::SwatchChoices;
|
||||
use super::{CommandSpec, EngineError, Result, Session, always, bad, cmd, color_value, has_doc, ids_param, index, ok, point_param, str_param};
|
||||
use crate::Clipboard;
|
||||
|
||||
@@ -34,6 +40,8 @@ const MAX_NAME: usize = 256;
|
||||
const MAX_THUMBNAIL: usize = 4 << 20;
|
||||
/// Pixels a side a thumbnail may claim (they are made at most 256).
|
||||
const MAX_THUMBNAIL_SIDE: u32 = 1024;
|
||||
/// Stops a gradient read from a file may have (fewer than 2 is no gradient).
|
||||
const MAX_STOPS: usize = 1024;
|
||||
|
||||
/// A library: its name and its items, newest last.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
@@ -48,6 +56,11 @@ pub struct Library {
|
||||
pub para_styles: Vec<TextStyleDef>,
|
||||
#[serde(default)]
|
||||
pub graphics: Vec<LibraryGraphic>,
|
||||
/// Files written before gradients and patterns have none.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub gradients: Vec<LibraryGradient>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub patterns: Vec<LibraryPattern>,
|
||||
/// User-named groups of its items, in the order they were made (#926); an item is in one
|
||||
/// group at most, the others are ungrouped. Files written before groups have none.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
@@ -77,6 +90,8 @@ pub enum ItemKind {
|
||||
CharStyle,
|
||||
ParaStyle,
|
||||
Graphic,
|
||||
Gradient,
|
||||
Pattern,
|
||||
/// A kind this version doesn't know (a newer one wrote it): dropped when the file is read.
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
@@ -104,6 +119,27 @@ pub struct LibraryGraphic {
|
||||
pub thumbnail: String,
|
||||
}
|
||||
|
||||
/// A gradient: its stops, kind and angle, without a placement or swatch links.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LibraryGradient {
|
||||
pub name: String,
|
||||
pub gradient: GradientPaint,
|
||||
}
|
||||
|
||||
/// A pattern: a native document whose objects are painted with pattern `pattern` (its definition
|
||||
/// and the resources its art uses), and its thumbnail.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LibraryPattern {
|
||||
pub name: String,
|
||||
/// The pattern's name in `data`.
|
||||
pub pattern: String,
|
||||
/// The native document, base64.
|
||||
pub data: String,
|
||||
/// A PNG of one swatch's worth of the pattern, at most 256 pixels a side, base64.
|
||||
#[serde(default)]
|
||||
pub thumbnail: String,
|
||||
}
|
||||
|
||||
/// The libraries of a session ([`Session::libraries`]).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Libraries {
|
||||
@@ -243,8 +279,8 @@ impl Libraries {
|
||||
}
|
||||
|
||||
impl Library {
|
||||
/// Every item as (kind, key: its name, a graphic's id, name shown): colours, then character
|
||||
/// and paragraph styles, then graphics.
|
||||
/// Every item as (kind, key: its name, a graphic's id, name shown): colors, gradients and
|
||||
/// patterns, then character and paragraph styles, then graphics.
|
||||
pub fn items(&self) -> impl Iterator<Item = (ItemKind, &str, &str)> {
|
||||
fn styles(kind: ItemKind, list: &[TextStyleDef]) -> impl Iterator<Item = (ItemKind, &str, &str)> {
|
||||
list.iter().map(move |t| (kind, t.name.as_str(), t.name.as_str()))
|
||||
@@ -252,6 +288,8 @@ impl Library {
|
||||
self.colors
|
||||
.iter()
|
||||
.map(|c| (ItemKind::Color, c.name.as_str(), c.name.as_str()))
|
||||
.chain(self.gradients.iter().map(|g| (ItemKind::Gradient, g.name.as_str(), g.name.as_str())))
|
||||
.chain(self.patterns.iter().map(|p| (ItemKind::Pattern, p.name.as_str(), p.name.as_str())))
|
||||
.chain(styles(ItemKind::CharStyle, &self.char_styles))
|
||||
.chain(styles(ItemKind::ParaStyle, &self.para_styles))
|
||||
.chain(self.graphics.iter().map(|g| (ItemKind::Graphic, g.id.as_str(), g.name.as_str())))
|
||||
@@ -294,6 +332,8 @@ impl Library {
|
||||
let (from, to_key) = (r.item.as_str(), to.item.as_str());
|
||||
match r.kind {
|
||||
ItemKind::Color => shift(&mut self.colors, |c| c.name.as_str(), from, to_key, after),
|
||||
ItemKind::Gradient => shift(&mut self.gradients, |g| g.name.as_str(), from, to_key, after),
|
||||
ItemKind::Pattern => shift(&mut self.patterns, |p| p.name.as_str(), from, to_key, after),
|
||||
ItemKind::CharStyle => shift(&mut self.char_styles, |c| c.name.as_str(), from, to_key, after),
|
||||
ItemKind::ParaStyle => shift(&mut self.para_styles, |c| c.name.as_str(), from, to_key, after),
|
||||
ItemKind::Graphic => shift(&mut self.graphics, |g| g.id.as_str(), from, to_key, after),
|
||||
@@ -304,16 +344,27 @@ impl Library {
|
||||
/// Make a library read from a file (hand-edited, imported) safe and consistent: at most
|
||||
/// [`MAX_ITEMS`] items of each kind and [`MAX_GROUPS`] groups, names clipped to [`MAX_NAME`]
|
||||
/// characters and unique within their kind (a repeated one gets a number), sizes finite, a
|
||||
/// thumbnail only when it is a small PNG, and groups holding only items there are, each in one
|
||||
/// group at most.
|
||||
/// thumbnail only when it is a small PNG, gradients of 2 to [`MAX_STOPS`] stops in range
|
||||
/// ([`clean_gradient`]; others are dropped), and groups holding only items there are, each in
|
||||
/// one group at most.
|
||||
fn sanitize(&mut self) {
|
||||
clip(&mut self.name);
|
||||
self.colors.truncate(MAX_ITEMS);
|
||||
self.gradients.retain_mut(|g| clean_gradient(&mut g.gradient));
|
||||
self.gradients.truncate(MAX_ITEMS);
|
||||
self.patterns.truncate(MAX_ITEMS);
|
||||
self.char_styles.truncate(MAX_ITEMS);
|
||||
self.para_styles.truncate(MAX_ITEMS);
|
||||
self.graphics.truncate(MAX_ITEMS);
|
||||
self.groups.truncate(MAX_GROUPS);
|
||||
uniquify(self.colors.iter_mut().map(|c| &mut c.name), "Color", " ", false);
|
||||
uniquify(self.gradients.iter_mut().map(|g| &mut g.name), "Gradient", " ", false);
|
||||
uniquify(self.patterns.iter_mut().map(|p| &mut p.name), "Pattern", " ", false);
|
||||
for p in &mut self.patterns {
|
||||
if !thumbnail_ok(&p.thumbnail) {
|
||||
p.thumbnail.clear();
|
||||
}
|
||||
}
|
||||
uniquify(self.char_styles.iter_mut().map(|t| &mut t.name), "Character Style", " ", false);
|
||||
uniquify(self.para_styles.iter_mut().map(|t| &mut t.name), "Paragraph Style", " ", false);
|
||||
uniquify(self.graphics.iter_mut().map(|g| &mut g.name), "Graphic", " ", false);
|
||||
@@ -339,6 +390,33 @@ impl Library {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make `g` a library gradient: no placement, freeform points or swatch links (a linked stop keeps
|
||||
/// the color it shows), the angle its placement showed (a finite one), and stops in order with
|
||||
/// offsets, opacities and midpoints in range. False when it has fewer than 2 or more than
|
||||
/// [`MAX_STOPS`] stops.
|
||||
fn clean_gradient(g: &mut GradientPaint) -> bool {
|
||||
if !(2..=MAX_STOPS).contains(&g.gradient.stops.len()) {
|
||||
return false;
|
||||
}
|
||||
g.angle = g.geom.map_or(g.angle, |geom| geom.angle_deg());
|
||||
g.geom = None;
|
||||
g.freeform = None;
|
||||
g.swatch = None;
|
||||
if !g.angle.is_finite() {
|
||||
g.angle = 0.0;
|
||||
}
|
||||
let within = |v: f32, lo: f32, hi: f32, or: f32| if v.is_finite() { v.clamp(lo, hi) } else { or };
|
||||
for st in &mut g.gradient.stops {
|
||||
st.offset = within(st.offset, 0.0, 1.0, 0.0);
|
||||
st.opacity = within(st.opacity, 0.0, 1.0, 1.0);
|
||||
st.midpoint = within(st.midpoint, 0.13, 0.87, 0.5);
|
||||
st.swatch = None;
|
||||
st.tint = 1.0;
|
||||
}
|
||||
g.gradient.stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
|
||||
true
|
||||
}
|
||||
|
||||
/// `name` cut to [`MAX_NAME`] characters.
|
||||
fn clip(name: &mut String) {
|
||||
if let Some((at, _)) = name.char_indices().nth(MAX_NAME) {
|
||||
@@ -429,6 +507,10 @@ enum Kind {
|
||||
Graphic,
|
||||
FillColor,
|
||||
StrokeColor,
|
||||
FillGradient,
|
||||
StrokeGradient,
|
||||
FillPattern,
|
||||
StrokePattern,
|
||||
CharStyle,
|
||||
ParaStyle,
|
||||
}
|
||||
@@ -440,21 +522,38 @@ impl Kind {
|
||||
Some("fillColor") => Kind::FillColor,
|
||||
Some("strokeColor") => Kind::StrokeColor,
|
||||
Some("color") => Kind::FillColor,
|
||||
Some("fillGradient" | "gradient") => Kind::FillGradient,
|
||||
Some("strokeGradient") => Kind::StrokeGradient,
|
||||
Some("fillPattern" | "pattern") => Kind::FillPattern,
|
||||
Some("strokePattern") => Kind::StrokePattern,
|
||||
Some("charStyle") => Kind::CharStyle,
|
||||
Some("paraStyle") => Kind::ParaStyle,
|
||||
_ => return Err(bad(cmd, "kind must be graphic, fillColor, strokeColor, charStyle or paraStyle")),
|
||||
_ => {
|
||||
return Err(bad(
|
||||
cmd,
|
||||
"kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle",
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The kind as a group names it (fill and stroke colours are one list).
|
||||
/// The kind as a group names it (fill and stroke colors, gradients and patterns are one list
|
||||
/// each).
|
||||
fn item_kind(self) -> ItemKind {
|
||||
match self {
|
||||
Kind::Graphic => ItemKind::Graphic,
|
||||
Kind::FillColor | Kind::StrokeColor => ItemKind::Color,
|
||||
Kind::FillGradient | Kind::StrokeGradient => ItemKind::Gradient,
|
||||
Kind::FillPattern | Kind::StrokePattern => ItemKind::Pattern,
|
||||
Kind::CharStyle => ItemKind::CharStyle,
|
||||
Kind::ParaStyle => ItemKind::ParaStyle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the kind paints strokes by default.
|
||||
fn stroke(self) -> bool {
|
||||
matches!(self, Kind::StrokeColor | Kind::StrokeGradient | Kind::StrokePattern)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -464,7 +563,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Libraries",
|
||||
[],
|
||||
None,
|
||||
"{} → {libraries: [{id, name, graphics, colors, charStyles, paraStyles, groups} (counts)], current: the current library's id or null, folder: the library folder, or null when libraries last for the session only (the web, headless)}",
|
||||
"{} → {libraries: [{id, name, graphics, colors, gradients, patterns, charStyles, paraStyles, groups} (counts)], current: the current library's id or null, folder: the library folder, or null when libraries last for the session only (the web, headless)}",
|
||||
always,
|
||||
list
|
||||
),
|
||||
@@ -473,7 +572,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Library",
|
||||
[],
|
||||
None,
|
||||
"{library?: id or name (default: the current library)} → {id, name, graphics: [{id, name, width, height, thumbnail: PNG base64}], colors: [{name, hex, color}], charStyles: [{name, attrs}], paraStyles: [{name, attrs}], groups: [{name, items: [{kind: color|charStyle|paraStyle|graphic, item: name (graphic: id)}]}] (an item is in one group at most; the others are ungrouped)}",
|
||||
"{library?: id or name (default: the current library)} → {id, name, graphics: [{id, name, width, height, thumbnail: PNG base64}], colors: [{name, hex, color}], gradients: [{name, gradient: {gradient: {kind, stops}, angle}}], patterns: [{name, pattern: its name in the item, thumbnail: PNG base64}], charStyles: [{name, attrs}], paraStyles: [{name, attrs}], groups: [{name, items: [{kind: color|gradient|pattern|charStyle|paraStyle|graphic, item: name (graphic: id)}]}] (an item is in one group at most; the others are ungrouped)}",
|
||||
always,
|
||||
get
|
||||
),
|
||||
@@ -518,7 +617,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add to Library",
|
||||
[],
|
||||
None,
|
||||
"{library?: id or name (default: the current library, made when there is none), kind: graphic|fillColor|strokeColor|charStyle|paraStyle, ids?: objects (default: the selection), name?, color?: colour (a colour to add, instead of the selection's)} add an item from the selection: graphic: a copy of the objects with every resource they use; fillColor/strokeColor: the first selected object's solid fill/stroke colour (else the default fill/stroke); charStyle/paraStyle: the selected text's character/paragraph attributes (the Type tool's range, else the first selected text object). An item the library already has (same kind and contents) isn't added again → {library, kind, name, id?: graphic id, existing: bool}",
|
||||
"{library?: id or name (default: the current library, made when there is none), kind: graphic|fillColor|strokeColor|fillGradient|strokeGradient|fillPattern|strokePattern|charStyle|paraStyle, ids?: objects (default: the selection), name?, color?: colour (a colour to add, instead of the selection's), swatch?: a gradient or pattern swatch of the document (fillGradient…strokePattern: added instead of the selection's paint)} add an item from the selection: graphic: a copy of the objects with every resource they use; fillColor/strokeColor: the first selected object's solid fill/stroke colour (else the default fill/stroke); fillGradient/strokeGradient: the first selected object's fill/stroke gradient (else the default's), without its placement or swatch links, named after its swatch, else \"Gradient\"; fillPattern/strokePattern: the first selected object's fill/stroke pattern (else the default's) with the resources its art uses, named after it; charStyle/paraStyle: the selected text's character/paragraph attributes (the Type tool's range, else the first selected text object). An item the library already has (same kind and contents) isn't added again → {library, kind, name, id?: graphic id, existing: bool}",
|
||||
has_doc,
|
||||
add
|
||||
),
|
||||
@@ -527,7 +626,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Use Library Item",
|
||||
[],
|
||||
None,
|
||||
"{library?, kind, item: name (graphic: id or name), center?: [x, y] (graphic; default: the first artboard's centre), to?: fill|stroke (colours; default: the kind's), recorded?: the item as it was when used (recorded in the journal so a replay matches)} graphic: place a copy centred on `center`, its resources pasted along, selected, one undo step; colours: paint the selection (and the defaults) as paint.setFill/paint.setStroke; charStyle/paraStyle: add the style to the document (a name taken by different attributes gets a number) and apply it to the selected text, one undo step → {kind, ids?, style?}",
|
||||
"{library?, kind, item: name (graphic: id or name), center?: [x, y] (graphic; default: the first artboard's centre), to?: fill|stroke (colors, gradients, patterns; default: the kind's), recorded?: the item as it was when used (recorded in the journal so a replay matches)} graphic: place a copy centred on `center`, its resources pasted along, selected, one undo step; colors and gradients: paint the selection (and the defaults) as paint.setFill/paint.setStroke, a gradient fitting each object; patterns: add the pattern and its swatch to the document (a name the document gives another pattern gets a number; the same pattern is reused) and paint the selection (and the defaults) with it, one undo step; charStyle/paraStyle: add the style to the document (a name taken by different attributes gets a number) and apply it to the selected text, one undo step → {kind, ids?, style?}",
|
||||
has_doc,
|
||||
use_item
|
||||
),
|
||||
@@ -590,7 +689,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Library",
|
||||
[],
|
||||
None,
|
||||
"{library?: id or name (default: the current library), path?} write a library (its colours, styles, graphics with their art and thumbnails, and groups) as a .vclibrary file (JSON, as libraries are kept) → {path, name}; without path → {data: the file's text, name}",
|
||||
"{library?: id or name (default: the current library), path?} write a library (its colours, gradients, patterns, styles, graphics with their art and thumbnails, and groups) as a .vclibrary file (JSON, as libraries are kept) → {path, name}; without path → {data: the file's text, name}",
|
||||
always,
|
||||
export
|
||||
),
|
||||
@@ -599,7 +698,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Import Library",
|
||||
[],
|
||||
None,
|
||||
"{path? | data?: file text | dataBase64?, name?: the library's name (default: the file's)} add the library of a .vclibrary file (as library.export writes) as a new library, made the current one; a name another library has gets a number (`Brand (2)`) → {id, name, graphics, colors, charStyles, paraStyles, groups (counts)}",
|
||||
"{path? | data?: file text | dataBase64?, name?: the library's name (default: the file's)} add the library of a .vclibrary file (as library.export writes) as a new library, made the current one; a name another library has gets a number (`Brand (2)`) → {id, name, graphics, colors, gradients, patterns, charStyles, paraStyles, groups (counts)}",
|
||||
always,
|
||||
import
|
||||
),
|
||||
@@ -626,6 +725,8 @@ fn counts(lib: &Library) -> Value {
|
||||
json!({
|
||||
"graphics": lib.graphics.len(),
|
||||
"colors": lib.colors.len(),
|
||||
"gradients": lib.gradients.len(),
|
||||
"patterns": lib.patterns.len(),
|
||||
"charStyles": lib.char_styles.len(),
|
||||
"paraStyles": lib.para_styles.len(),
|
||||
"groups": lib.groups.len(),
|
||||
@@ -656,6 +757,8 @@ fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"name": lib.name,
|
||||
"graphics": lib.graphics.iter().map(|g| json!({"id": g.id, "name": g.name, "width": g.width, "height": g.height, "thumbnail": g.thumbnail})).collect::<Vec<_>>(),
|
||||
"colors": lib.colors.iter().map(|c| json!({"name": c.name, "hex": c.color.to_hex(), "color": c.color})).collect::<Vec<_>>(),
|
||||
"gradients": lib.gradients.iter().map(|g| json!({"name": g.name, "gradient": g.gradient})).collect::<Vec<_>>(),
|
||||
"patterns": lib.patterns.iter().map(|p| json!({"name": p.name, "pattern": p.pattern, "thumbnail": p.thumbnail})).collect::<Vec<_>>(),
|
||||
"charStyles": lib.char_styles.iter().map(|t| json!({"name": t.name, "attrs": t.attrs})).collect::<Vec<_>>(),
|
||||
"paraStyles": lib.para_styles.iter().map(|t| json!({"name": t.name, "attrs": t.attrs})).collect::<Vec<_>>(),
|
||||
"groups": lib.groups,
|
||||
@@ -706,7 +809,9 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Read what is added before a library is made for it, so a failure makes none.
|
||||
let item = match kind {
|
||||
Kind::Graphic => Item::Graphic(graphic_of(s, p, C)?),
|
||||
Kind::FillColor | Kind::StrokeColor => Item::Color(color_of(s, p, kind == Kind::StrokeColor, C)?),
|
||||
Kind::FillColor | Kind::StrokeColor => Item::Color(color_of(s, p, kind.stroke(), C)?),
|
||||
Kind::FillGradient | Kind::StrokeGradient => Item::Gradient(gradient_of(s, p, kind.stroke(), C)?),
|
||||
Kind::FillPattern | Kind::StrokePattern => Item::Pattern(pattern_of(s, p, kind.stroke(), C)?),
|
||||
Kind::CharStyle | Kind::ParaStyle => {
|
||||
let para = kind == Kind::ParaStyle;
|
||||
let attrs = super::textstyles::selected_style_attrs(s, para)?.ok_or_else(|| bad(C, "select some type first"))?;
|
||||
@@ -740,6 +845,26 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
lib.colors.push(LibraryColor { name: name.clone(), color });
|
||||
json!({ "name": name, "existing": false })
|
||||
}
|
||||
Item::Gradient(mut g) => {
|
||||
full(lib.gradients.len())?;
|
||||
if let Some(old) = lib.gradients.iter().find(|o| o.gradient == g.gradient) {
|
||||
return Ok(json!({ "name": old.name, "existing": true }));
|
||||
}
|
||||
g.name = unique(&given.clone().unwrap_or(g.name), |n| lib.gradients.iter().any(|o| o.name == n));
|
||||
let v = json!({ "name": g.name, "existing": false });
|
||||
lib.gradients.push(g);
|
||||
v
|
||||
}
|
||||
Item::Pattern(mut pat) => {
|
||||
full(lib.patterns.len())?;
|
||||
if let Some(old) = lib.patterns.iter().find(|o| o.data == pat.data) {
|
||||
return Ok(json!({ "name": old.name, "existing": true }));
|
||||
}
|
||||
pat.name = unique(&given.clone().unwrap_or(pat.name), |n| lib.patterns.iter().any(|o| o.name == n));
|
||||
let v = json!({ "name": pat.name, "existing": false });
|
||||
lib.patterns.push(pat);
|
||||
v
|
||||
}
|
||||
Item::Style(para, attrs) => {
|
||||
let list = if para { &mut lib.para_styles } else { &mut lib.char_styles };
|
||||
full(list.len())?;
|
||||
@@ -762,6 +887,8 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
enum Item {
|
||||
Graphic(LibraryGraphic),
|
||||
Color((String, Color)),
|
||||
Gradient(LibraryGradient),
|
||||
Pattern(LibraryPattern),
|
||||
Style(bool, Map<String, Value>),
|
||||
}
|
||||
|
||||
@@ -804,23 +931,7 @@ fn color_of(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<(String,
|
||||
let color = color_value(c).ok_or_else(|| bad(cmd, "bad colour"))?;
|
||||
return Ok((color.to_hex(), color));
|
||||
}
|
||||
let st = s.doc()?;
|
||||
let ids = match ids_param(p, "ids") {
|
||||
Some(ids) => ids,
|
||||
None => st.selection.objects.clone(),
|
||||
};
|
||||
let from_art = ids.iter().filter_map(|id| st.doc.node(*id)).find_map(|n| {
|
||||
let paint = if stroke { n.appearance.stroke().map(|x| &x.paint) } else { n.appearance.fill().map(|f| &f.paint) };
|
||||
paint.cloned()
|
||||
});
|
||||
let paint = match from_art {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let d = &s.paint;
|
||||
if stroke { d.stroke.clone() } else { d.fill.clone() }
|
||||
}
|
||||
};
|
||||
match paint {
|
||||
match selected_paint(s, p, stroke)? {
|
||||
Paint::Solid { color, swatch, .. } => {
|
||||
let name = swatch.filter(|n| !n.is_empty()).unwrap_or_else(|| color.to_hex());
|
||||
Ok((name, color))
|
||||
@@ -830,6 +941,87 @@ fn color_of(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<(String,
|
||||
}
|
||||
}
|
||||
|
||||
/// The first selected object's (or `ids`') fill (`stroke`: stroke) paint (type's: its first
|
||||
/// character's, as the proxy shows it), else the default one.
|
||||
fn selected_paint(s: &Session, p: &Value, stroke: bool) -> Result<Paint> {
|
||||
let st = s.doc()?;
|
||||
let ids = match ids_param(p, "ids") {
|
||||
Some(ids) => ids,
|
||||
None => st.selection.objects.clone(),
|
||||
};
|
||||
let from_art = ids.iter().filter_map(|id| st.doc.node(*id)).find_map(|n| {
|
||||
let (paint, to_doc, _) = n.proxy_paint(stroke, None)?;
|
||||
// Type's characters paint in its own space: a gradient there takes its placement in the
|
||||
// document, so that it keeps the direction it shows.
|
||||
match (paint, n.proxy_gradient(stroke, None)) {
|
||||
(Paint::Gradient(g), Some((_, geom))) if to_doc != vectorcraft_geom::Affine::IDENTITY => {
|
||||
Some(Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), ..(**g).clone() })))
|
||||
}
|
||||
_ => Some(paint.clone()),
|
||||
}
|
||||
});
|
||||
Ok(match from_art {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let d = &s.paint;
|
||||
if stroke { d.stroke.clone() } else { d.fill.clone() }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Gradient swatch `swatch` of the document, else the first selected object's fill (`stroke`:
|
||||
/// stroke) gradient, else the default one, as a library gradient ([`clean_gradient`]) named after
|
||||
/// its swatch, else "Gradient".
|
||||
fn gradient_of(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<LibraryGradient> {
|
||||
let (name, paint) = match str_param(p, "swatch") {
|
||||
Some(name) => {
|
||||
let sw = s.doc()?.doc.swatch(name).filter(|w| matches!(w.paint, Paint::Gradient(_)));
|
||||
(Some(name.to_string()), sw.ok_or_else(|| bad(cmd, format!("no gradient swatch `{name}`")))?.paint.clone())
|
||||
}
|
||||
None => (None, selected_paint(s, p, stroke)?),
|
||||
};
|
||||
let Paint::Gradient(mut g) = paint else {
|
||||
return Err(bad(cmd, if stroke { "the selection has no gradient stroke" } else { "the selection has no gradient fill" }));
|
||||
};
|
||||
let name = name.or_else(|| g.swatch.clone()).filter(|n| !n.trim().is_empty()).unwrap_or_else(|| "Gradient".to_string());
|
||||
if !clean_gradient(&mut g) {
|
||||
return Err(bad(cmd, "the library item is damaged"));
|
||||
}
|
||||
Ok(LibraryGradient { name, gradient: *g })
|
||||
}
|
||||
|
||||
/// Pattern swatch `swatch` of the document, else the first selected object's fill (`stroke`:
|
||||
/// stroke) pattern, else the default one, as a library pattern named after it: a document of one
|
||||
/// rectangle painted with it, one period of its tiling, with the resources its art uses.
|
||||
fn pattern_of(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<LibraryPattern> {
|
||||
let d = &s.doc()?.doc;
|
||||
let name = match str_param(p, "swatch") {
|
||||
Some(name) => match d.swatch(name).map(|w| &w.paint) {
|
||||
Some(Paint::Pattern { pattern, .. }) => pattern.clone(),
|
||||
_ if d.pattern(name).is_some() => name.to_string(),
|
||||
_ => return Err(bad(cmd, format!("no pattern swatch `{name}`"))),
|
||||
},
|
||||
None => match selected_paint(s, p, stroke)? {
|
||||
Paint::Pattern { pattern, .. } => pattern,
|
||||
_ => return Err(bad(cmd, if stroke { "the selection has no pattern stroke" } else { "the selection has no pattern fill" })),
|
||||
},
|
||||
};
|
||||
let def = d.pattern(&name).ok_or_else(|| bad(cmd, format!("no pattern swatch `{name}`")))?;
|
||||
let (w, h) = def.period();
|
||||
let r = vectorcraft_geom::Rect::from_origin_size((0.0, 0.0), (w, h));
|
||||
let paint = vectorcraft_doc::pattern::pattern_paint(&name);
|
||||
let node = Node::path(NodeId(1), vectorcraft_geom::shapes::rectangle(r), Appearance::basic(paint, Paint::None, 0.0));
|
||||
let mut doc = Clipboard::gather(d, vec![node]).to_document();
|
||||
doc.artboards.truncate(1);
|
||||
match doc.artboards.first_mut() {
|
||||
Some(a) => a.rect = r,
|
||||
None => return Err(bad(cmd, "the graphic's document has no artboard")),
|
||||
}
|
||||
let thumbnail = super::fileio::preview_png(&doc)?.map(|b| vectorcraft_format::base64_encode(&b)).unwrap_or_default();
|
||||
let data = vectorcraft_format::base64_encode(&vectorcraft_format::save(&doc, false));
|
||||
Ok(LibraryPattern { name: name.clone(), pattern: name, data, thumbnail })
|
||||
}
|
||||
|
||||
/// A character style's name from its font, style and size ("Source Sans 3 Bold 24 pt"); a
|
||||
/// paragraph style's from its alignment and size too, else "Paragraph Style".
|
||||
fn style_name(attrs: &Map<String, Value>, para: bool) -> String {
|
||||
@@ -871,6 +1063,14 @@ fn use_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = lib.colors.iter().find(|c| c.name == item).ok_or_else(missing)?;
|
||||
json!({ "color": c.color })
|
||||
}
|
||||
Kind::FillGradient | Kind::StrokeGradient => {
|
||||
let g = lib.gradients.iter().find(|g| g.name == item).ok_or_else(missing)?;
|
||||
json!({ "gradient": g.gradient })
|
||||
}
|
||||
Kind::FillPattern | Kind::StrokePattern => {
|
||||
let pat = lib.patterns.iter().find(|x| x.name == item).ok_or_else(missing)?;
|
||||
json!({ "pattern": pat.pattern, "data": pat.data })
|
||||
}
|
||||
Kind::CharStyle | Kind::ParaStyle => {
|
||||
let list = if kind == Kind::ParaStyle { &lib.para_styles } else { &lib.char_styles };
|
||||
let t = list.iter().find(|t| t.name == item).ok_or_else(missing)?;
|
||||
@@ -902,16 +1102,30 @@ fn use_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(r)
|
||||
}
|
||||
Kind::FillColor | Kind::StrokeColor => {
|
||||
let stroke = match str_param(p, "to") {
|
||||
Some("fill") => false,
|
||||
Some("stroke") => true,
|
||||
Some(_) => return Err(bad(C, "to must be fill or stroke")),
|
||||
None => kind == Kind::StrokeColor,
|
||||
};
|
||||
let stroke = to_stroke(p, kind)?;
|
||||
let color = data.get("color").cloned().ok_or_else(|| bad(C, "no colour"))?;
|
||||
s.execute(if stroke { "paint.setStroke" } else { "paint.setFill" }, &json!({ "color": color }))?;
|
||||
Ok(json!({ "kind": if stroke { "strokeColor" } else { "fillColor" } }))
|
||||
}
|
||||
Kind::FillGradient | Kind::StrokeGradient => {
|
||||
let stroke = to_stroke(p, kind)?;
|
||||
let damaged = || bad(C, "the library item is damaged");
|
||||
let mut g: GradientPaint = data.get("gradient").cloned().and_then(|v| serde_json::from_value(v).ok()).ok_or_else(damaged)?;
|
||||
if !clean_gradient(&mut g) {
|
||||
return Err(damaged());
|
||||
}
|
||||
// RGB stops in a CMYK document are stored as CMYK, as a library color is.
|
||||
for st in &mut g.gradient.stops {
|
||||
st.color = super::paint::new_color(s, &json!({}), st.color);
|
||||
}
|
||||
super::paint::apply_paint(s, &json!({}), Paint::Gradient(Box::new(g)), !stroke)?;
|
||||
Ok(json!({ "kind": if stroke { "strokeGradient" } else { "fillGradient" } }))
|
||||
}
|
||||
Kind::FillPattern | Kind::StrokePattern => {
|
||||
let stroke = to_stroke(p, kind)?;
|
||||
let pattern = use_pattern(s, &data, stroke)?;
|
||||
Ok(json!({ "kind": if stroke { "strokePattern" } else { "fillPattern" }, "pattern": pattern }))
|
||||
}
|
||||
Kind::CharStyle | Kind::ParaStyle => {
|
||||
let para = kind == Kind::ParaStyle;
|
||||
let name = data.get("name").and_then(Value::as_str).map(str::trim).filter(|n| !n.is_empty()).unwrap_or("Style").to_string();
|
||||
@@ -932,6 +1146,62 @@ fn use_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `to` param of `library.use`: whether the item paints the stroke (default: the kind's).
|
||||
fn to_stroke(p: &Value, kind: Kind) -> Result<bool> {
|
||||
match str_param(p, "to") {
|
||||
Some("fill") => Ok(false),
|
||||
Some("stroke") => Ok(true),
|
||||
Some(_) => Err(bad("library.use", "to must be fill or stroke")),
|
||||
None => Ok(kind.stroke()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add library pattern `data` (`{pattern, data}`) to the document, with its swatch and the
|
||||
/// resources its art uses (the same pattern there already is reused; a name the document gives
|
||||
/// another one gets a number), and paint the selection's fill (`stroke`: stroke) and the default
|
||||
/// with it, one undo step → the pattern's name in the document.
|
||||
fn use_pattern(s: &mut Session, data: &Value, stroke: bool) -> Result<String> {
|
||||
const C: &str = "library.use";
|
||||
let damaged = || bad(C, "the library item is damaged");
|
||||
let bytes = data.get("data").and_then(Value::as_str).and_then(vectorcraft_format::base64_decode).ok_or_else(damaged)?;
|
||||
let doc = vectorcraft_format::load(&bytes).map_err(|_| damaged())?;
|
||||
let name = data.get("pattern").and_then(Value::as_str).filter(|n| doc.pattern(n).is_some()).ok_or_else(damaged)?.to_string();
|
||||
// Only what paints with the pattern is brought along.
|
||||
let clip = Clipboard::from_document(&doc);
|
||||
let clip = Clipboard::gather(&doc, clip.nodes.into_iter().filter(|n| vectorcraft_doc::pattern::uses_pattern(n, Some(&name))).collect());
|
||||
if clip.nodes.is_empty() {
|
||||
return Err(damaged());
|
||||
}
|
||||
let choices = SwatchChoices::default();
|
||||
// The pattern's name in the document, worked out on a copy: nothing to add is no edit.
|
||||
let mut probe = (*s.doc()?.doc).clone();
|
||||
let imported = clip.import_into(&mut probe, &choices, false);
|
||||
let renamed = imported.renamed.iter().find(|r| r["kind"] == "pattern" && r["from"] == name.as_str()).and_then(|r| r["to"].as_str());
|
||||
let target = renamed.unwrap_or(&name).to_string();
|
||||
let paint = vectorcraft_doc::pattern::pattern_paint(&target);
|
||||
// A pattern the document holds without a swatch (an imported SVG's) gets one.
|
||||
let has_swatch =
|
||||
|d: &vectorcraft_doc::Document| d.swatches_iter().any(|w| matches!(&w.paint, Paint::Pattern { pattern, .. } if *pattern == target));
|
||||
let added = imported.added > 0 || !has_swatch(&probe);
|
||||
drop(probe);
|
||||
in_one_step(s, |s| {
|
||||
if added {
|
||||
s.edit("Add to Swatches", |d, _| {
|
||||
clip.import_into(d, &choices, false);
|
||||
if !has_swatch(d) {
|
||||
let swatch = d.free_swatch_name(&target);
|
||||
d.swatches.push(vectorcraft_color::Swatch { name: swatch, paint: paint.clone(), global: false, spot: false });
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
// The pattern itself, not a swatch of its name (a color swatch can share it); `swatch` keeps
|
||||
// the placement of a pattern the targets already have, as picking its swatch does.
|
||||
super::paint::apply_paint(s, &json!({ "swatch": target }), paint.clone(), !stroke)
|
||||
})?;
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
/// Run `f`'s commands as one undo step, or as part of the step of an undo group open in the active
|
||||
/// document, and undo them together when it fails ([`Session::begin_undo_scope`]).
|
||||
fn in_one_step<T>(s: &mut Session, f: impl FnOnce(&mut Session) -> Result<T>) -> Result<T> {
|
||||
@@ -952,6 +1222,8 @@ fn remove_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
match kind {
|
||||
Kind::Graphic => lib.graphics.retain(|g| g.id != r.item),
|
||||
Kind::FillColor | Kind::StrokeColor => lib.colors.retain(|c| c.name != r.item),
|
||||
Kind::FillGradient | Kind::StrokeGradient => lib.gradients.retain(|g| g.name != r.item),
|
||||
Kind::FillPattern | Kind::StrokePattern => lib.patterns.retain(|x| x.name != r.item),
|
||||
Kind::CharStyle => lib.char_styles.retain(|t| t.name != r.item),
|
||||
Kind::ParaStyle => lib.para_styles.retain(|t| t.name != r.item),
|
||||
}
|
||||
|
||||
@@ -423,7 +423,7 @@ fn apply_to_proxy(s: &mut Session, p: &Value, paint: Paint, cmd: &str) -> Result
|
||||
/// Set the fill (or stroke) of the targets of `p` (the `item` and `ids` params, see
|
||||
/// `paint.setFill`) and of the defaults for new art, focus its proxy unless `focus` is false, and
|
||||
/// remember the paint. Replies `{objects}`: how many objects took the paint.
|
||||
fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<Value> {
|
||||
pub(crate) fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<Value> {
|
||||
let cmd = if fill { "paint.setFill" } else { "paint.setStroke" };
|
||||
let focus = switch_or(p, "focus", true, cmd)?;
|
||||
let span = spans_targets(p, &paint).map_err(|e| bad(cmd, e))?;
|
||||
|
||||
@@ -111,6 +111,293 @@ fn colours_come_from_the_selection_and_paint_it() {
|
||||
assert!(s.execute("library.use", &json!({"library": lib, "kind": "fillColor", "item": "#nope"})).is_err());
|
||||
}
|
||||
|
||||
/// The fill (`stroke`: stroke) paint of object `id`.
|
||||
fn paint_of(s: &Session, id: &Value, stroke: bool) -> vectorcraft_color::Paint {
|
||||
let n = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
|
||||
if stroke { n.appearance.stroke().unwrap().paint.clone() } else { n.appearance.fill().unwrap().paint.clone() }
|
||||
}
|
||||
|
||||
/// The gradient of paint `p`.
|
||||
fn gradient(p: &vectorcraft_color::Paint) -> &vectorcraft_color::GradientPaint {
|
||||
match p {
|
||||
vectorcraft_color::Paint::Gradient(g) => g,
|
||||
other => panic!("not a gradient: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient added from a placed fill keeps its stops, kind and angle without the placement, and
|
||||
/// paints another object's fill or stroke with the same stops; a gradient swatch adds under its
|
||||
/// name.
|
||||
#[test]
|
||||
fn a_gradient_goes_into_a_library_and_paints_another_object() {
|
||||
let mut s = session();
|
||||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||||
let a = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 20}))["id"].clone();
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 0.4, "color": "#00ff00", "opacity": 0.5, "midpoint": 0.3}, {"offset": 1, "color": "#0000ff"}]);
|
||||
let g = json!({"kind": "radial", "stops": stops, "start": [0, 0], "end": [40, 20]});
|
||||
run(&mut s, "paint.setFill", json!({"gradient": g, "ids": [a]}));
|
||||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||||
assert!(s.execute("library.add", &json!({"library": lib, "kind": "strokeGradient"})).is_err(), "its stroke is solid");
|
||||
let added = run(&mut s, "library.add", json!({"library": lib, "kind": "fillGradient"}));
|
||||
assert_eq!((added["name"].as_str(), added["existing"].as_bool()), (Some("Gradient"), Some(false)));
|
||||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "fillGradient"}))["existing"], true);
|
||||
let source = gradient(&paint_of(&s, &a, false)).clone();
|
||||
// In another document: a rectangle's fill, then its stroke.
|
||||
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
|
||||
let b = run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 80, "height": 80}))["id"].clone();
|
||||
run(&mut s, "select.set", json!({"ids": [b]}));
|
||||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "fillGradient", "item": "Gradient"}));
|
||||
assert_eq!(r["kind"], "fillGradient");
|
||||
let fill = gradient(&paint_of(&s, &b, false)).clone();
|
||||
assert_eq!(fill.gradient, source.gradient, "the same kind and stops");
|
||||
assert_eq!((fill.angle, fill.geom), (source.angle, None), "fitted to the object it paints");
|
||||
run(&mut s, "library.use", json!({"library": lib, "kind": "fillGradient", "item": "Gradient", "to": "stroke"}));
|
||||
assert_eq!(gradient(&paint_of(&s, &b, true)).gradient, source.gradient);
|
||||
// A gradient swatch adds under its name.
|
||||
run(
|
||||
&mut s,
|
||||
"swatch.new",
|
||||
json!({"name": "Sunset", "gradient": {"stops": [{"offset": 0, "color": "#ffcc00"}, {"offset": 1, "color": "#cc3300"}]}}),
|
||||
);
|
||||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "fillGradient", "swatch": "Sunset"}))["name"], "Sunset");
|
||||
assert!(s.execute("library.add", &json!({"library": lib, "kind": "fillGradient", "swatch": "White"})).is_err(), "not a gradient swatch");
|
||||
let got = run(&mut s, "library.get", json!({"library": lib}));
|
||||
let names: Vec<&str> = got["gradients"].as_array().unwrap().iter().map(|g| g["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(names, ["Gradient", "Sunset"]);
|
||||
assert!(s.execute("library.use", &json!({"library": lib, "kind": "fillGradient", "item": "Nope"})).is_err());
|
||||
}
|
||||
|
||||
/// A gradient added from an object that was rotated keeps the direction it shows there.
|
||||
#[test]
|
||||
fn a_gradient_from_rotated_art_keeps_its_direction() {
|
||||
let mut s = session();
|
||||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||||
let a = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 20}))["id"].clone();
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]);
|
||||
run(&mut s, "paint.setFill", json!({"gradient": {"kind": "linear", "stops": stops}, "ids": [a]}));
|
||||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||||
run(&mut s, "object.rotate", json!({"angle": 90}));
|
||||
let shown = gradient(&paint_of(&s, &a, false)).geom.expect("placed by the rotation").angle_deg();
|
||||
assert!((shown.abs() - 90.0).abs() < 1e-6, "{shown}");
|
||||
run(&mut s, "library.add", json!({"library": lib, "kind": "fillGradient"}));
|
||||
let got = run(&mut s, "library.get", json!({"library": lib}));
|
||||
let angle = got["gradients"][0]["gradient"]["angle"].as_f64().unwrap();
|
||||
assert!((angle - shown).abs() < 1e-6, "{angle} vs {shown}");
|
||||
}
|
||||
|
||||
/// Type's fill is its characters' paint: a gradient or a color on selected type is what it adds,
|
||||
/// not the default for new art.
|
||||
#[test]
|
||||
fn type_adds_the_paint_of_its_characters() {
|
||||
let mut s = session();
|
||||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||||
let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Hi"}))["id"].clone();
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]);
|
||||
run(&mut s, "paint.setFill", json!({"gradient": {"kind": "linear", "stops": stops}, "ids": [t]}));
|
||||
run(&mut s, "paint.setStroke", json!({"color": "#00ff00", "ids": [t]}));
|
||||
// The defaults for new art differ from the type's paint.
|
||||
run(&mut s, "select.set", json!({"ids": []}));
|
||||
run(&mut s, "paint.setFill", json!({"color": "#123456"}));
|
||||
run(&mut s, "paint.setStroke", json!({"color": "#654321"}));
|
||||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||||
run(&mut s, "library.add", json!({"library": lib, "kind": "fillGradient"}));
|
||||
run(&mut s, "library.add", json!({"library": lib, "kind": "strokeColor"}));
|
||||
let got = run(&mut s, "library.get", json!({"library": lib}));
|
||||
assert_eq!(got["gradients"][0]["gradient"]["gradient"]["stops"].as_array().map(Vec::len), Some(2), "{got}");
|
||||
assert_eq!(got["colors"][0]["name"].as_str().map(str::to_lowercase).as_deref(), Some("#00ff00"), "{got}");
|
||||
// Rotated type: the gradient keeps the direction it shows in the document.
|
||||
run(&mut s, "object.rotate", json!({"angle": 90}));
|
||||
let shown = s.doc().unwrap().doc.node(NodeId(t.as_u64().unwrap())).unwrap().proxy_gradient(false, None).unwrap().1.angle_deg();
|
||||
assert!((shown.abs() - 90.0).abs() < 1e-6, "{shown}");
|
||||
run(&mut s, "library.add", json!({"library": lib, "kind": "fillGradient"}));
|
||||
let got = run(&mut s, "library.get", json!({"library": lib}));
|
||||
let angle = got["gradients"].as_array().unwrap().last().unwrap()["gradient"]["angle"].as_f64().unwrap();
|
||||
assert!((angle - shown).abs() < 1e-6, "{angle} vs {shown}");
|
||||
}
|
||||
|
||||
/// Pattern `name` made from object `id`, in a 20-point tile.
|
||||
fn make_pattern(s: &mut Session, id: &Value, name: &str) {
|
||||
run(s, "select.set", json!({"ids": [id]}));
|
||||
run(s, "object.pattern.make", json!({"name": name, "width": 20, "height": 20}));
|
||||
run(s, "object.pattern.done", json!({}));
|
||||
}
|
||||
|
||||
/// A pattern added from a fill comes with its art; used in a new document it defines the pattern
|
||||
/// and its swatch and paints the selection in one undo step, used again it reuses that pattern,
|
||||
/// and a document whose pattern of that name differs gets it under another name.
|
||||
#[test]
|
||||
fn a_pattern_item_defines_its_pattern_in_a_new_document() {
|
||||
let mut s = session();
|
||||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||||
let dot = run(&mut s, "shape.ellipse", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].clone();
|
||||
run(&mut s, "paint.setFill", json!({"color": "#cc3300", "ids": [dot]}));
|
||||
make_pattern(&mut s, &dot, "Dots");
|
||||
let a = run(&mut s, "shape.rectangle", json!({"x": 50, "y": 50, "width": 40, "height": 40}))["id"].clone();
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Dots", "ids": [a]}));
|
||||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||||
assert!(s.execute("library.add", &json!({"library": lib, "kind": "strokePattern"})).is_err(), "its stroke is solid");
|
||||
let added = run(&mut s, "library.add", json!({"library": lib, "kind": "fillPattern"}));
|
||||
assert_eq!((added["name"].as_str(), added["existing"].as_bool()), (Some("Dots"), Some(false)));
|
||||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "fillPattern", "swatch": "Dots"}))["existing"], true);
|
||||
let got = run(&mut s, "library.get", json!({"library": lib}));
|
||||
assert!(!got["patterns"][0]["thumbnail"].as_str().unwrap().is_empty(), "a thumbnail of the pattern");
|
||||
let art = s.doc().unwrap().doc.pattern("Dots").unwrap().art.clone();
|
||||
// A new document has no pattern until the item is used.
|
||||
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
|
||||
let b = run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 80, "height": 80}))["id"].clone();
|
||||
run(&mut s, "select.set", json!({"ids": [b]}));
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
assert!(s.doc().unwrap().doc.pattern("Dots").is_none());
|
||||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "fillPattern", "item": "Dots"}));
|
||||
assert_eq!((r["kind"].as_str(), r["pattern"].as_str()), (Some("fillPattern"), Some("Dots")));
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.pattern("Dots").unwrap().art.len(), art.len());
|
||||
assert!(d.swatch("Dots").is_some(), "its swatch comes along");
|
||||
assert!(matches!(paint_of(&s, &b, false), vectorcraft_color::Paint::Pattern { ref pattern, .. } if pattern == "Dots"));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
// Used again, on the stroke: the same pattern.
|
||||
run(&mut s, "library.use", json!({"library": lib, "kind": "fillPattern", "item": "Dots", "to": "stroke"}));
|
||||
assert!(matches!(paint_of(&s, &b, true), vectorcraft_color::Paint::Pattern { ref pattern, .. } if pattern == "Dots"));
|
||||
assert_eq!(s.doc().unwrap().doc.patterns.len(), 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.doc().unwrap().doc.pattern("Dots").is_none(), "undone with the paint");
|
||||
// Another document's own "Dots" stays; the library's comes in under another name.
|
||||
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
|
||||
let sq = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 6, "height": 6}))["id"].clone();
|
||||
// Not the library's pattern, which the default fill and stroke now name.
|
||||
run(&mut s, "paint.setFill", json!({"color": "#0044aa", "ids": [sq]}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [sq]}));
|
||||
make_pattern(&mut s, &sq, "Dots");
|
||||
let c = run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 80, "height": 80}))["id"].clone();
|
||||
run(&mut s, "select.set", json!({"ids": [c]}));
|
||||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "fillPattern", "item": "Dots"}));
|
||||
let renamed = r["pattern"].as_str().unwrap().to_string();
|
||||
assert_ne!(renamed, "Dots");
|
||||
assert!(matches!(paint_of(&s, &c, false), vectorcraft_color::Paint::Pattern { ref pattern, .. } if *pattern == renamed));
|
||||
assert_eq!(s.doc().unwrap().doc.patterns.len(), 2);
|
||||
}
|
||||
|
||||
/// A pattern used in a document that already holds it paints with the pattern even when a color
|
||||
/// swatch there has the pattern's name (an imported SVG pattern can be named `White`).
|
||||
#[test]
|
||||
fn a_pattern_item_paints_its_pattern_over_a_color_swatch_of_its_name() {
|
||||
let mut s = session();
|
||||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||||
let dot = run(&mut s, "shape.ellipse", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].clone();
|
||||
run(&mut s, "paint.setFill", json!({"color": "#cc3300", "ids": [dot]}));
|
||||
make_pattern(&mut s, &dot, "Dots");
|
||||
let a = run(&mut s, "shape.rectangle", json!({"x": 50, "y": 50, "width": 40, "height": 40}))["id"].clone();
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Dots", "ids": [a]}));
|
||||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||||
run(&mut s, "library.add", json!({"library": lib, "kind": "fillPattern"}));
|
||||
// The swatch named `Dots` becomes a solid color; the pattern of that name stays.
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
for sw in &mut doc.swatches {
|
||||
if sw.name == "Dots" {
|
||||
sw.paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 1.0, 0.0));
|
||||
}
|
||||
}
|
||||
assert!(doc.pattern("Dots").is_some());
|
||||
let b = run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 40, "height": 40}))["id"].clone();
|
||||
run(&mut s, "select.set", json!({"ids": [b]}));
|
||||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "fillPattern", "item": "Dots"}));
|
||||
let used = r["pattern"].as_str().unwrap().to_string();
|
||||
let paint = paint_of(&s, &b, false);
|
||||
assert!(matches!(paint, vectorcraft_color::Paint::Pattern { ref pattern, .. } if *pattern == used), "{paint:?}");
|
||||
}
|
||||
|
||||
/// A pattern used in a document that holds it without a swatch (as an imported SVG can) gets its
|
||||
/// swatch, in the same undo step as the paint.
|
||||
#[test]
|
||||
fn a_pattern_item_adds_the_swatch_of_a_pattern_the_document_holds_without_one() {
|
||||
let mut s = session();
|
||||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||||
let dot = run(&mut s, "shape.ellipse", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].clone();
|
||||
run(&mut s, "paint.setFill", json!({"color": "#cc3300", "ids": [dot]}));
|
||||
make_pattern(&mut s, &dot, "Dots");
|
||||
let a = run(&mut s, "shape.rectangle", json!({"x": 50, "y": 50, "width": 40, "height": 40}))["id"].clone();
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Dots", "ids": [a]}));
|
||||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||||
run(&mut s, "library.add", json!({"library": lib, "kind": "fillPattern"}));
|
||||
let is_dots = |w: &vectorcraft_color::Swatch| matches!(&w.paint, vectorcraft_color::Paint::Pattern { pattern, .. } if pattern == "Dots");
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).swatches.retain(|w| !is_dots(w));
|
||||
let b = run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 40, "height": 40}))["id"].clone();
|
||||
run(&mut s, "select.set", json!({"ids": [b]}));
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
run(&mut s, "library.use", json!({"library": lib, "kind": "fillPattern", "item": "Dots"}));
|
||||
assert_eq!(s.doc().unwrap().doc.patterns.len(), 1, "the pattern there is reused");
|
||||
assert!(s.doc().unwrap().doc.swatches_iter().any(is_dots), "its swatch is added");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(!s.doc().unwrap().doc.swatches_iter().any(is_dots), "undone with the paint");
|
||||
}
|
||||
|
||||
/// Gradients and patterns go out to a library file and come back; read from a file, a gradient
|
||||
/// with too few stops is dropped, one out of range is put in range, and a pattern whose data is
|
||||
/// damaged or doesn't hold its pattern fails cleanly when used.
|
||||
#[test]
|
||||
fn library_gradients_and_patterns_round_trip_and_damaged_ones_fail_cleanly() {
|
||||
let mut s = session();
|
||||
run(&mut s, "library.create", json!({"name": "Brand"}));
|
||||
let a = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].clone();
|
||||
make_pattern(&mut s, &a, "Squares");
|
||||
run(
|
||||
&mut s,
|
||||
"swatch.new",
|
||||
json!({"name": "Sunset", "gradient": {"stops": [{"offset": 0, "color": "#ffcc00"}, {"offset": 1, "color": "#cc3300"}]}}),
|
||||
);
|
||||
run(&mut s, "library.add", json!({"kind": "fillPattern", "swatch": "Squares"}));
|
||||
run(&mut s, "library.add", json!({"kind": "fillGradient", "swatch": "Sunset"}));
|
||||
run(
|
||||
&mut s,
|
||||
"library.createGroup",
|
||||
json!({"name": "Fills", "items": [{"kind": "gradient", "item": "Sunset"}, {"kind": "pattern", "item": "Squares"}]}),
|
||||
);
|
||||
let data = run(&mut s, "library.export", json!({}))["data"].as_str().unwrap().to_string();
|
||||
let r = run(&mut s, "library.import", json!({"data": data}));
|
||||
assert_eq!((r["gradients"].as_u64(), r["patterns"].as_u64(), r["groups"].as_u64()), (Some(1), Some(1), Some(1)));
|
||||
let (x, y) = (run(&mut s, "library.get", json!({"library": "Brand"})), run(&mut s, "library.get", json!({"library": r["id"]})));
|
||||
for k in ["gradients", "patterns", "groups"] {
|
||||
assert_eq!(x[k], y[k], "{k}");
|
||||
}
|
||||
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
||||
run(&mut s, "library.use", json!({"library": r["id"], "kind": "fillPattern", "item": "Squares"}));
|
||||
assert!(s.doc().unwrap().doc.pattern("Squares").is_some(), "an imported pattern defines itself");
|
||||
let stop = |o: f64| json!({"offset": o, "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}});
|
||||
let paint = |stops: Vec<Value>| json!({"gradient": {"kind": "Linear", "stops": stops}, "angle": 30});
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let empty = vectorcraft_format::base64_encode(&vectorcraft_format::save(&doc, false));
|
||||
let messy = json!({
|
||||
"name": "Messy",
|
||||
"gradients": [
|
||||
{"name": "One", "gradient": paint(vec![stop(0.0)])},
|
||||
{"name": "Wild", "gradient": paint(vec![stop(7.0), {let mut v = stop(-2.0); v["midpoint"] = json!(5); v["opacity"] = json!(-1); v}])}
|
||||
],
|
||||
"patterns": [
|
||||
{"name": "Broken", "pattern": "P", "data": "%%%"},
|
||||
{"name": "Missing", "pattern": "Nowhere", "data": empty}
|
||||
],
|
||||
"groups": [{"name": "G", "items": [{"kind": "gradient", "item": "One"}, {"kind": "gradient", "item": "Wild"}]}]
|
||||
});
|
||||
let r = run(&mut s, "library.import", json!({"data": messy.to_string()}));
|
||||
assert_eq!((r["gradients"].as_u64(), r["patterns"].as_u64()), (Some(1), Some(2)));
|
||||
let got = run(&mut s, "library.get", json!({}));
|
||||
let wild = &got["gradients"][0]["gradient"];
|
||||
assert_eq!(wild["gradient"]["stops"][0]["offset"], 0.0);
|
||||
assert!((wild["gradient"]["stops"][0]["midpoint"].as_f64().unwrap() - 0.87).abs() < 1e-6, "{wild}");
|
||||
assert_eq!(wild["gradient"]["stops"][0]["opacity"], 0.0);
|
||||
assert_eq!(wild["gradient"]["stops"][1]["offset"], 1.0);
|
||||
assert_eq!(got["groups"], json!([{"name": "G", "items": [{"kind": "gradient", "item": "Wild"}]}]));
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
for item in ["Broken", "Missing"] {
|
||||
let e = s.execute("library.use", &json!({"kind": "fillPattern", "item": item})).unwrap_err().to_string();
|
||||
assert!(e.contains("the library item is damaged"), "{item}: {e}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "nothing changed");
|
||||
run(&mut s, "library.use", json!({"kind": "fillGradient", "item": "Wild"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_styles_come_from_the_selected_type_and_apply_in_one_step() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1148,9 +1148,14 @@ fn libraries(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
let lib = r["id"].clone();
|
||||
let Ok(got) = no_panic(s.execute("library.get", &json!({"library": lib}))) else { return };
|
||||
let _ = no_panic(s.execute("select.all", &json!({})));
|
||||
for (list, kind, key) in
|
||||
[("graphics", "graphic", "id"), ("colors", "fillColor", "name"), ("charStyles", "charStyle", "name"), ("paraStyles", "paraStyle", "name")]
|
||||
{
|
||||
for (list, kind, key) in [
|
||||
("graphics", "graphic", "id"),
|
||||
("colors", "fillColor", "name"),
|
||||
("gradients", "fillGradient", "name"),
|
||||
("patterns", "fillPattern", "name"),
|
||||
("charStyles", "charStyle", "name"),
|
||||
("paraStyles", "paraStyle", "name"),
|
||||
] {
|
||||
if let Some(item) = got[list].get(0) {
|
||||
let _ = no_panic(s.execute("library.use", &json!({"library": lib, "kind": kind, "item": item[key]})));
|
||||
let to = json!({"library": lib, "kind": kind, "item": item[key], "group": got["groups"][0]["name"]});
|
||||
@@ -1172,8 +1177,8 @@ fn libraries(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
})
|
||||
}
|
||||
|
||||
/// A Libraries panel library of the rich document's first object (a graphic), a colour, its
|
||||
/// type's character and paragraph styles and a group, exported, made once.
|
||||
/// A Libraries panel library of the rich document's first object (a graphic) and its gradient, a
|
||||
/// colour, a pattern, its type's character and paragraph styles and a group, exported, made once.
|
||||
fn saved_library() -> String {
|
||||
static TEXT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
TEXT.get_or_init(|| {
|
||||
@@ -1182,6 +1187,11 @@ fn saved_library() -> String {
|
||||
let first = s.doc().unwrap().doc.layers[0].children().unwrap()[0].id.0;
|
||||
let g = s.execute("library.add", &json!({"kind": "graphic", "ids": [first]})).unwrap()["id"].clone();
|
||||
s.execute("library.add", &json!({"kind": "fillColor", "color": "#336699"})).unwrap();
|
||||
s.execute("library.add", &json!({"kind": "fillGradient", "ids": [first]})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [first]})).unwrap();
|
||||
s.execute("object.pattern.make", &json!({"name": "Tiles", "width": 200, "height": 150})).unwrap();
|
||||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||||
s.execute("library.add", &json!({"kind": "fillPattern", "swatch": "Tiles"})).unwrap();
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
for kind in ["charStyle", "paraStyle"] {
|
||||
s.execute("library.add", &json!({"kind": kind})).unwrap();
|
||||
|
||||
@@ -3112,6 +3112,11 @@ font-technology Variable Променливи
|
||||
Stroke Щрих
|
||||
Stroke (X), double-click for the Color Picker Щрих (X), щракнете двукратно за палитрата с цветове
|
||||
Stroke Color Цвят на щриха
|
||||
Fill Gradient Градиент на запълване
|
||||
Stroke Gradient Градиент на щриха
|
||||
Fill Pattern Мостра на запълване
|
||||
Stroke Pattern Мостра на щриха
|
||||
Patterns Мостри
|
||||
Stroke Options Опции на щриха
|
||||
Character: Знак:
|
||||
Character options Опции на знака
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4033,6 +4033,11 @@
|
||||
Stroke Traç
|
||||
Stroke (X), double-click for the Color Picker Traç (X), fes doble clic per al selector de color
|
||||
Stroke Color Color del traç
|
||||
Fill Gradient Degradat de farciment
|
||||
Stroke Gradient Degradat del traç
|
||||
Fill Pattern Motiu de farciment
|
||||
Stroke Pattern Motiu del traç
|
||||
Patterns Motius
|
||||
Stroke Direction Direcció del traç
|
||||
Stroke Length Longitud del traç
|
||||
Stroke Options Opcions del traç
|
||||
@@ -5103,6 +5108,7 @@
|
||||
@msg No fonts are missing in this document No falta cap tipus de lletra en aquest document
|
||||
@msg no format to export to no hi ha cap format per exportar-hi
|
||||
@msg no free recovery area no hi ha àrea de recuperació lliure
|
||||
@msg no gradient swatch `{name}` no hi ha la mostra de degradat `{name}`
|
||||
@msg no gradient stop or freeform point is selected no hi ha cap parada de degradat ni cap punt de forma lliure seleccionat
|
||||
@msg no graphic style `{name}` no hi ha l’estil gràfic `{name}`
|
||||
@msg no graphic style `{n}` no hi ha l’estil gràfic `{n}`
|
||||
@@ -5117,6 +5123,7 @@
|
||||
@msg no native format no hi ha format natiu
|
||||
@msg no pages no hi ha pàgines
|
||||
@msg no pattern `{name}` no hi ha el motiu `{name}`
|
||||
@msg no pattern swatch `{name}` no hi ha la mostra de motiu `{name}`
|
||||
@msg no perspective grid preset named `{name}` no hi ha cap preajust de quadrícula de perspectiva anomenat `{name}`
|
||||
@msg no plug-in {0} is installed no hi ha instal·lat el complement {0}
|
||||
@msg no plug-in {id} is installed no hi ha instal·lat el complement {id}
|
||||
@@ -5404,6 +5411,7 @@
|
||||
@msg the image's pixels don't match its size els píxels de la imatge no coincideixen amb la seva mida
|
||||
@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set L’últim inici de VectorCraft va acabar a {name} abans que aparegués la seva finestra, així que s’ha iniciat amb {summary}. Per tornar a provar aquell processador gràfic, canvia Preferències › Rendiment › Processador gràfic o inicia amb WGPU_BACKEND definit
|
||||
@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set L’últim inici de VectorCraft va acabar mentre llistava els processadors gràfics, així que s’ha iniciat amb {summary} només amb OpenGL. Per tornar a provar els altres sistemes gràfics, canvia Preferències › Rendiment › Processador gràfic o inicia amb WGPU_BACKEND definit
|
||||
@msg the library item is damaged l’element de la biblioteca està malmès
|
||||
@msg the layer has no objects to clip by la capa no té objectes amb què retallar
|
||||
@msg the layer or group is locked la capa o el grup està bloquejat
|
||||
@msg the layer to flatten into is hidden la capa on aplanar està oculta
|
||||
@@ -5444,6 +5452,10 @@
|
||||
@msg the screen color sampler is already open el mostrejador de color de pantalla ja és obert
|
||||
@msg the search couldn't start: try again in a moment la cerca no s’ha pogut iniciar: torna-ho a provar d’aquí a un moment
|
||||
@msg the selected stroke's profile is already listed as {_1} el perfil del traç seleccionat ja apareix com a {_1}
|
||||
@msg the selection has no gradient fill la selecció no té farciment de degradat
|
||||
@msg the selection has no gradient stroke la selecció no té traç de degradat
|
||||
@msg the selection has no pattern fill la selecció no té farciment de motiu
|
||||
@msg the selection has no pattern stroke la selecció no té traç de motiu
|
||||
@msg the selection has no URL (or they differ) la selecció no té cap URL (o difereixen)
|
||||
@msg the selection isn't in perspective la selecció no és en perspectiva
|
||||
@msg The shape is too complex for a 3D effect La forma és massa complexa per a un efecte 3D
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -414,6 +414,11 @@
|
||||
Fill Color Barva výplně
|
||||
Opacity Krytí
|
||||
Stroke Color Barva tahu
|
||||
Fill Gradient Přechod výplně
|
||||
Stroke Gradient Přechod tahu
|
||||
Fill Pattern Vzorek výplně
|
||||
Stroke Pattern Vzorek tahu
|
||||
Patterns Vzorky
|
||||
Stroke Weight Tloušťka tahu
|
||||
Graphic Style Grafický styl
|
||||
Symbol Instance Instance symbolu
|
||||
@@ -4117,6 +4122,13 @@ imageTrace Outline Obrys
|
||||
# Status and error messages
|
||||
@msg no library `{id}` knihovna `{id}` neexistuje
|
||||
@msg select some type first nejprve vyberte text
|
||||
@msg the selection has no gradient fill výběr nemá přechodovou výplň
|
||||
@msg the selection has no gradient stroke výběr nemá přechodový tah
|
||||
@msg the selection has no pattern fill výběr nemá vzorkovou výplň
|
||||
@msg the selection has no pattern stroke výběr nemá vzorkový tah
|
||||
@msg no gradient swatch `{name}` přechodová položka vzorníku `{name}` neexistuje
|
||||
@msg no pattern swatch `{name}` vzorková položka vzorníku `{name}` neexistuje
|
||||
@msg the library item is damaged položka knihovny je poškozená
|
||||
@msg {_1} has no {_2} style {_1} nemá styl {_2}
|
||||
@msg a layer couldn't be read vrstvu nelze přečíst
|
||||
@msg it has more than one artboard má více než jednu pracovní plochu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -376,6 +376,11 @@
|
||||
Fill Color Fyldfarve
|
||||
Opacity Opacitet
|
||||
Stroke Color Stregfarve
|
||||
Fill Gradient Fyldgradient
|
||||
Stroke Gradient Streggradient
|
||||
Fill Pattern Fyldmønster
|
||||
Stroke Pattern Stregmønster
|
||||
Patterns Mønstre
|
||||
Stroke Weight Stregtykkelse
|
||||
Graphic Style Grafisk stil
|
||||
Symbol Instance Symbolforekomst
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3147,6 +3147,11 @@ effect Points Punkte
|
||||
Stroke Kontur
|
||||
Stroke (X), double-click for the Color Picker Kontur (X), Doppelklick öffnet den Farbwähler
|
||||
Stroke Color Konturfarbe
|
||||
Fill Gradient Flächenverlauf
|
||||
Stroke Gradient Konturverlauf
|
||||
Fill Pattern Flächenmuster
|
||||
Stroke Pattern Konturmuster
|
||||
Patterns Muster
|
||||
Stroke Options Konturoptionen
|
||||
Character: Zeichen:
|
||||
Character options Zeichenoptionen
|
||||
@@ -3729,6 +3734,13 @@ theme Appearance Erscheinungsbild
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` keine Bibliothek `{id}`
|
||||
@msg select some type first wählen Sie zuerst Text aus
|
||||
@msg the selection has no gradient fill die Auswahl hat keinen Flächenverlauf
|
||||
@msg the selection has no gradient stroke die Auswahl hat keinen Konturverlauf
|
||||
@msg the selection has no pattern fill die Auswahl hat kein Flächenmuster
|
||||
@msg the selection has no pattern stroke die Auswahl hat kein Konturmuster
|
||||
@msg no gradient swatch `{name}` kein Verlaufsfarbfeld `{name}`
|
||||
@msg no pattern swatch `{name}` kein Musterfarbfeld `{name}`
|
||||
@msg the library item is damaged das Bibliothekselement ist beschädigt
|
||||
@msg {_1} has no {_2} style {_1} hat keinen Schnitt „{_2}“
|
||||
@msg a layer couldn't be read eine Ebene konnte nicht gelesen werden
|
||||
@msg it has more than one artboard es enthält mehr als eine Zeichenfläche
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1912,6 +1912,11 @@ font-technology Variable Μεταβλητές
|
||||
Strikethrough Διακριτή διαγραφή
|
||||
Stroke Περίγραμμα
|
||||
Stroke Color Χρώμα περιγράμματος
|
||||
Fill Gradient Ντεγκραντέ γεμίσματος
|
||||
Stroke Gradient Ντεγκραντέ περιγράμματος
|
||||
Fill Pattern Μοτίβο γεμίσματος
|
||||
Stroke Pattern Μοτίβο περιγράμματος
|
||||
Patterns Μοτίβα
|
||||
Stroke Options Επιλογές περιγράμματος
|
||||
Character: Χαρακτήρας:
|
||||
Stroke: Περίγραμμα:
|
||||
@@ -2054,6 +2059,13 @@ axis Both Και τα δύο
|
||||
Dock Panel Αγκύρωση πάνελ
|
||||
@msg no library `{id}` Δεν υπάρχει βιβλιοθήκη με αναγνωριστικό `{id}`
|
||||
@msg select some type first Επιλέξτε πρώτα κάποιο κείμενο
|
||||
@msg the selection has no gradient fill η επιλογή δεν έχει ντεγκραντέ γεμίσματος
|
||||
@msg the selection has no gradient stroke η επιλογή δεν έχει ντεγκραντέ περιγράμματος
|
||||
@msg the selection has no pattern fill η επιλογή δεν έχει μοτίβο γεμίσματος
|
||||
@msg the selection has no pattern stroke η επιλογή δεν έχει μοτίβο περιγράμματος
|
||||
@msg no gradient swatch `{name}` δεν υπάρχει δείγμα ντεγκραντέ `{name}`
|
||||
@msg no pattern swatch `{name}` δεν υπάρχει δείγμα μοτίβου `{name}`
|
||||
@msg the library item is damaged το στοιχείο της βιβλιοθήκης είναι κατεστραμμένο
|
||||
@msg {_1} has no {_2} style Το {_1} δεν έχει στυλ {_2}
|
||||
@msg a layer couldn't be read Δεν ήταν δυνατή η ανάγνωση ενός επιπέδου
|
||||
@msg it has more than one artboard Περιέχει περισσότερους από έναν πίνακες σχεδίασης
|
||||
|
||||
|
@@ -3181,6 +3181,11 @@ font-technology Variable Variables
|
||||
Stroke Trazo
|
||||
Stroke (X), double-click for the Color Picker Trazo (X), doble clic para el selector de color
|
||||
Stroke Color Color de trazo
|
||||
Fill Gradient Degradado de relleno
|
||||
Stroke Gradient Degradado de trazo
|
||||
Fill Pattern Motivo de relleno
|
||||
Stroke Pattern Motivo de trazo
|
||||
Patterns Motivos
|
||||
Stroke Options Opciones de trazo
|
||||
Character: Carácter:
|
||||
Character options Opciones de carácter
|
||||
@@ -3809,6 +3814,13 @@ axis Both Ambos
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` no hay ninguna biblioteca `{id}`
|
||||
@msg select some type first selecciona primero algún texto
|
||||
@msg the selection has no gradient fill la selección no tiene un degradado de relleno
|
||||
@msg the selection has no gradient stroke la selección no tiene un degradado de trazo
|
||||
@msg the selection has no pattern fill la selección no tiene un motivo de relleno
|
||||
@msg the selection has no pattern stroke la selección no tiene un motivo de trazo
|
||||
@msg no gradient swatch `{name}` no existe la muestra de degradado `{name}`
|
||||
@msg no pattern swatch `{name}` no existe la muestra de motivo `{name}`
|
||||
@msg the library item is damaged el elemento de la biblioteca está dañado
|
||||
@msg {_1} has no {_2} style {_1} no tiene el estilo {_2}
|
||||
@msg a layer couldn't be read no se pudo leer una capa
|
||||
@msg it has more than one artboard tiene más de una mesa de trabajo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -411,6 +411,11 @@
|
||||
Fill Color Täyttöväri
|
||||
Opacity Peittävyys
|
||||
Stroke Color Viivan väri
|
||||
Fill Gradient Täytön liukuväri
|
||||
Stroke Gradient Viivan liukuväri
|
||||
Fill Pattern Täytön kuvio
|
||||
Stroke Pattern Viivan kuvio
|
||||
Patterns Kuviot
|
||||
Stroke Weight Viivan paksuus
|
||||
Graphic Style Grafiikkatyyli
|
||||
Symbol Instance Symbolin ilmentymä
|
||||
@@ -4036,6 +4041,13 @@ imageTrace Outline Ääriviiva
|
||||
@plural {n} stroke|{n} strokes {n} viiva|{n} viivaa
|
||||
@msg no library `{id}` ei kirjastoa `{id}`
|
||||
@msg select some type first valitse ensin tekstiä
|
||||
@msg the selection has no gradient fill valinnalla ei ole liukuväritäyttöä
|
||||
@msg the selection has no gradient stroke valinnalla ei ole liukuväriviivaa
|
||||
@msg the selection has no pattern fill valinnalla ei ole kuviotäyttöä
|
||||
@msg the selection has no pattern stroke valinnalla ei ole kuvioviivaa
|
||||
@msg no gradient swatch `{name}` ei liukuvärimallia `{name}`
|
||||
@msg no pattern swatch `{name}` ei kuviomallia `{name}`
|
||||
@msg the library item is damaged kirjaston kohde on vioittunut
|
||||
@msg {_1} has no {_2} style {_1}: ei tyyliä {_2}
|
||||
@msg a layer couldn't be read tasoa ei voitu lukea
|
||||
@msg it has more than one artboard siinä on useampi kuin yksi piirtoalusta
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3178,6 +3178,11 @@ font-technology Variable Variables
|
||||
Stroke Contour
|
||||
Stroke (X), double-click for the Color Picker Contour (X), double-cliquez pour ouvrir le sélecteur de couleurs
|
||||
Stroke Color Couleur du contour
|
||||
Fill Gradient Dégradé de fond
|
||||
Stroke Gradient Dégradé de contour
|
||||
Fill Pattern Motif de fond
|
||||
Stroke Pattern Motif de contour
|
||||
Patterns Motifs
|
||||
Stroke Options Options de contour
|
||||
Character: Caractère :
|
||||
Character options Options de caractère
|
||||
@@ -3806,6 +3811,13 @@ axis Both Les deux
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` aucune bibliothèque `{id}`
|
||||
@msg select some type first sélectionnez d’abord du texte
|
||||
@msg the selection has no gradient fill la sélection n’a pas de dégradé de fond
|
||||
@msg the selection has no gradient stroke la sélection n’a pas de dégradé de contour
|
||||
@msg the selection has no pattern fill la sélection n’a pas de motif de fond
|
||||
@msg the selection has no pattern stroke la sélection n’a pas de motif de contour
|
||||
@msg no gradient swatch `{name}` aucune nuance de dégradé `{name}`
|
||||
@msg no pattern swatch `{name}` aucune nuance de motif `{name}`
|
||||
@msg the library item is damaged l’élément de bibliothèque est endommagé
|
||||
@msg {_1} has no {_2} style {_1} n’a pas de style {_2}
|
||||
@msg a layer couldn't be read un calque n’a pas pu être lu
|
||||
@msg it has more than one artboard il a plus d'un plan de travail
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3843,6 +3843,11 @@ mark-style Japanese Japán
|
||||
Stroke Körvonal
|
||||
Stroke (X), double-click for the Color Picker Körvonal (X), dupla kattintás: Színválasztó
|
||||
Stroke Color Körvonalszín
|
||||
Fill Gradient Kitöltés színátmenete
|
||||
Stroke Gradient Körvonal színátmenete
|
||||
Fill Pattern Kitöltés mintázata
|
||||
Stroke Pattern Körvonal mintázata
|
||||
Patterns Mintázatok
|
||||
Stroke Direction Vonásirány
|
||||
Stroke Length Vonáshossz
|
||||
Stroke options Körvonal beállításai
|
||||
@@ -5004,6 +5009,13 @@ imageTrace Outline Körvonal
|
||||
@msg select paths or anchor points jelölj ki útvonalakat vagy horgonypontokat
|
||||
@msg select paths to join jelöld ki az összekapcsolandó útvonalakat
|
||||
@msg select some type first előbb jelölj ki szöveget
|
||||
@msg the selection has no gradient fill a kijelölésnek nincs színátmenetes kitöltése
|
||||
@msg the selection has no gradient stroke a kijelölésnek nincs színátmenetes körvonala
|
||||
@msg the selection has no pattern fill a kijelölésnek nincs mintázatos kitöltése
|
||||
@msg the selection has no pattern stroke a kijelölésnek nincs mintázatos körvonala
|
||||
@msg no gradient swatch `{name}` nincs `{name}` nevű színátmenet-színminta
|
||||
@msg no pattern swatch `{name}` nincs `{name}` nevű mintázat-színminta
|
||||
@msg the library item is damaged a könyvtárelem sérült
|
||||
@msg Select text or click into text with the Type tool to insert glyphs Glifák beszúrásához jelölj ki szöveget, vagy kattints bele egy szövegbe a Szöveg eszközzel
|
||||
@msg select the art to repeat jelöld ki az ismétlendő grafikát
|
||||
@msg select the artwork to warp jelöld ki a torzítandó grafikát
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -376,6 +376,11 @@
|
||||
Fill Color Warna isian
|
||||
Opacity Opasitas
|
||||
Stroke Color Warna stroke
|
||||
Fill Gradient Gradien isian
|
||||
Stroke Gradient Gradien stroke
|
||||
Fill Pattern Pola isian
|
||||
Stroke Pattern Pola stroke
|
||||
Patterns Pola
|
||||
Stroke Weight Ketebalan stroke
|
||||
Graphic Style Gaya grafis
|
||||
Symbol Instance Instans simbol
|
||||
|
||||
|
@@ -3215,6 +3215,11 @@ mark-style Japanese Giapponese
|
||||
Stroke Traccia
|
||||
Stroke (X), double-click for the Color Picker Traccia (X), fai doppio clic per il selettore colore
|
||||
Stroke Color Colore traccia
|
||||
Fill Gradient Sfumatura riempimento
|
||||
Stroke Gradient Sfumatura traccia
|
||||
Fill Pattern Motivo riempimento
|
||||
Stroke Pattern Motivo traccia
|
||||
Patterns Motivi
|
||||
Stroke Options Opzioni traccia
|
||||
Character: Carattere:
|
||||
Character options Opzioni carattere
|
||||
@@ -3849,6 +3854,13 @@ axis Both Entrambi
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` nessuna libreria `{id}`
|
||||
@msg select some type first seleziona prima del testo
|
||||
@msg the selection has no gradient fill la selezione non ha una sfumatura di riempimento
|
||||
@msg the selection has no gradient stroke la selezione non ha una sfumatura di traccia
|
||||
@msg the selection has no pattern fill la selezione non ha un motivo di riempimento
|
||||
@msg the selection has no pattern stroke la selezione non ha un motivo di traccia
|
||||
@msg no gradient swatch `{name}` nessun campione sfumatura `{name}`
|
||||
@msg no pattern swatch `{name}` nessun campione motivo `{name}`
|
||||
@msg the library item is damaged l'elemento della libreria è danneggiato
|
||||
@msg {_1} has no {_2} style {_1} non ha lo stile {_2}
|
||||
@msg a layer couldn't be read non è stato possibile leggere un livello
|
||||
@msg it has more than one artboard ha più di una tavola da disegno
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -421,6 +421,11 @@ font-technology Variable バリアブル
|
||||
Pie End Angle: 扇形の終了角度:
|
||||
Pie Start Angle: 扇形の開始角度:
|
||||
Stroke Color カラー (線)
|
||||
Fill Gradient グラデーション (塗り)
|
||||
Stroke Gradient グラデーション (線)
|
||||
Fill Pattern パターン (塗り)
|
||||
Stroke Pattern パターン (線)
|
||||
Patterns パターン
|
||||
Stroke Weight 線幅
|
||||
Graphic Style グラフィックスタイル
|
||||
Symbol Instance シンボルインスタンス
|
||||
@@ -4529,6 +4534,13 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg choose a folder to export to 書き出し先のフォルダーを選択してください
|
||||
@msg the selection has no URL (or they differ) 選択したオブジェクトに URL がありません (または URL がそれぞれ異なります)
|
||||
@msg select some type first 先にテキストを選択してください
|
||||
@msg the selection has no gradient fill 選択したオブジェクトに塗りのグラデーションがありません
|
||||
@msg the selection has no gradient stroke 選択したオブジェクトに線のグラデーションがありません
|
||||
@msg the selection has no pattern fill 選択したオブジェクトに塗りのパターンがありません
|
||||
@msg the selection has no pattern stroke 選択したオブジェクトに線のパターンがありません
|
||||
@msg no gradient swatch `{name}` グラデーションスウォッチ `{name}` が見つかりません
|
||||
@msg no pattern swatch `{name}` パターンスウォッチ `{name}` が見つかりません
|
||||
@msg the library item is damaged ライブラリの項目が破損しています
|
||||
@msg {_1} font families available {_1} 個のフォントファミリーを使用できます
|
||||
@msg {_1} has no {_2} style {_1} には {_2} スタイルがありません
|
||||
@msg Copied the CSS of {rules} object(s) {rules} 個のオブジェクトの CSS をコピーしました
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -334,6 +334,11 @@
|
||||
Fill Color შევსების ფერი
|
||||
Opacity გაუმჭვირვალობა
|
||||
Stroke Color კონტურის ფერი
|
||||
Fill Gradient შევსების გრადიენტი
|
||||
Stroke Gradient კონტურის გრადიენტი
|
||||
Fill Pattern შევსების პატერნი
|
||||
Stroke Pattern კონტურის პატერნი
|
||||
Patterns პატერნები
|
||||
Stroke Weight კონტურის სისქე
|
||||
Graphic Style გრაფიკული სტილი
|
||||
Symbol Instance სიმბოლოს ეგზემპლარი
|
||||
@@ -3617,6 +3622,13 @@ font-technology Variable ცვლადი
|
||||
@plural {n} stroke|{n} strokes კონტური: {n}
|
||||
@msg no library `{id}` ბიბლიოთეკა `{id}` არ არსებობს
|
||||
@msg select some type first ჯერ მონიშნეთ ტექსტი
|
||||
@msg the selection has no gradient fill მონიშნულს შევსების გრადიენტი არ აქვს
|
||||
@msg the selection has no gradient stroke მონიშნულს კონტურის გრადიენტი არ აქვს
|
||||
@msg the selection has no pattern fill მონიშნულს შევსების პატერნი არ აქვს
|
||||
@msg the selection has no pattern stroke მონიშნულს კონტურის პატერნი არ აქვს
|
||||
@msg no gradient swatch `{name}` გრადიენტის ფერის ნიმუში `{name}` არ არის
|
||||
@msg no pattern swatch `{name}` პატერნის ფერის ნიმუში `{name}` არ არის
|
||||
@msg the library item is damaged ბიბლიოთეკის ელემენტი დაზიანებულია
|
||||
@msg {_1} has no {_2} style {_1}-ს არ აქვს სტილი {_2}
|
||||
@msg a layer couldn't be read შრის წაკითხვა ვერ მოხერხდა
|
||||
@msg it has more than one artboard მას ერთზე მეტი სამუშაო დაფა აქვს
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -417,6 +417,11 @@ font-technology Variable 가변
|
||||
Pie End Angle: 파이 끝 각도:
|
||||
Pie Start Angle: 파이 시작 각도:
|
||||
Stroke Color 선 색상
|
||||
Fill Gradient 칠 그레이디언트
|
||||
Stroke Gradient 선 그레이디언트
|
||||
Fill Pattern 칠 패턴
|
||||
Stroke Pattern 선 패턴
|
||||
Patterns 패턴
|
||||
Stroke Weight 선 두께
|
||||
Graphic Style 그래픽 스타일
|
||||
Symbol Instance 심볼 인스턴스
|
||||
@@ -4461,6 +4466,13 @@ theme Appearance 외관
|
||||
@msg choose a folder to export to 내보낼 폴더를 선택하세요
|
||||
@msg the selection has no URL (or they differ) 선택 항목에 URL이 없거나 서로 다릅니다
|
||||
@msg select some type first 먼저 문자를 선택하세요
|
||||
@msg the selection has no gradient fill 선택 항목에 칠 그레이디언트가 없습니다
|
||||
@msg the selection has no gradient stroke 선택 항목에 선 그레이디언트가 없습니다
|
||||
@msg the selection has no pattern fill 선택 항목에 칠 패턴이 없습니다
|
||||
@msg the selection has no pattern stroke 선택 항목에 선 패턴이 없습니다
|
||||
@msg no gradient swatch `{name}` 그레이디언트 견본 `{name}`이(가) 없습니다
|
||||
@msg no pattern swatch `{name}` 패턴 견본 `{name}`이(가) 없습니다
|
||||
@msg the library item is damaged 라이브러리 항목이 손상되었습니다
|
||||
@msg {_1} font families available 사용 가능한 글꼴 패밀리 {_1}개
|
||||
@msg {_1} has no {_2} style {_1}에는 {_2} 스타일이 없습니다
|
||||
@msg Copied the CSS of {rules} object(s) 개체 {rules}개의 CSS를 복사했습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -390,6 +390,11 @@
|
||||
Fill Color Vulkleur
|
||||
Opacity Dekking
|
||||
Stroke Color Contourkleur
|
||||
Fill Gradient Vulverloop
|
||||
Stroke Gradient Contourverloop
|
||||
Fill Pattern Vulpatroon
|
||||
Stroke Pattern Contourpatroon
|
||||
Patterns Patronen
|
||||
Stroke Weight Contourdikte
|
||||
Graphic Style Grafische stijl
|
||||
Symbol Instance Symboolinstantie
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -374,6 +374,11 @@
|
||||
Fill Color Kolor wypełnienia
|
||||
Opacity Krycie
|
||||
Stroke Color Kolor obrysu
|
||||
Fill Gradient Gradient wypełnienia
|
||||
Stroke Gradient Gradient obrysu
|
||||
Fill Pattern Wzorek wypełnienia
|
||||
Stroke Pattern Wzorek obrysu
|
||||
Patterns Wzorki
|
||||
Stroke Weight Grubość obrysu
|
||||
Graphic Style Styl graficzny
|
||||
Symbol Instance Wystąpienie symbolu
|
||||
@@ -3941,6 +3946,13 @@ imageTrace Outline Kontur
|
||||
@plural {n} stroke|{n} strokes {n} obrys|{n} obrysy|{n} obrysów
|
||||
@msg no library `{id}` brak biblioteki `{id}`
|
||||
@msg select some type first najpierw zaznacz tekst
|
||||
@msg the selection has no gradient fill zaznaczenie nie ma gradientu wypełnienia
|
||||
@msg the selection has no gradient stroke zaznaczenie nie ma gradientu obrysu
|
||||
@msg the selection has no pattern fill zaznaczenie nie ma wzorka wypełnienia
|
||||
@msg the selection has no pattern stroke zaznaczenie nie ma wzorka obrysu
|
||||
@msg no gradient swatch `{name}` brak próbki gradientu `{name}`
|
||||
@msg no pattern swatch `{name}` brak próbki wzorka `{name}`
|
||||
@msg the library item is damaged element biblioteki jest uszkodzony
|
||||
@msg {_1} has no {_2} style {_1} nie ma stylu {_2}
|
||||
@msg a layer couldn't be read nie udało się odczytać warstwy
|
||||
@msg it has more than one artboard ma więcej niż jeden obszar kompozycji
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2630,6 +2630,11 @@
|
||||
Strikethrough Tachado
|
||||
Stroke Traçado
|
||||
Stroke Color Cor do Traçado
|
||||
Fill Gradient Gradiente de Preenchimento
|
||||
Stroke Gradient Gradiente do Traçado
|
||||
Fill Pattern Padrão de Preenchimento
|
||||
Stroke Pattern Padrão do Traçado
|
||||
Patterns Padrões
|
||||
Stroke Options Opções de Traçado
|
||||
Stroke Weight Peso do Traçado
|
||||
Character: Caractere:
|
||||
@@ -3396,6 +3401,13 @@ axis Both Ambos
|
||||
Drag to the window's left edge to dock Arraste até a borda esquerda da janela para encaixar
|
||||
@msg no library `{id}` nenhuma biblioteca `{id}`
|
||||
@msg select some type first selecione algum texto primeiro
|
||||
@msg the selection has no gradient fill a seleção não tem gradiente de preenchimento
|
||||
@msg the selection has no gradient stroke a seleção não tem gradiente de traçado
|
||||
@msg the selection has no pattern fill a seleção não tem padrão de preenchimento
|
||||
@msg the selection has no pattern stroke a seleção não tem padrão de traçado
|
||||
@msg no gradient swatch `{name}` nenhuma amostra de gradiente `{name}`
|
||||
@msg no pattern swatch `{name}` nenhuma amostra de padrão `{name}`
|
||||
@msg the library item is damaged o item da biblioteca está danificado
|
||||
@msg {_1} has no {_2} style {_1} não tem o estilo {_2}
|
||||
@msg a layer couldn't be read não foi possível ler uma camada
|
||||
@msg it has more than one artboard tem mais de uma prancheta
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3211,6 +3211,11 @@ mark-style Japanese Японский
|
||||
Stroke Обводка
|
||||
Stroke (X), double-click for the Color Picker Обводка (X), дважды щёлкните для палитры цветов
|
||||
Stroke Color Цвет обводки
|
||||
Fill Gradient Градиент заливки
|
||||
Stroke Gradient Градиент обводки
|
||||
Fill Pattern Узор заливки
|
||||
Stroke Pattern Узор обводки
|
||||
Patterns Узоры
|
||||
Stroke Options Параметры обводки
|
||||
Character: Символ:
|
||||
Character options Параметры символа
|
||||
@@ -3845,6 +3850,13 @@ axis Both Оба
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` нет библиотеки `{id}`
|
||||
@msg select some type first сначала выделите текст
|
||||
@msg the selection has no gradient fill у выделения нет градиентной заливки
|
||||
@msg the selection has no gradient stroke у выделения нет градиентной обводки
|
||||
@msg the selection has no pattern fill у выделения нет узорной заливки
|
||||
@msg the selection has no pattern stroke у выделения нет узорной обводки
|
||||
@msg no gradient swatch `{name}` нет градиентного образца `{name}`
|
||||
@msg no pattern swatch `{name}` нет образца узора `{name}`
|
||||
@msg the library item is damaged элемент библиотеки повреждён
|
||||
@msg {_1} has no {_2} style у {_1} нет начертания {_2}
|
||||
@msg a layer couldn't be read не удалось прочитать слой
|
||||
@msg it has more than one artboard в нём несколько монтажных областей
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3112,6 +3112,11 @@ font-technology Variable Variabla
|
||||
Stroke Kontur
|
||||
Stroke (X), double-click for the Color Picker Kontur (X), dubbelklicka för färgväljaren
|
||||
Stroke Color Konturfärg
|
||||
Fill Gradient Fyllnadsgradient
|
||||
Stroke Gradient Konturgradient
|
||||
Fill Pattern Fyllnadsmönster
|
||||
Stroke Pattern Konturmönster
|
||||
Patterns Mönster
|
||||
Stroke Options Konturalternativ
|
||||
Character: Tecken:
|
||||
Character options Teckenalternativ
|
||||
@@ -3730,6 +3735,13 @@ axis Both Båda
|
||||
Float Tool Group Gör verktygsgruppen flytande
|
||||
@msg no library `{id}` inget bibliotek `{id}`
|
||||
@msg select some type first markera någon text först
|
||||
@msg the selection has no gradient fill markeringen har ingen fyllnadsgradient
|
||||
@msg the selection has no gradient stroke markeringen har ingen konturgradient
|
||||
@msg the selection has no pattern fill markeringen har inget fyllnadsmönster
|
||||
@msg the selection has no pattern stroke markeringen har inget konturmönster
|
||||
@msg no gradient swatch `{name}` ingen gradientfärgruta `{name}`
|
||||
@msg no pattern swatch `{name}` ingen mönsterfärgruta `{name}`
|
||||
@msg the library item is damaged biblioteksobjektet är skadat
|
||||
@msg {_1} has no {_2} style {_1} har ingen stil {_2}
|
||||
@msg a layer couldn't be read ett lager kunde inte läsas
|
||||
@msg it has more than one artboard den har mer än en rityta
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -3017,6 +3017,11 @@ font-technology Variable Değişken
|
||||
Stroke Kontur
|
||||
Stroke (X), double-click for the Color Picker Kontur (X), Renk Seçici için çift tıklayın
|
||||
Stroke Color Kontur Rengi
|
||||
Fill Gradient Dolgu Degradesi
|
||||
Stroke Gradient Kontur Degradesi
|
||||
Fill Pattern Dolgu Deseni
|
||||
Stroke Pattern Kontur Deseni
|
||||
Patterns Desenler
|
||||
Stroke Options Kontur Seçenekleri
|
||||
Character: Karakter:
|
||||
Character options Karakter seçenekleri
|
||||
@@ -3621,6 +3626,13 @@ axis Both Her İkisi
|
||||
Float Tool Group Araç Grubunu Yüzer Yap
|
||||
@msg no library `{id}` `{id}` kütüphanesi yok
|
||||
@msg select some type first önce bir tür seçin
|
||||
@msg the selection has no gradient fill seçimin degrade dolgusu yok
|
||||
@msg the selection has no gradient stroke seçimin degrade konturu yok
|
||||
@msg the selection has no pattern fill seçimin desen dolgusu yok
|
||||
@msg the selection has no pattern stroke seçimin desen konturu yok
|
||||
@msg no gradient swatch `{name}` degrade renk örneği yok `{name}`
|
||||
@msg no pattern swatch `{name}` desen renk örneği yok `{name}`
|
||||
@msg the library item is damaged kitaplık öğesi bozuk
|
||||
@msg {_1} has no {_2} style {_1}'nin {_2} stili yok
|
||||
@msg a layer couldn't be read bir katman okunamadı
|
||||
@msg it has more than one artboard birden fazla çalışma yüzeyi var
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -3208,6 +3208,11 @@ effect Points Точки
|
||||
Stroke Обведення
|
||||
Stroke (X), double-click for the Color Picker Обведення (X), двічі клацніть на палітрі кольорів
|
||||
Stroke Color Колір обведення
|
||||
Fill Gradient Градієнт заливки
|
||||
Stroke Gradient Градієнт обведення
|
||||
Fill Pattern Візерунок заливки
|
||||
Stroke Pattern Візерунок обведення
|
||||
Patterns Візерунки
|
||||
Stroke Options Параметри обведення
|
||||
Character: Символ:
|
||||
Character options Параметри символу
|
||||
@@ -3833,6 +3838,13 @@ axis Both Обидві
|
||||
Float Tool Group Від’єднати групу інструментів
|
||||
@msg no library `{id}` немає бібліотеки `{id}`
|
||||
@msg select some type first спочатку виділіть текст
|
||||
@msg the selection has no gradient fill виділення не має градієнтної заливки
|
||||
@msg the selection has no gradient stroke виділення не має градієнтного обведення
|
||||
@msg the selection has no pattern fill виділення не має візерункової заливки
|
||||
@msg the selection has no pattern stroke виділення не має візерункового обведення
|
||||
@msg no gradient swatch `{name}` немає градієнтного зразка `{name}`
|
||||
@msg no pattern swatch `{name}` немає зразка візерунка `{name}`
|
||||
@msg the library item is damaged елемент бібліотеки пошкоджено
|
||||
@msg {_1} has no {_2} style {_1} не має накреслення {_2}
|
||||
@msg a layer couldn't be read не вдалося прочитати шар
|
||||
@msg it has more than one artboard у ньому кілька монтажних областей
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 749 and column 40.
|
@@ -3564,6 +3564,11 @@ mark-style Japanese Kiểu Nhật
|
||||
Stroke Nét
|
||||
Stroke (X), double-click for the Color Picker Nét (X), bấm đúp để mở bộ chọn màu
|
||||
Stroke Color Màu nét
|
||||
Fill Gradient Chuyển sắc tô
|
||||
Stroke Gradient Chuyển sắc nét
|
||||
Fill Pattern Họa tiết tô
|
||||
Stroke Pattern Họa tiết nét
|
||||
Patterns Họa tiết
|
||||
Stroke Direction Hướng nét
|
||||
Stroke Length Độ dài nét
|
||||
Stroke Options Tùy chọn nét
|
||||
@@ -4656,6 +4661,13 @@ theme Appearance Giao diện
|
||||
@msg select paths or anchor points chọn đường dẫn hoặc điểm neo
|
||||
@msg select paths to join hãy chọn đường dẫn để nối
|
||||
@msg select some type first hãy chọn chữ trước
|
||||
@msg the selection has no gradient fill vùng chọn không có tô chuyển sắc
|
||||
@msg the selection has no gradient stroke vùng chọn không có nét chuyển sắc
|
||||
@msg the selection has no pattern fill vùng chọn không có tô họa tiết
|
||||
@msg the selection has no pattern stroke vùng chọn không có nét họa tiết
|
||||
@msg no gradient swatch `{name}` không có mẫu màu chuyển sắc `{name}`
|
||||
@msg no pattern swatch `{name}` không có mẫu họa tiết `{name}`
|
||||
@msg the library item is damaged mục thư viện bị hỏng
|
||||
@msg Select text or click into text with the Type tool to insert glyphs Chọn văn bản hoặc bấm vào văn bản bằng công cụ Chữ để chèn glyph
|
||||
@msg select the art to repeat hãy chọn tác phẩm để lặp lại
|
||||
@msg select the artwork to warp hãy chọn tác phẩm để biến dạng
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4096 and column 10.
|
@@ -3046,6 +3046,11 @@ font-script Japanese 汉字
|
||||
Stroke 描边
|
||||
Stroke (X), double-click for the Color Picker 描边 (X),双击打开拾色器
|
||||
Stroke Color 描边颜色
|
||||
Fill Gradient 填色渐变
|
||||
Stroke Gradient 描边渐变
|
||||
Fill Pattern 填色图案
|
||||
Stroke Pattern 描边图案
|
||||
Patterns 图案
|
||||
Stroke Options 描边选项
|
||||
Character: 字符:
|
||||
Character options 字符选项
|
||||
@@ -4250,6 +4255,13 @@ imageTrace Outline 轮廓
|
||||
@msg choose a folder to export to 请选择要导出到的文件夹
|
||||
@msg the selection has no URL (or they differ) 所选对象没有 URL (或其 URL 各不相同)
|
||||
@msg select some type first 请先选择一些文字
|
||||
@msg the selection has no gradient fill 所选对象没有渐变填色
|
||||
@msg the selection has no gradient stroke 所选对象没有渐变描边
|
||||
@msg the selection has no pattern fill 所选对象没有图案填色
|
||||
@msg the selection has no pattern stroke 所选对象没有图案描边
|
||||
@msg no gradient swatch `{name}` 没有渐变色板 `{name}`
|
||||
@msg no pattern swatch `{name}` 没有图案色板 `{name}`
|
||||
@msg the library item is damaged 库项目已损坏
|
||||
@msg {_1} font families available 有 {_1} 个可用的字体系列
|
||||
@msg {_1} has no {_2} style {_1} 没有 {_2} 样式
|
||||
@msg Copied the CSS of {rules} object(s) 已复制 {rules} 个对象的 CSS
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2204,6 +2204,13 @@ file-filter Templates 範本
|
||||
@msg choose a folder to export to 請選擇要匯出至的資料夾
|
||||
@msg the selection has no URL (or they differ) 選取範圍沒有 URL (或其 URL 各不相同)
|
||||
@msg select some type first 請先選取一些文字
|
||||
@msg the selection has no gradient fill 選取範圍沒有漸層填色
|
||||
@msg the selection has no gradient stroke 選取範圍沒有漸層筆畫
|
||||
@msg the selection has no pattern fill 選取範圍沒有圖樣填色
|
||||
@msg the selection has no pattern stroke 選取範圍沒有圖樣筆畫
|
||||
@msg no gradient swatch `{name}` 沒有漸層色票 `{name}`
|
||||
@msg no pattern swatch `{name}` 沒有圖樣色票 `{name}`
|
||||
@msg the library item is damaged 資料庫項目已損毀
|
||||
@msg {_1} font families available 有 {_1} 個可用的字體家族
|
||||
@msg {_1} has no {_2} style {_1} 沒有 {_2} 樣式
|
||||
@msg Copied the CSS of {rules} object(s) 已複製 {rules} 個物件的 CSS
|
||||
@@ -4006,6 +4013,11 @@ font-script Japanese 漢字
|
||||
Stroke 筆畫
|
||||
Stroke (X), double-click for the Color Picker 筆畫 (X),按兩下開啟檢色器
|
||||
Stroke Color 筆畫顏色
|
||||
Fill Gradient 填色漸層
|
||||
Stroke Gradient 筆畫漸層
|
||||
Fill Pattern 填色圖樣
|
||||
Stroke Pattern 筆畫圖樣
|
||||
Patterns 圖樣
|
||||
Stroke Options 筆畫選項
|
||||
Character: 字元:
|
||||
Character options 字元選項
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1,9 +1,11 @@
|
||||
//! Libraries panel (Window › Libraries, #745): the current library's colours, character and
|
||||
//! paragraph styles and graphics, from the engine's `library.*` commands. Art dragged off the canvas
|
||||
//! onto the panel, and swatches dragged in, are added to it; the + button adds the selection's
|
||||
//! graphic, fill or stroke colour or text styles. A colour click paints the selection through the
|
||||
//! active Fill/Stroke proxy, a style click applies it to the selected type, and a graphic is
|
||||
//! dragged onto the canvas (or double-clicked) to place a copy. Libraries are kept on this machine.
|
||||
//! Libraries panel (Window › Libraries, #745): the current library's colors, gradients, patterns,
|
||||
//! character and paragraph styles and graphics, from the engine's `library.*` commands. Art
|
||||
//! dragged off the canvas onto the panel, and swatches dragged in (colors, gradients and
|
||||
//! patterns), are added to it; the + button adds the selection's graphic, fill or stroke color,
|
||||
//! pattern or gradient, or text styles. A click on a color, gradient or pattern paints the
|
||||
//! selection through the active Fill/Stroke proxy, a style click applies it to the selected type,
|
||||
//! and a graphic is dragged onto the canvas (or double-clicked) to place a copy. Libraries are
|
||||
//! kept on this machine.
|
||||
//!
|
||||
//! The search field filters the items by name; items can be moved into user-named groups, shown as
|
||||
//! collapsible sections after the ungrouped items; and the panel menu exports the library to a
|
||||
@@ -116,6 +118,19 @@ fn grabbed(drag: &PanelDrag, library: &str) -> Option<Grab> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `library.add` params that add what `drag` holds to library `library`: a color (not one of
|
||||
/// that library's own), or a gradient or pattern swatch, by its name.
|
||||
fn swatch_add(drag: &PanelDrag, library: &str) -> Option<Value> {
|
||||
let PanelDrag::Paint { paint, params, library: from, .. } = drag else { return None };
|
||||
let swatch = || params.get("swatch").and_then(Value::as_str);
|
||||
match paint {
|
||||
Paint::Solid { color, .. } if from.as_ref().is_none_or(|(l, _)| l != library) => Some(json!({ "kind": "fillColor", "color": color })),
|
||||
Paint::Gradient(_) => swatch().map(|s| json!({ "kind": "fillGradient", "swatch": s })),
|
||||
Paint::Pattern { .. } => swatch().map(|s| json!({ "kind": "fillPattern", "swatch": s })),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the item list draws from.
|
||||
struct Items<'a> {
|
||||
library: &'a str,
|
||||
@@ -291,13 +306,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
act = Some(Act::Run("library.add", json!({ "kind": "graphic", "ids": ids })));
|
||||
}
|
||||
if let Some(drag) = zone.dnd_hover_payload::<PanelDrag>()
|
||||
&& let PanelDrag::Paint { paint: Paint::Solid { color, .. }, library, .. } = &*drag
|
||||
&& library.as_ref().is_none_or(|(l, _)| *l != lib_id)
|
||||
&& let Some(add) = swatch_add(&drag, &lib_id)
|
||||
{
|
||||
ui.painter().rect_stroke(zone.rect, 0.0, Stroke::new(1.5, t.accent), StrokeKind::Inside);
|
||||
let color = *color;
|
||||
if zone.dnd_release_payload::<PanelDrag>().is_some() {
|
||||
act = Some(Act::Run("library.add", json!({ "kind": "fillColor", "color": color })));
|
||||
act = Some(Act::Run("library.add", add));
|
||||
}
|
||||
}
|
||||
let bar_top = ui.cursor().top();
|
||||
@@ -311,6 +324,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
("Graphic", "graphic"),
|
||||
("Fill Color", "fillColor"),
|
||||
("Stroke Color", "strokeColor"),
|
||||
("Fill Pattern", "fillPattern"),
|
||||
("Stroke Pattern", "strokePattern"),
|
||||
("Fill Gradient", "fillGradient"),
|
||||
("Stroke Gradient", "strokeGradient"),
|
||||
("Character Style", "charStyle"),
|
||||
("Paragraph Style", "paraStyle"),
|
||||
] {
|
||||
@@ -386,6 +403,33 @@ impl Items<'_> {
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
let gradients: Vec<_> = lib.gradients.iter().filter(|g| keep(ItemKind::Gradient, &g.name, &g.name)).collect();
|
||||
if !gradients.is_empty() {
|
||||
heading(ui, tl!("Gradients"));
|
||||
self.cells(ui, |ui| {
|
||||
for g in gradients {
|
||||
let (r, resp) = self.cell(ui, CHIP, ROW);
|
||||
let chip = self.chip_rect(ui, r, &resp, &g.name);
|
||||
widgets::swatch_tile(ui, chip, &Paint::Gradient(Box::new(g.gradient.clone())), false, false);
|
||||
self.paint_item(ui, resp, ItemKind::Gradient, &g.name, act, over);
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
let patterns: Vec<_> = lib.patterns.iter().filter(|p| keep(ItemKind::Pattern, &p.name, &p.name)).collect();
|
||||
if !patterns.is_empty() {
|
||||
heading(ui, tl!("Patterns"));
|
||||
self.cells(ui, |ui| {
|
||||
for p in patterns {
|
||||
let (r, resp) = self.cell(ui, CHIP, ROW);
|
||||
let chip = self.chip_rect(ui, r, &resp, &p.name);
|
||||
png_tile(ui, chip, self.library, &format!("pattern:{}", p.name), &p.thumbnail);
|
||||
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
self.paint_item(ui, resp, ItemKind::Pattern, &p.name, act, over);
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
for (kind, list, title) in
|
||||
[(ItemKind::CharStyle, &lib.char_styles, tl!("Character Styles")), (ItemKind::ParaStyle, &lib.para_styles, tl!("Paragraph Styles"))]
|
||||
{
|
||||
@@ -465,6 +509,31 @@ impl Items<'_> {
|
||||
ui.allocate_exact_size(size, Sense::click_and_drag())
|
||||
}
|
||||
|
||||
/// Where a gradient's or pattern's chip goes in its cell `r`: the whole cell in the icon view,
|
||||
/// else a small chip at the left of its row, which shows `name`.
|
||||
fn chip_rect(&self, ui: &Ui, r: Rect, resp: &egui::Response, name: &str) -> Rect {
|
||||
if self.list {
|
||||
list_row(ui, r, resp, name, None);
|
||||
Rect::from_center_size(r.left_center() + vec2(10.0, 0.0), vec2(14.0, 14.0))
|
||||
} else {
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient's or pattern's cell `resp`: it drags to another place in the list, says its name
|
||||
/// when hovered in the icon view, paints the selection through the active Fill/Stroke proxy
|
||||
/// when clicked, and has the item menu.
|
||||
fn paint_item(&self, ui: &Ui, resp: egui::Response, kind: ItemKind, name: &str, act: &mut Option<Act>, over: &mut bool) {
|
||||
widgets::drag_source(ui, &resp, || PanelDrag::LibraryItem { library: self.library.to_string(), kind, item: name.to_string() });
|
||||
self.drop_next_to(ui, &resp, kind, name, self.list, act, over);
|
||||
let resp = if self.list { resp } else { resp.on_hover_text(name) };
|
||||
if resp.clicked() {
|
||||
let to = if self.fill_active { "fill" } else { "stroke" };
|
||||
*act = Some(Act::Run("library.use", json!({ "kind": kind_param(kind), "item": name, "to": to })));
|
||||
}
|
||||
resp.context_menu(|ui| self.item_menu(ui, act, kind, name));
|
||||
}
|
||||
|
||||
/// An item of `kind` dragged over item `key`'s cell `resp` lands just before or after it, by
|
||||
/// the pointer's half (`row`: rows stack downwards; else chips and tiles run across), in its
|
||||
/// group: an accent line shows where, and `over` is set. Dropped on itself it stays.
|
||||
@@ -541,6 +610,8 @@ fn kind_param(kind: ItemKind) -> &'static str {
|
||||
ItemKind::Graphic => "graphic",
|
||||
ItemKind::CharStyle => "charStyle",
|
||||
ItemKind::ParaStyle => "paraStyle",
|
||||
ItemKind::Gradient => "fillGradient",
|
||||
ItemKind::Pattern => "fillPattern",
|
||||
ItemKind::Color | ItemKind::Unknown => "fillColor",
|
||||
}
|
||||
}
|
||||
@@ -650,11 +721,16 @@ fn heading(ui: &mut Ui, text: &str) {
|
||||
|
||||
/// Graphic `g`'s thumbnail on white in `r`.
|
||||
fn tile(ui: &Ui, r: Rect, library: &str, g: &LibraryGraphic) {
|
||||
png_tile(ui, r, library, &format!("graphic:{}", g.id), &g.thumbnail);
|
||||
}
|
||||
|
||||
/// The PNG thumbnail `png` (base64) of item `key` (its kind and key) of `library` on white in `r`.
|
||||
fn png_tile(ui: &Ui, r: Rect, library: &str, key: &str, png: &str) {
|
||||
ui.painter().rect_filled(r, 0.0, egui::Color32::WHITE);
|
||||
// As many pixels as the tile shows (the art fits inside a 3-point margin).
|
||||
let px = (r.width().max(r.height()) - 6.0) * ui.ctx().pixels_per_point();
|
||||
let px = if px.is_finite() { px.ceil().clamp(8.0, 256.0) as u32 } else { 256 };
|
||||
if let Some(tex) = thumbnail(ui.ctx(), library, g, px) {
|
||||
if let Some(tex) = thumbnail(ui.ctx(), library, key, png, px) {
|
||||
let size = tex.size_vec2();
|
||||
let k = ((r.width() - 6.0) / size.x).min((r.height() - 6.0) / size.y).min(1.0);
|
||||
let fit = Rect::from_center_size(r.center(), size * k);
|
||||
@@ -662,27 +738,27 @@ fn tile(ui: &Ui, r: Rect, library: &str, g: &LibraryGraphic) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A thumbnail texture's library, graphic id, thumbnail length (for a changed graphic) and size.
|
||||
/// A thumbnail texture's library, item (its kind and key), thumbnail length (for a changed item)
|
||||
/// and size.
|
||||
type ThumbKey = (String, String, usize, u32);
|
||||
|
||||
/// The texture of graphic `g`'s PNG thumbnail at most `px` pixels a side, made once for each size.
|
||||
/// It is shrunk here, smoothly: the GPU draws it without mipmaps, so a texture much larger than its
|
||||
/// tile would break thin strokes up.
|
||||
fn thumbnail(ctx: &egui::Context, library: &str, g: &LibraryGraphic, px: u32) -> Option<egui::TextureHandle> {
|
||||
/// The texture of item `item`'s PNG thumbnail `png` (base64) at most `px` pixels a side, made once
|
||||
/// for each size. It is shrunk here, smoothly: the GPU draws it without mipmaps, so a texture much
|
||||
/// larger than its tile would break thin strokes up.
|
||||
fn thumbnail(ctx: &egui::Context, library: &str, item: &str, png: &str, px: u32) -> Option<egui::TextureHandle> {
|
||||
thread_local! {
|
||||
static CACHE: crate::graphics::TexCache<HashMap<ThumbKey, Option<egui::TextureHandle>>> = crate::graphics::TexCache::default();
|
||||
}
|
||||
let key = (library.to_string(), g.id.clone(), g.thumbnail.len(), px);
|
||||
let key = (library.to_string(), item.to_string(), png.len(), px);
|
||||
if let Some(t) = CACHE.with(|c| c.borrow().get(&key).cloned()) {
|
||||
return t;
|
||||
}
|
||||
let tex = vectorcraft_format::base64_decode(&g.thumbnail)
|
||||
.and_then(|png| image::load_from_memory_with_format(&png, image::ImageFormat::Png).ok())
|
||||
.map(|img| {
|
||||
let tex =
|
||||
vectorcraft_format::base64_decode(png).and_then(|png| image::load_from_memory_with_format(&png, image::ImageFormat::Png).ok()).map(|img| {
|
||||
let img = if img.width().max(img.height()) > px { img.resize(px, px, image::imageops::FilterType::Triangle) } else { img };
|
||||
let rgba = img.to_rgba8();
|
||||
let image = egui::ColorImage::from_rgba_unmultiplied([rgba.width() as usize, rgba.height() as usize], rgba.as_raw());
|
||||
ctx.load_texture(format!("library-{library}-{}-{px}", g.id), image, egui::TextureOptions::LINEAR)
|
||||
ctx.load_texture(format!("library-{library}-{item}-{px}"), image, egui::TextureOptions::LINEAR)
|
||||
});
|
||||
CACHE.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
|
||||
@@ -34,9 +34,10 @@ fn the_panel_draws_its_libraries() {
|
||||
assert_eq!((lib.graphics.len(), lib.colors.len(), lib.char_styles.len(), lib.para_styles.len()), (1, 1, 1, 1));
|
||||
// The thumbnail decodes.
|
||||
let ctx = egui::Context::default();
|
||||
assert!(thumbnail(&ctx, "Brand", &lib.graphics[0], 256).is_some());
|
||||
let g = &lib.graphics[0];
|
||||
assert!(thumbnail(&ctx, "Brand", &g.id, &g.thumbnail, 256).is_some());
|
||||
// Shrunk to the size it is drawn at.
|
||||
let small = thumbnail(&ctx, "Brand", &lib.graphics[0], 40).unwrap();
|
||||
let small = thumbnail(&ctx, "Brand", &g.id, &g.thumbnail, 40).unwrap();
|
||||
assert_eq!(small.size().into_iter().max(), Some(40), "{:?}", small.size());
|
||||
}
|
||||
|
||||
@@ -259,6 +260,56 @@ fn a_long_list_scrolls_also_while_dragging() {
|
||||
assert!(moved < first.top(), "the list scrolled down: {} → {moved}", first.top());
|
||||
}
|
||||
|
||||
/// Gradient and pattern swatches dropped on the list are added under their names and show under
|
||||
/// their headings; a click on one paints the selection through the active Fill/Stroke proxy.
|
||||
#[test]
|
||||
fn gradients_and_patterns_come_from_swatches_and_paint_the_selection() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
app.run("library.create", json!({"name": "Brand"})).unwrap();
|
||||
let sunset = json!({"stops": [{"offset": 0, "color": "#ffcc00"}, {"offset": 1, "color": "#cc3300"}]});
|
||||
app.run("swatch.new", json!({"name": "Sunset", "gradient": sunset})).unwrap();
|
||||
let dot = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 8, "height": 8})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({"ids": [dot]})).unwrap();
|
||||
app.run("object.pattern.make", json!({"name": "Checks", "width": 16, "height": 16})).unwrap();
|
||||
app.run("object.pattern.done", json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
crate::panels::set_pstate(&ctx, LIST_VIEW, true);
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
let into = at(&texts, "Brand").center() + vec2(0.0, 80.0);
|
||||
for name in ["Sunset", "Checks"] {
|
||||
let paint = app.session.active().unwrap().doc.swatch(name).unwrap().paint.clone();
|
||||
let drag = PanelDrag::Paint { paint, params: json!({"swatch": name}), rows: None, library: None };
|
||||
drop_at(&mut app, &ctx, drag, into);
|
||||
}
|
||||
let lib = app.session.libraries.get("Brand").unwrap();
|
||||
assert_eq!((lib.gradients.len(), lib.patterns.len()), (1, 1));
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
for s in ["Gradients", "Sunset", "Patterns", "Checks"] {
|
||||
at(&texts, s);
|
||||
}
|
||||
let id = app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 60, "height": 60})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let click = |pos| {
|
||||
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
[vec![egui::Event::PointerMoved(pos), button(true)], vec![button(false)]]
|
||||
};
|
||||
let fill = |app: &VectorcraftApp| {
|
||||
let d = &app.session.active().unwrap().doc;
|
||||
d.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().appearance.fill_paint()
|
||||
};
|
||||
for events in click(at(&texts, "Sunset").center()) {
|
||||
frame_at(&mut app, &ctx, 600.0, events);
|
||||
}
|
||||
assert!(matches!(fill(&app), Paint::Gradient(_)), "{:?}", fill(&app));
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
for events in click(at(&texts, "Checks").center()) {
|
||||
frame_at(&mut app, &ctx, 600.0, events);
|
||||
}
|
||||
assert!(matches!(fill(&app), Paint::Pattern { ref pattern, .. } if pattern == "Checks"), "{:?}", fill(&app));
|
||||
}
|
||||
|
||||
/// New Group… and Rename Group… ask for a name and act on the library and group they came
|
||||
/// from; the dialog shows only the name.
|
||||
#[test]
|
||||
|
||||
+22
-7
@@ -2024,7 +2024,8 @@ they answer its file's name without the extension and `count: 0`.
|
||||
|
||||
## Libraries
|
||||
|
||||
The Libraries panel's libraries hold graphics, colours and character and paragraph styles that any document can use.
|
||||
The Libraries panel's libraries hold graphics, colours, gradients, patterns and character and paragraph styles that any
|
||||
document can use.
|
||||
They are kept on this machine: each is a `.vclibrary` file (JSON) named after the library (`CON-.vclibrary` for a
|
||||
library named after a Windows device such as `CON`) in the `Libraries` folder next to the preferences
|
||||
(none in the web app and headless sessions, where they last for the session). `library.list` lists them (`id`, `name`,
|
||||
@@ -2034,7 +2035,7 @@ size and a PNG `thumbnail` in base64; colours with `name`, `hex` and `color`; st
|
||||
`library.setCurrent {library}` manage them; `library` is an id or a name, and without it the current library (the one
|
||||
the panel shows) is meant.
|
||||
|
||||
`library.add {library?, kind, ids?, name?, color?}` adds from the selection: `graphic` (a copy of the objects with the
|
||||
`library.add {library?, kind, ids?, name?, color?, swatch?}` adds from the selection: `graphic` (a copy of the objects with the
|
||||
images, symbols, patterns, swatches, styles and brushes they use, as Copy takes them), `fillColor` / `strokeColor` (the
|
||||
first selected object's solid colour, else the default one, or `color`), `charStyle` / `paraStyle` (the selected
|
||||
text's attributes). An item the library already has is reported as `existing`. `library.use {library?, kind, item,
|
||||
@@ -2044,6 +2045,18 @@ to the document under its name without the white space around it (numbered when
|
||||
attributes) and applied to the selected text, each as one undo step. `library.removeItem {library?, kind, item}` removes
|
||||
one.
|
||||
|
||||
Gradients and patterns are kinds of their own (the panel's + button lists Fill Gradient, Stroke Gradient, Fill Pattern
|
||||
and Stroke Pattern, and a gradient or pattern swatch dragged from the Swatches panel onto the list is added).
|
||||
`library.add {kind: "fillGradient" | "strokeGradient", swatch?}` adds the first selected object's fill or stroke
|
||||
gradient (else the default one, or the gradient swatch `swatch`) without its placement or swatch links, named after its
|
||||
swatch, else `Gradient`; `library.use` paints the selection's fill or stroke with it, fitted to each object.
|
||||
`library.add {kind: "fillPattern" | "strokePattern", swatch?}` adds the first selected object's fill or stroke pattern
|
||||
(else the default one, or the pattern swatch `swatch`) with the images, symbols, patterns, swatches, styles and brushes
|
||||
its art uses, and a thumbnail of one swatch's worth of it; `library.use` adds the pattern and its swatch to the document (the
|
||||
same pattern there is reused, and a name the document gives another pattern gets a number) and paints the selection's
|
||||
fill or stroke with it, as one undo step, and returns the pattern's name in the document as `pattern`. `library.get`
|
||||
lists them as `gradients: [{name, gradient}]` and `patterns: [{name, pattern, thumbnail}]`.
|
||||
|
||||
Items can be put in user-named groups, which the panel shows as collapsible sections after the ungrouped items (its
|
||||
search field filters every item by name; `library.get` lists them all to filter). `library.createGroup {library?,
|
||||
name?, items?: [{kind, item}]}` makes one (a name another group has, ignoring case, gets a number) with those items
|
||||
@@ -2053,15 +2066,17 @@ deletes one, its items staying in the library ungrouped; and `library.moveItem {
|
||||
of its kind), just before or after that item in the order its kind is listed and shown in. `library.moveGroup
|
||||
{library?, group, to}` moves a group to place `to` of the list (0: first; past the end: last). Dragging items and
|
||||
group headers in the panel runs these. An item is in one group at most; `library.get` lists the groups as `groups:
|
||||
[{name, items: [{kind: "color"|"charStyle"|"paraStyle"|"graphic", item}]}]` (a graphic by its id), and each kind's
|
||||
[{name, items: [{kind: "color"|"gradient"|"pattern"|"charStyle"|"paraStyle"|"graphic", item}]}]` (a graphic by its id), and each kind's
|
||||
items in the order the panel shows them.
|
||||
|
||||
`library.export {library?, path?}` writes a library (its colours, styles, graphics with their art and thumbnails, and
|
||||
its groups) as a `.vclibrary` file, the JSON the library folder keeps (without `path`: `{data}`, the file's text).
|
||||
`library.export {library?, path?}` writes a library (its colours, gradients, patterns, styles, graphics with their art
|
||||
and thumbnails, and its groups) as a `.vclibrary` file, the JSON the library folder keeps (without `path`: `{data}`, the file's text).
|
||||
`library.import {path? | data? | dataBase64?, name?}` adds the library of such a file as a new library and makes it
|
||||
the current one; a name another library has gets a number (`Brand (2)`). Imported files are checked: at most 10,000
|
||||
items of each kind and 1,000 groups, names clipped and made unique, thumbnails kept only when they are small PNGs, and
|
||||
groups naming items that aren't there dropped. Files written before groups load as they are.
|
||||
items of each kind and 1,000 groups, names clipped and made unique, thumbnails kept only when they are small PNGs,
|
||||
gradients with fewer than 2 or more than 1,024 stops dropped and the others' offsets, opacities and midpoints put in
|
||||
range, and groups naming items that aren't there dropped. Files written before groups load as they are. Using a
|
||||
pattern whose data is damaged fails with an error and leaves the document as it was.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"library.add","params":{"kind":"graphic","name":"Logo"}}}
|
||||
|
||||
@@ -929,6 +929,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one.
|
||||
- **Neutral wording:** labels, MCP tool text, docs and packaging use neutral names, and `cargo xtask brands` (part of `cargo xtask ci`) fails on vendor names.
|
||||
- **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize.
|
||||
The Libraries panel holds gradients and patterns: its + button adds the selection's fill or stroke gradient or pattern, a gradient or pattern swatch dragged from the Swatches panel is added, and a click paints the selection through the active Fill/Stroke proxy, a pattern bringing its definition and swatch into the document.
|
||||
Object › Lock › All Artwork Above and Object › Hide › All Artwork Above deselect the selected objects they lock or hide, the key object included.
|
||||
Object › Hide › Unselected Artwork (Cmd/Ctrl+Alt+Shift+3, `object.hide.unselected`) hides, in one undo step, every object Select All takes that is not selected and holds no selected object; the members of a group that holds a selected object are hidden one by one, and Object › Show All shows them again. It is disabled in isolation mode. Public references: [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html), "Hide unselected artwork"; [How to lock, hide, and delete objects in Illustrator](https://web.archive.org/web/20220303070637/https://helpx.adobe.com/illustrator/using/locking-hiding-deleting-objects.html), which says the Show All and Hide menus are unavailable in isolation mode.
|
||||
In isolation mode, the items of Object › Lock and Object › Hide, Unlock All and Show All are greyed out ([lock, hide, and delete objects](https://web.archive.org/web/20220303070637/https://helpx.adobe.com/illustrator/using/locking-hiding-deleting-objects.html)); the Contextual Task Bar's Lock button is greyed out too, the Properties panel's Quick Actions have no Lock, and the commands return an error. Pattern editing mode and opacity mask editing do not count as isolation mode for them.
|
||||
|
||||
Reference in New Issue
Block a user