mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 11:56:50 -06:00
Merge fix/575
This commit is contained in:
@@ -71,6 +71,7 @@ fn registry() -> &'static Registry {
|
||||
list.extend(create::specs());
|
||||
list.extend(edit::specs());
|
||||
list.extend(lists::specs());
|
||||
list.extend(crate::selection_colors::specs());
|
||||
list.extend(arrange::specs());
|
||||
list.extend(structure::specs());
|
||||
list.extend(section::specs());
|
||||
|
||||
@@ -43,6 +43,7 @@ pub mod plugins;
|
||||
pub mod proto_run;
|
||||
pub mod rename;
|
||||
pub mod rotation_origin;
|
||||
pub mod selection_colors;
|
||||
pub mod shape_handles;
|
||||
pub mod shapebuilder;
|
||||
pub mod snap;
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Paints used by the selection, including descendants and effective text-range fills.
|
||||
//! Targets name paint slots rather than colors: a live edit must not capture other slots when
|
||||
//! its color happens to become equal to another selection color.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use figcraft_color::{Paint, PaintKind};
|
||||
use figcraft_doc::{Document, NodeId, NodeKind, StyleKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::cmd::lists::{BoundList, list_binding};
|
||||
use crate::params::*;
|
||||
use crate::{CommandSpec, Result, Session, bad, spec};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct PaintTarget {
|
||||
pub id: NodeId,
|
||||
pub fills: bool,
|
||||
pub index: usize,
|
||||
/// Character offsets, not byte offsets. None means the node's fill or stroke list.
|
||||
pub range: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl PaintTarget {
|
||||
pub fn paints(&self, doc: &Document) -> Option<Vec<Paint>> {
|
||||
let n = doc.node(self.id)?;
|
||||
if let Some((start, end)) = self.range {
|
||||
let t = n.kind.text()?;
|
||||
if !self.fills || start > end || end > t.char_count() || (start == end && end != 0) {
|
||||
return None;
|
||||
}
|
||||
Some(t.style_at(start).fills.clone().unwrap_or_else(|| n.fills.clone()))
|
||||
} else {
|
||||
Some(if self.fills { n.fills.clone() } else { n.strokes.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint(&self, doc: &Document) -> Option<Paint> {
|
||||
self.paints(doc)?.get(self.index).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SelectionColor {
|
||||
pub paint: Paint,
|
||||
pub style: Option<String>,
|
||||
pub variable: Option<String>,
|
||||
pub targets: Vec<PaintTarget>,
|
||||
}
|
||||
|
||||
/// Deterministic order, one target per slot even when a parent and child are both selected.
|
||||
/// Images and patterns are assets rather than selection colors. Hidden paints are omitted.
|
||||
pub fn collect(doc: &Document, ids: &[NodeId]) -> Vec<SelectionColor> {
|
||||
let mut groups: Vec<SelectionColor> = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut by_paint: BTreeMap<(Option<String>, Option<String>, String), usize> = BTreeMap::new();
|
||||
let mut pending: Vec<_> = ids.iter().rev().copied().collect();
|
||||
while let Some(id) = pending.pop() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
let Some(n) = doc.node(id) else { continue };
|
||||
if n.is_mask {
|
||||
continue;
|
||||
}
|
||||
if !matches!(n.kind, NodeKind::BooleanOp { .. }) {
|
||||
pending.extend(n.children.iter().rev().copied());
|
||||
}
|
||||
let mut add = |paints: &[Paint], fills: bool, range: Option<(usize, usize)>| {
|
||||
for (index, paint) in paints.iter().enumerate() {
|
||||
if !paint.visible || !matches!(paint.kind, PaintKind::Solid { .. } | PaintKind::Gradient(_)) {
|
||||
continue;
|
||||
}
|
||||
let target = PaintTarget { id, fills, index, range };
|
||||
let inherited = range.is_none_or(|(start, _)| n.kind.text().is_some_and(|t| t.style_at(start).fills.is_none()));
|
||||
let style = inherited.then(|| if fills { n.styles.fill.clone() } else { n.styles.stroke.clone() }).flatten();
|
||||
let list = if fills { BoundList::Fills } else { BoundList::Strokes };
|
||||
let variable = inherited
|
||||
.then(|| {
|
||||
n.bound.iter().find_map(|(key, var)| {
|
||||
list_binding(key).filter(|(l, i, field)| *l == list && *i == index && *field == "color").map(|_| var.clone())
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
let Ok(value) = serde_json::to_string(paint) else { continue };
|
||||
let key = (style.clone(), variable.clone(), value);
|
||||
if let Some(index) = by_paint.get(&key).copied() {
|
||||
if let Some(g) = groups.get_mut(index) {
|
||||
g.targets.push(target);
|
||||
}
|
||||
} else {
|
||||
by_paint.insert(key, groups.len());
|
||||
groups.push(SelectionColor { paint: paint.clone(), style, variable, targets: vec![target] });
|
||||
}
|
||||
}
|
||||
};
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
let spans = t.spans();
|
||||
if spans.iter().all(|(_, _, style)| style.fills.is_none()) {
|
||||
add(&n.fills, true, None);
|
||||
} else {
|
||||
for (start, end, style) in spans {
|
||||
add(style.fills.as_deref().unwrap_or(&n.fills), true, Some((start, end)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
add(&n.fills, true, None);
|
||||
}
|
||||
add(&n.strokes, false, None);
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
spec!(
|
||||
"selection.colors",
|
||||
"Selection colors",
|
||||
"{ids?} → {colors: [{paint, targets}]}; includes descendants and text ranges",
|
||||
false,
|
||||
|s, p| { Ok(json!({"colors": collect(s.doc()?, &targets(s, p))})) }
|
||||
),
|
||||
spec!("selection.setPaint", "Change selection color", "{targets: [{id, fills, index, range: [start,end] | null}], paint}", true, set_paint),
|
||||
spec!(
|
||||
"selection.applyColorStyle",
|
||||
"Apply selection color style",
|
||||
"{targets, style}; applies the color style to matching fill/stroke lists (text ranges keep typography)",
|
||||
true,
|
||||
apply_style
|
||||
),
|
||||
spec!(
|
||||
"selection.selectColor",
|
||||
"Select layers with color",
|
||||
"{targets} → {ids}; selects the layers owning these paint slots",
|
||||
false,
|
||||
select_color
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn checked_targets(s: &Session, p: &Value, cmd: &str) -> Result<Vec<PaintTarget>> {
|
||||
let ts: Vec<PaintTarget> = parse(cmd, p, "targets")?.ok_or_else(|| bad(cmd, "`targets` is required"))?;
|
||||
if ts.is_empty() {
|
||||
return Err(bad(cmd, "`targets` must not be empty"));
|
||||
}
|
||||
for t in &ts {
|
||||
if t.paint(s.doc()?).is_none() {
|
||||
return Err(bad(cmd, "a selection color target no longer exists"));
|
||||
}
|
||||
}
|
||||
Ok(ts)
|
||||
}
|
||||
|
||||
/// Detach variables only where the explicit edit changed their value. In particular, an
|
||||
/// opacity edit keeps the color binding and a range edit keeps other characters' node bindings.
|
||||
fn unbind_changed(n: &mut figcraft_doc::Node, t: &PaintTarget, old: &Paint, new: &Paint) {
|
||||
// Range fills override the node's paints; the node binding still belongs to other chars.
|
||||
if t.range.is_some() {
|
||||
return;
|
||||
}
|
||||
let list = if t.fills { BoundList::Fills } else { BoundList::Strokes };
|
||||
n.bound.retain(|key, _| {
|
||||
let Some((l, index, field)) = list_binding(key) else { return true };
|
||||
if l != list || index != t.index {
|
||||
return true;
|
||||
}
|
||||
match field {
|
||||
"color" => match (&old.kind, &new.kind) {
|
||||
(PaintKind::Solid { color: a }, PaintKind::Solid { color: b }) => a == b,
|
||||
_ => false,
|
||||
},
|
||||
"opacity" => old.opacity == new.opacity,
|
||||
_ => {
|
||||
let stop = field.strip_prefix("stops/").and_then(|s| s.strip_suffix("/color")).and_then(|s| s.parse::<usize>().ok());
|
||||
match (stop, &old.kind, &new.kind) {
|
||||
(Some(i), PaintKind::Gradient(a), PaintKind::Gradient(b)) => a.stops.get(i).map(|s| s.color) == b.stops.get(i).map(|s| s.color),
|
||||
(Some(_), _, _) => false,
|
||||
_ => old == new,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_paint(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const CMD: &str = "selection.setPaint";
|
||||
let targets = checked_targets(s, p, CMD)?;
|
||||
let paint = get(p, "paint").and_then(paint_of).ok_or_else(|| bad(CMD, "`paint` must be a paint"))?;
|
||||
for target in &targets {
|
||||
let mut paints = target.paints(s.doc()?).ok_or_else(|| bad(CMD, "invalid target"))?;
|
||||
let Some(slot) = paints.get_mut(target.index) else { return Err(bad(CMD, "invalid paint index")) };
|
||||
if *slot == paint {
|
||||
continue;
|
||||
}
|
||||
let old = slot.clone();
|
||||
*slot = paint.clone();
|
||||
let n = s.doc_mut()?.get_mut(target.id)?;
|
||||
unbind_changed(n, target, &old, &paint);
|
||||
if let Some((start, end)) = target.range {
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
t.restyle(start, end, |style| style.fills = Some(paints.clone()));
|
||||
}
|
||||
} else if target.fills {
|
||||
n.fills = paints;
|
||||
n.styles.fill = None;
|
||||
} else {
|
||||
n.strokes = paints;
|
||||
n.styles.stroke = None;
|
||||
}
|
||||
}
|
||||
Ok(json!({"ok": true}))
|
||||
}
|
||||
|
||||
fn apply_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const CMD: &str = "selection.applyColorStyle";
|
||||
let targets = checked_targets(s, p, CMD)?;
|
||||
let name = req_string(CMD, p, "style")?;
|
||||
let style = s.doc()?.style(&name).cloned().ok_or_else(|| bad(CMD, "no such color style"))?;
|
||||
if !matches!(style.kind, StyleKind::Paint { .. }) {
|
||||
return Err(bad(CMD, "a color style is required"));
|
||||
}
|
||||
for target in &targets {
|
||||
if let Some((start, end)) = target.range {
|
||||
// Text ranges have typography-style links, not paint-style links. Copy resolved paint
|
||||
// values just as an explicit range fill does; never replace its font or size.
|
||||
let StyleKind::Paint { paints } = crate::style_vars::kind_for(s.doc()?, &style, target.id).into_owned() else { continue };
|
||||
let n = s.doc_mut()?.get_mut(target.id)?;
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
t.restyle(start, end, |st| st.fills = Some(paints.clone()));
|
||||
}
|
||||
} else {
|
||||
crate::cmd::styles::apply_style_ref(s.doc_mut()?, target.id, if target.fills { "fillStyle" } else { "strokeStyle" }, Some(&style.id))?;
|
||||
}
|
||||
}
|
||||
Ok(json!({"ok": true}))
|
||||
}
|
||||
|
||||
fn select_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ts = checked_targets(s, p, "selection.selectColor")?;
|
||||
let ids: Vec<_> = ts.iter().map(|t| t.id).collect::<BTreeSet<_>>().into_iter().collect();
|
||||
crate::cmd::edit::select_and_page(s, ids.clone())?;
|
||||
Ok(json!({"ids": ids}))
|
||||
}
|
||||
@@ -415,6 +415,9 @@ fn every_command_survives_junk_params() {
|
||||
// Online files: ids, servers, refreshes, actions and names as junk.
|
||||
json!({"file": [1], "server": 7, "refresh": "maybe", "action": "share", "name": {"x": 1}}),
|
||||
json!({"file": "../x", "server": "wss://\u{0}", "refresh": true, "action": "unshare", "name": 5}),
|
||||
// Selection color slots: malformed indexes, ranges and paints.
|
||||
json!({"targets": [{"id": 2, "fills": true, "index": -1, "range": [0, 1e308]}], "paint": {"type": "solid", "color": "invalid"}}),
|
||||
json!({"targets": [{"id": 2, "fills": "yes", "index": 0, "range": [4, 1]}], "style": 5}),
|
||||
// Rotation origins and placing with a tool as junk.
|
||||
json!({"tool": [1], "x": 1e308, "y": "NaN", "reset": "maybe", "on": {"x": 1}}),
|
||||
json!({"tool": "frame", "x": -1e308, "y": 1e308, "reset": true, "on": "yes"}),
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use figcraft_color::Paint;
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_engine::{
|
||||
Session,
|
||||
selection_colors::{SelectionColor, collect},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
|
||||
s.execute(cmd, &p).unwrap()
|
||||
}
|
||||
fn rectangle(s: &mut Session, color: &str) -> NodeId {
|
||||
NodeId(run(s, "shape.rectangle", json!({"width":40,"height":40,"props":{"fill":color}}))["id"].as_u64().unwrap())
|
||||
}
|
||||
fn group(s: &Session, ids: &[NodeId], hex: &str) -> SelectionColor {
|
||||
collect(s.doc().unwrap(), ids).into_iter().find(|g| g.paint.solid_color().is_some_and(|c| c.to_hex() == hex)).unwrap()
|
||||
}
|
||||
fn fill(s: &Session, id: NodeId) -> String {
|
||||
s.doc().unwrap().node(id).unwrap().fills[0].solid_color().unwrap().to_hex()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregate_descendants_deduplicate_roots_and_ignore_images_and_hidden_paints() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#00ff00");
|
||||
run(&mut s, "stroke.add", json!({"id":b,"color":"#ff0000"}));
|
||||
let parent = NodeId(run(&mut s, "object.group", json!({"ids":[a,b]}))["id"].as_u64().unwrap());
|
||||
let colors = collect(s.doc().unwrap(), &[parent, a]);
|
||||
assert_eq!(colors.len(), 2);
|
||||
assert_eq!(group(&s, &[parent, a], "FF0000").targets.len(), 2);
|
||||
run(&mut s, "fill.add", json!({"id":a,"paint":Paint::image("synthetic",figcraft_color::ImageScaleMode::Fill)}));
|
||||
run(&mut s, "fill.add", json!({"id":a,"paint":{"type":"solid","color":"#0000ff","visible":false}}));
|
||||
assert_eq!(collect(s.doc().unwrap(), &[parent]).len(), 2);
|
||||
run(&mut s, "select.set", json!({"ids":[parent]}));
|
||||
assert_eq!(run(&mut s, "selection.colors", json!({}))["colors"].as_array().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_only_matching_slots_is_one_undo_step_and_keeps_selection() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#0000ff");
|
||||
let c = rectangle(&mut s, "#00ff00");
|
||||
run(&mut s, "stroke.add", json!({"id":b,"color":"#ff0000"}));
|
||||
run(&mut s, "select.set", json!({"ids":[a,b]}));
|
||||
let red = group(&s, &[a, b], "FF0000");
|
||||
let before = s.undo_depth();
|
||||
run(&mut s, "selection.setPaint", json!({"targets":red.targets,"paint":"#ffff00"}));
|
||||
assert_eq!(fill(&s, a), "FFFF00");
|
||||
assert_eq!(fill(&s, b), "0000FF");
|
||||
assert_eq!(fill(&s, c), "00FF00");
|
||||
assert_eq!(s.doc().unwrap().node(b).unwrap().strokes[0].solid_color().unwrap().to_hex(), "FFFF00");
|
||||
assert_eq!(s.selection(), vec![a, b]);
|
||||
assert_eq!(s.undo_depth().0, before.0 + 1);
|
||||
s.undo().unwrap();
|
||||
assert_eq!(fill(&s, a), "FF0000");
|
||||
s.redo().unwrap();
|
||||
assert_eq!(fill(&s, a), "FFFF00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_picker_targets_do_not_expand_when_colors_merge() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#0000ff");
|
||||
let red = group(&s, &[a, b], "FF0000");
|
||||
let before = s.undo_depth();
|
||||
s.begin_interaction("Color").unwrap();
|
||||
for hex in ["#0000ff", "#00ff00"] {
|
||||
run(&mut s, "selection.setPaint", json!({"targets":red.targets,"paint":hex}));
|
||||
}
|
||||
s.commit_interaction().unwrap();
|
||||
assert_eq!(fill(&s, a), "00FF00");
|
||||
assert_eq!(fill(&s, b), "0000FF");
|
||||
assert_eq!(s.undo_depth().0, before.0 + 1);
|
||||
s.undo().unwrap();
|
||||
assert_eq!(fill(&s, a), "FF0000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_range_colors_use_character_offsets_and_preserve_typography() {
|
||||
let mut s = Session::new();
|
||||
let id = NodeId(run(&mut s, "text.create", json!({"text":"Яab","props":{"fill":"#ff0000"}}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "text.style", json!({"id":id,"start":1,"end":2,"fontSize":30,"textFills":["#0000ff"]}));
|
||||
let blue = group(&s, &[id], "0000FF");
|
||||
assert_eq!(blue.targets[0].range, Some((1, 2)));
|
||||
run(&mut s, "selection.setPaint", json!({"targets":blue.targets,"paint":"#00ff00"}));
|
||||
let n = s.doc().unwrap().node(id).unwrap();
|
||||
let t = n.kind.text().unwrap();
|
||||
assert_eq!(t.characters, "Яab");
|
||||
assert_eq!(t.style_at(1).font_size, 30.0);
|
||||
assert_eq!(t.style_at(1).fills.as_ref().unwrap()[0].solid_color().unwrap().to_hex(), "00FF00");
|
||||
assert_eq!(t.style_at(0).fills.as_deref().unwrap_or(&n.fills)[0].solid_color().unwrap().to_hex(), "FF0000");
|
||||
assert_eq!(t.style_at(2).fills.as_deref().unwrap_or(&n.fills)[0].solid_color().unwrap().to_hex(), "FF0000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selecting_color_selects_its_unique_owners_without_document_history() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#0000ff");
|
||||
run(&mut s, "stroke.add", json!({"id":a,"color":"#ff0000"}));
|
||||
let red = group(&s, &[a, b], "FF0000");
|
||||
let before = s.undo_depth();
|
||||
run(&mut s, "selection.selectColor", json!({"targets":red.targets}));
|
||||
assert_eq!(s.selection(), vec![a]);
|
||||
assert_eq!(s.undo_depth(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applying_color_styles_links_matching_fill_and_stroke_lists_only() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#0000ff");
|
||||
run(&mut s, "stroke.add", json!({"id":b,"color":"#ff0000"}));
|
||||
let red = group(&s, &[a, b], "FF0000");
|
||||
let style = run(&mut s, "style.create", json!({"type":"paint","name":"Brand","value":"#00ff00"}))["id"].clone();
|
||||
let before = s.undo_depth();
|
||||
run(&mut s, "selection.applyColorStyle", json!({"targets":red.targets,"style":style}));
|
||||
assert_eq!(fill(&s, a), "00FF00");
|
||||
assert_eq!(fill(&s, b), "0000FF");
|
||||
assert_eq!(s.doc().unwrap().node(a).unwrap().styles.fill.as_deref(), style.as_str());
|
||||
assert_eq!(s.doc().unwrap().node(b).unwrap().styles.stroke.as_deref(), style.as_str());
|
||||
assert_eq!(s.undo_depth().0, before.0 + 1);
|
||||
run(&mut s, "style.update", json!({"style":style,"value":"#ffff00"}));
|
||||
assert_eq!(fill(&s, a), "FFFF00");
|
||||
assert_eq!(fill(&s, b), "0000FF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_color_detaches_its_binding_but_keeps_other_bindings() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
run(&mut s, "variable.createCollection", json!({"name":"Theme"}));
|
||||
let v = run(&mut s, "variable.create", json!({"collection":"Theme","name":"Brand","type":"color","value":"#ff0000"}))["id"].clone();
|
||||
run(&mut s, "variable.bind", json!({"id":a,"field":"fill","variable":v}));
|
||||
let red = group(&s, &[a], "FF0000");
|
||||
run(&mut s, "selection.setPaint", json!({"targets":red.targets,"paint":"#0000ff"}));
|
||||
assert_eq!(fill(&s, a), "0000FF");
|
||||
assert!(s.doc().unwrap().node(a).unwrap().bound.is_empty());
|
||||
s.undo().unwrap();
|
||||
assert_eq!(fill(&s, a), "FF0000");
|
||||
assert!(!s.doc().unwrap().node(a).unwrap().bound.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_or_stale_targets_roll_back_atomically() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let mut red = group(&s, &[a], "FF0000");
|
||||
let mut bad = red.targets[0].clone();
|
||||
bad.index = 999;
|
||||
red.targets.push(bad);
|
||||
let before = s.undo_depth();
|
||||
assert!(s.execute("selection.setPaint", &json!({"targets":red.targets,"paint":"#0000ff"})).is_err());
|
||||
assert_eq!(fill(&s, a), "FF0000");
|
||||
assert_eq!(s.undo_depth(), before);
|
||||
for command in ["selection.setPaint", "selection.selectColor", "selection.applyColorStyle"] {
|
||||
for params in [json!({}), json!({"targets":[]}), json!({"targets":[{"id":a,"fills":true,"index":0,"range":[0,1]}]})] {
|
||||
assert!(s.execute(command, ¶ms).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_resolved_colors_from_different_styles_or_variables_stay_separate() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#ff0000");
|
||||
let c = rectangle(&mut s, "#ff0000");
|
||||
let style = run(&mut s, "style.create", json!({"type":"paint","name":"Brand","value":"#ff0000"}))["id"].clone();
|
||||
run(&mut s, "style.apply", json!({"id":a,"style":style,"target":"fill"}));
|
||||
run(&mut s, "variable.createCollection", json!({"name":"Theme"}));
|
||||
let variable = run(&mut s, "variable.create", json!({"collection":"Theme","name":"Accent","type":"color","value":"#ff0000"}))["id"].clone();
|
||||
run(&mut s, "variable.bind", json!({"id":b,"field":"fill","variable":variable}));
|
||||
let colors = collect(s.doc().unwrap(), &[a, b, c]);
|
||||
assert_eq!(colors.len(), 3);
|
||||
assert_eq!(colors[0].style.as_deref(), style.as_str());
|
||||
assert_eq!(colors[1].variable.as_deref(), variable.as_str());
|
||||
assert!(colors[2].style.is_none() && colors[2].variable.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradients_and_multiple_paints_are_edited_without_replacing_other_slots() {
|
||||
use figcraft_color::{Color, Gradient, GradientKind};
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let paint = Paint::gradient(Gradient::default_for(GradientKind::Linear, Color::BLACK));
|
||||
run(&mut s, "fill.add", json!({"id":a,"paint":paint}));
|
||||
let colors = collect(s.doc().unwrap(), &[a]);
|
||||
assert_eq!(colors.len(), 2);
|
||||
let gradient = colors.iter().find(|g| g.paint == paint).unwrap();
|
||||
run(&mut s, "selection.setPaint", json!({"targets":gradient.targets,"paint":"#00ff00"}));
|
||||
let paints = &s.doc().unwrap().node(a).unwrap().fills;
|
||||
assert_eq!(paints.len(), 2);
|
||||
assert_eq!(paints[0].solid_color().unwrap().to_hex(), "FF0000");
|
||||
assert_eq!(paints[1].solid_color().unwrap().to_hex(), "00FF00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masks_and_boolean_operands_are_not_selection_colors() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#00ff00");
|
||||
let boolean = NodeId(run(&mut s, "bool.union", json!({"ids":[a,b]}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "fill.set", json!({"id":boolean,"color":"#0000ff"}));
|
||||
let colors = collect(s.doc().unwrap(), &[boolean]);
|
||||
assert_eq!(colors.len(), 1);
|
||||
assert_eq!(colors[0].paint.solid_color().unwrap().to_hex(), "0000FF");
|
||||
s.doc_mut().unwrap().get_mut(boolean).unwrap().is_mask = true;
|
||||
assert!(collect(s.doc().unwrap(), &[boolean]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_edit_keeps_a_node_binding_that_other_characters_inherit() {
|
||||
let mut s = Session::new();
|
||||
let id = NodeId(run(&mut s, "text.create", json!({"text":"abc","props":{"fill":"#ff0000"}}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "variable.createCollection", json!({"name":"Theme"}));
|
||||
let v = run(&mut s, "variable.create", json!({"collection":"Theme","name":"Brand","type":"color","value":"#ff0000"}))["id"].clone();
|
||||
run(&mut s, "variable.bind", json!({"id":id,"field":"fill","variable":v}));
|
||||
run(&mut s, "text.style", json!({"id":id,"start":1,"end":2,"textFills":["#0000ff"]}));
|
||||
let blue = group(&s, &[id], "0000FF");
|
||||
run(&mut s, "selection.setPaint", json!({"targets":blue.targets,"paint":"#00ff00"}));
|
||||
assert!(!s.doc().unwrap().node(id).unwrap().bound.is_empty());
|
||||
assert_eq!(fill(&s, id), "FF0000");
|
||||
assert_eq!(
|
||||
s.doc().unwrap().node(id).unwrap().kind.text().unwrap().style_at(1).fills.as_ref().unwrap()[0].solid_color().unwrap().to_hex(),
|
||||
"00FF00"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_color_edits_respect_read_only_version_previews() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
run(&mut s, "file.saveVersion", json!({"name":"Before"}));
|
||||
run(&mut s, "version.preview", json!({"index":0}));
|
||||
let red = group(&s, &[a], "FF0000");
|
||||
let before = s.undo_depth();
|
||||
assert!(s.execute("selection.setPaint", &json!({"targets":red.targets,"paint":"#0000ff"})).is_err());
|
||||
assert_eq!(fill(&s, a), "FF0000");
|
||||
assert_eq!(s.undo_depth(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opacity_edits_keep_the_color_variable_binding() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
run(&mut s, "variable.createCollection", json!({"name":"Theme"}));
|
||||
let v = run(&mut s, "variable.create", json!({"collection":"Theme","name":"Brand","type":"color","value":"#ff0000"}))["id"].clone();
|
||||
run(&mut s, "variable.bind", json!({"id":a,"field":"fill","variable":v}));
|
||||
let mut red = group(&s, &[a], "FF0000");
|
||||
red.paint.opacity = 0.5;
|
||||
run(&mut s, "selection.setPaint", json!({"targets":red.targets,"paint":red.paint}));
|
||||
let n = s.doc().unwrap().node(a).unwrap();
|
||||
assert_eq!(n.fills[0].opacity, 0.5);
|
||||
assert!(!n.bound.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whole_text_fill_keeps_a_color_style_link() {
|
||||
let mut s = Session::new();
|
||||
let id = NodeId(run(&mut s, "text.create", json!({"text":"Text","props":{"fill":"#ff0000"}}))["id"].as_u64().unwrap());
|
||||
let red = group(&s, &[id], "FF0000");
|
||||
assert_eq!(red.targets[0].range, None);
|
||||
let style = run(&mut s, "style.create", json!({"type":"paint","name":"Brand","value":"#0000ff"}))["id"].clone();
|
||||
run(&mut s, "selection.applyColorStyle", json!({"targets":red.targets,"style":style}));
|
||||
assert_eq!(s.doc().unwrap().node(id).unwrap().styles.fill.as_deref(), style.as_str());
|
||||
run(&mut s, "style.update", json!({"style":style,"value":"#00ff00"}));
|
||||
assert_eq!(fill(&s, id), "00FF00");
|
||||
}
|
||||
@@ -638,6 +638,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Návrh rozhraní a prototypování. Svobodný nástroj s otevřeným zdrojovým kódem, napsaný od základu v jazyce Rust nezávisle na kódu jiných produktů. Součást sady ArtCraft Crafting Apps."
|
||||
"Intersect" = "Průnik"
|
||||
"Intersects" = "Průniky"
|
||||
"Invalid color" = "Neplatná barva"
|
||||
"Invite link" = "Odkaz s pozvánkou"
|
||||
"Invite link copied" = "Odkaz s pozvánkou zkopírován"
|
||||
"It asks permission to:" = "Žádá o oprávnění:"
|
||||
@@ -804,6 +805,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Žádné větve. Větev je kopie souboru pro vyzkoušení změn, které pak zkontrolujete a sloučíte."
|
||||
"No category" = "Bez kategorie"
|
||||
"No changes" = "Žádné změny"
|
||||
"No color styles" = "Žádné barevné styly"
|
||||
"No colour variables fit here." = "Žádné vhodné barevné proměnné."
|
||||
"No colour variables yet. Create them in Local variables." = "Zatím žádné barevné proměnné. Vytvořte je v Místních proměnných."
|
||||
"No comments yet" = "Zatím žádné komentáře"
|
||||
@@ -1161,12 +1163,14 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "Vybrat vše se stejným"
|
||||
"Select layer" = "Vybrat vrstvu"
|
||||
"Select layers to apply the style to" = "Vyberte vrstvy, na které chcete použít styl"
|
||||
"Select layers with this color" = "Vybrat vrstvy s touto barvou"
|
||||
"Select overlapping shapes" = "Vyberte překrývající se tvary"
|
||||
"Select points to edit their position, radius and mirroring" = "Vyberte body a upravte jejich polohu, poloměr a zrcadlení"
|
||||
"Select slot" = "Vybrat slot"
|
||||
"Selection" = "Výběr"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Rámeček výběru, {w} × {h} na pozici {x}, {y}: šipky posouvají, Shift+šipky nebo {cmd}+šipky mění velikost, Enter vybere, Escape zruší"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Rámeček výběru: šipky posouvají, Shift+šipky nebo {cmd}+šipky mění velikost, Enter vybere, Escape zruší"
|
||||
"Selection colors" = "Barvy výběru"
|
||||
"Send backward" = "Přesunout o úroveň níž"
|
||||
"Send report" = "Odeslat hlášení"
|
||||
"Send to back" = "Přesunout do pozadí"
|
||||
@@ -1992,6 +1996,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "Vybrat stejný tah"
|
||||
"select.sameText" = "Vybrat stejné vlastnosti textu"
|
||||
"select.set" = "Vybrat"
|
||||
"selection.applyColorStyle" = "Použít barevný styl na výběr"
|
||||
"selection.colors" = "Barvy výběru"
|
||||
"selection.selectColor" = "Vybrat vrstvy s touto barvou"
|
||||
"selection.setPaint" = "Změnit barvu výběru"
|
||||
"shape.arrow" = "Šipka"
|
||||
"shape.ellipse" = "Elipsa"
|
||||
"shape.line" = "Čára"
|
||||
|
||||
@@ -623,6 +623,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Interface-Design und Prototyping. Ein kostenloses Open-Source-Designwerkzeug, als Clean-Room-Entwicklung von Grund auf in Rust geschrieben, Teil der ArtCraft Crafting Apps."
|
||||
"Intersect" = "Schnittmenge"
|
||||
"Intersects" = "Schnittmengen"
|
||||
"Invalid color" = "Ungültige Farbe"
|
||||
"Invite link" = "Einladungslink"
|
||||
"Invite link copied" = "Einladungslink kopiert"
|
||||
"It asks permission to:" = "Es möchte:"
|
||||
@@ -777,6 +778,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Keine Branches. Ein Branch ist eine Kopie der Datei, in der Sie Änderungen ausprobieren, prüfen und dann zusammenführen."
|
||||
"No category" = "Keine Kategorie"
|
||||
"No changes" = "Keine Änderungen"
|
||||
"No color styles" = "Keine Farbstile"
|
||||
"No colour variables fit here." = "Hier passen keine Farbvariablen."
|
||||
"No colour variables yet. Create them in Local variables." = "Noch keine Farbvariablen. Erstellen Sie sie unter Lokale Variablen."
|
||||
"No comments yet" = "Noch keine Kommentare"
|
||||
@@ -1125,12 +1127,14 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "Alle auswählen mit gleicher"
|
||||
"Select layer" = "Ebene auswählen"
|
||||
"Select layers to apply the style to" = "Wählen Sie die Ebenen aus, auf die der Stil angewendet werden soll"
|
||||
"Select layers with this color" = "Ebenen mit dieser Farbe auswählen"
|
||||
"Select overlapping shapes" = "Wählen Sie überlappende Formen aus"
|
||||
"Select points to edit their position, radius and mirroring" = "Wählen Sie Punkte aus, um Position, Radius und Spiegelung zu bearbeiten"
|
||||
"Select slot" = "Slot auswählen"
|
||||
"Selection" = "Auswahl"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Auswahlrahmen, {w} × {h} bei {x}, {y}: Pfeiltasten verschieben ihn, Umschalt+Pfeiltasten oder {cmd}+Pfeiltasten ändern die Größe, Eingabe wählt aus, Esc bricht ab"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Auswahlrahmen: Pfeiltasten verschieben ihn, Umschalt+Pfeiltasten oder {cmd}+Pfeiltasten ändern die Größe, Eingabe wählt aus, Esc bricht ab"
|
||||
"Selection colors" = "Auswahlfarben"
|
||||
"Send backward" = "Eine Ebene nach hinten"
|
||||
"Send report" = "Meldung senden"
|
||||
"Send to back" = "In den Hintergrund"
|
||||
@@ -1935,6 +1939,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "Gleiche Kontur auswählen"
|
||||
"select.sameText" = "Gleiche Texteigenschaften auswählen"
|
||||
"select.set" = "Auswählen"
|
||||
"selection.applyColorStyle" = "Farbstil auf Auswahl anwenden"
|
||||
"selection.colors" = "Auswahlfarben"
|
||||
"selection.selectColor" = "Ebenen mit dieser Farbe auswählen"
|
||||
"selection.setPaint" = "Farbe der Auswahl ändern"
|
||||
"shape.arrow" = "Pfeil"
|
||||
"shape.ellipse" = "Ellipse"
|
||||
"shape.line" = "Linie"
|
||||
|
||||
@@ -651,6 +651,7 @@ name = "English"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps."
|
||||
"Intersect" = "Intersect"
|
||||
"Intersects" = "Intersects"
|
||||
"Invalid color" = "Invalid color"
|
||||
"Invite link" = "Invite link"
|
||||
"Invite link copied" = "Invite link copied"
|
||||
"It asks permission to:" = "It asks permission to:"
|
||||
@@ -817,6 +818,7 @@ name = "English"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "No branches. A branch is a copy of the file to try changes in, then review and merge."
|
||||
"No category" = "No category"
|
||||
"No changes" = "No changes"
|
||||
"No color styles" = "No color styles"
|
||||
"No colour variables fit here." = "No colour variables fit here."
|
||||
"No colour variables yet. Create them in Local variables." = "No colour variables yet. Create them in Local variables."
|
||||
"No comments yet" = "No comments yet"
|
||||
@@ -1182,9 +1184,11 @@ name = "English"
|
||||
"Select all with same" = "Select all with same"
|
||||
"Select layer" = "Select layer"
|
||||
"Select layers to apply the style to" = "Select layers to apply the style to"
|
||||
"Select layers with this color" = "Select layers with this color"
|
||||
"Select overlapping shapes" = "Select overlapping shapes"
|
||||
"Select points to edit their position, radius and mirroring" = "Select points to edit their position, radius and mirroring"
|
||||
"Select slot" = "Select slot"
|
||||
"Selection colors" = "Selection colors"
|
||||
"Selection" = "Selection"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels"
|
||||
|
||||
@@ -652,6 +652,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Diseño de interfaces y prototipos. Una herramienta de diseño gratuita y de código abierto, de implementación independiente (clean-room), escrita desde cero en Rust, parte de las Crafting Apps de ArtCraft."
|
||||
"Intersect" = "Intersecar"
|
||||
"Intersects" = "Intersecciones"
|
||||
"Invalid color" = "Color no válido"
|
||||
"Invite link" = "Enlace de invitación"
|
||||
"Invite link copied" = "Enlace de invitación copiado"
|
||||
"It asks permission to:" = "Pide permiso para:"
|
||||
@@ -818,6 +819,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "No hay ramas. Una rama es una copia del archivo para probar cambios y luego revisarlos y fusionarlos."
|
||||
"No category" = "Sin categoría"
|
||||
"No changes" = "Sin cambios"
|
||||
"No color styles" = "No hay estilos de color"
|
||||
"No colour variables fit here." = "Ninguna variable de color encaja aquí."
|
||||
"No colour variables yet. Create them in Local variables." = "Aún no hay variables de color. Créalas en Variables locales."
|
||||
"No comments yet" = "Aún no hay comentarios"
|
||||
@@ -1183,9 +1185,11 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "Seleccionar todo con el mismo"
|
||||
"Select layer" = "Seleccionar capa"
|
||||
"Select layers to apply the style to" = "Selecciona capas para aplicarles el estilo"
|
||||
"Select layers with this color" = "Seleccionar capas con este color"
|
||||
"Select overlapping shapes" = "Selecciona formas superpuestas"
|
||||
"Select points to edit their position, radius and mirroring" = "Selecciona puntos para editar su posición, radio y reflejo"
|
||||
"Select slot" = "Seleccionar ranura"
|
||||
"Selection colors" = "Colores de la selección"
|
||||
"Selection" = "Selección"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Cuadro de selección, {w} por {h} en {x}, {y}: las flechas lo mueven, Mayús+flechas o {cmd}+flechas cambian su tamaño, Intro selecciona, Esc cancela"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Cuadro de selección: las flechas lo mueven, Mayús+flechas o {cmd}+flechas cambian su tamaño, Intro selecciona, Esc cancela"
|
||||
@@ -2024,6 +2028,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "Seleccionar mismo trazo"
|
||||
"select.sameText" = "Seleccionar mismas propiedades de texto"
|
||||
"select.set" = "Seleccionar"
|
||||
"selection.applyColorStyle" = "Aplicar estilo de color a la selección"
|
||||
"selection.colors" = "Colores de la selección"
|
||||
"selection.selectColor" = "Seleccionar capas con este color"
|
||||
"selection.setPaint" = "Cambiar color de la selección"
|
||||
"shape.arrow" = "Flecha"
|
||||
"shape.ellipse" = "Elipse"
|
||||
"shape.line" = "Línea"
|
||||
|
||||
@@ -619,6 +619,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Design d’interfaces et prototypage. Un outil de design libre et gratuit, développé en salle blanche (clean-room) et écrit de zéro en Rust, membre des Crafting Apps d’ArtCraft."
|
||||
"Intersect" = "Intersection"
|
||||
"Intersects" = "Intersections"
|
||||
"Invalid color" = "Couleur non valide"
|
||||
"Invite link" = "Lien d’invitation"
|
||||
"Invite link copied" = "Lien d’invitation copié"
|
||||
"It asks permission to:" = "Il demande l’autorisation de :"
|
||||
@@ -773,6 +774,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Aucune branche. Une branche est une copie du fichier pour essayer des modifications, puis les relire et les fusionner."
|
||||
"No category" = "Sans catégorie"
|
||||
"No changes" = "Aucune modification"
|
||||
"No color styles" = "Aucun style de couleur"
|
||||
"No colour variables fit here." = "Aucune variable de couleur ne convient ici."
|
||||
"No colour variables yet. Create them in Local variables." = "Aucune variable de couleur pour l’instant. Créez-en dans Variables locales."
|
||||
"No comments yet" = "Aucun commentaire pour l’instant"
|
||||
@@ -1119,12 +1121,14 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "Sélectionner tout ce qui a le même"
|
||||
"Select layer" = "Sélectionner le calque"
|
||||
"Select layers to apply the style to" = "Sélectionnez les calques auxquels appliquer le style"
|
||||
"Select layers with this color" = "Sélectionner les calques de cette couleur"
|
||||
"Select overlapping shapes" = "Sélectionnez des formes qui se chevauchent"
|
||||
"Select points to edit their position, radius and mirroring" = "Sélectionnez des points pour modifier leur position, leur rayon et leur symétrie"
|
||||
"Select slot" = "Sélectionner l’emplacement"
|
||||
"Selection" = "Sélection"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Zone de sélection, {w} sur {h} à {x}, {y} : les flèches la déplacent, Maj+flèches ou {cmd}+flèches la redimensionnent, Entrée sélectionne, Échap annule"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Zone de sélection : les flèches la déplacent, Maj+flèches ou {cmd}+flèches la redimensionnent, Entrée sélectionne, Échap annule"
|
||||
"Selection colors" = "Couleurs de la sélection"
|
||||
"Send backward" = "Reculer"
|
||||
"Send report" = "Envoyer le signalement"
|
||||
"Send to back" = "Mettre à l’arrière-plan"
|
||||
@@ -1926,6 +1930,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "Sélectionner le même contour"
|
||||
"select.sameText" = "Sélectionner les mêmes propriétés de texte"
|
||||
"select.set" = "Sélectionner"
|
||||
"selection.applyColorStyle" = "Appliquer un style de couleur à la sélection"
|
||||
"selection.colors" = "Couleurs de la sélection"
|
||||
"selection.selectColor" = "Sélectionner les calques de cette couleur"
|
||||
"selection.setPaint" = "Changer la couleur de la sélection"
|
||||
"shape.arrow" = "Flèche"
|
||||
"shape.ellipse" = "Ellipse"
|
||||
"shape.line" = "Ligne"
|
||||
|
||||
@@ -642,6 +642,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Desain antarmuka dan prototyping. Alat desain clean-room gratis dan open-source yang ditulis dari nol dengan Rust, bagian dari ArtCraft Crafting Apps."
|
||||
"Intersect" = "Irisan"
|
||||
"Intersects" = "Beririsan"
|
||||
"Invalid color" = "Warna tidak valid"
|
||||
"Invite link" = "Link undangan"
|
||||
"Invite link copied" = "Link undangan disalin"
|
||||
"It asks permission to:" = "Plugin ini meminta izin untuk:"
|
||||
@@ -808,6 +809,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Tidak ada branch. Branch adalah salinan file untuk mencoba perubahan, lalu ditinjau dan di-merge."
|
||||
"No category" = "Tanpa kategori"
|
||||
"No changes" = "Tidak ada perubahan"
|
||||
"No color styles" = "Tidak ada gaya warna"
|
||||
"No colour variables fit here." = "Tidak ada variabel warna yang cocok di sini."
|
||||
"No colour variables yet. Create them in Local variables." = "Belum ada variabel warna. Buat di Variabel lokal."
|
||||
"No comments yet" = "Belum ada komentar"
|
||||
@@ -1165,12 +1167,14 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "Pilih semua yang sama"
|
||||
"Select layer" = "Pilih layer"
|
||||
"Select layers to apply the style to" = "Pilih layer untuk menerapkan gaya"
|
||||
"Select layers with this color" = "Pilih layer dengan warna ini"
|
||||
"Select overlapping shapes" = "Pilih bentuk yang tumpang tindih"
|
||||
"Select points to edit their position, radius and mirroring" = "Pilih titik untuk mengedit posisi, radius, dan pencerminannya"
|
||||
"Select slot" = "Pilih slot"
|
||||
"Selection" = "Pilihan"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Kotak pilihan, {w} x {h} di {x}, {y}: panah untuk memindahkan, Shift+panah atau {cmd}+panah untuk mengubah ukuran, Enter untuk memilih, Escape untuk membatalkan"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Kotak pilihan: panah untuk memindahkan, Shift+panah atau {cmd}+panah untuk mengubah ukuran, Enter untuk memilih, Escape untuk membatalkan"
|
||||
"Selection colors" = "Warna pilihan"
|
||||
"Send backward" = "Bawa mundur"
|
||||
"Send report" = "Kirim laporan"
|
||||
"Send to back" = "Bawa ke paling belakang"
|
||||
@@ -1996,6 +2000,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "Pilih stroke yang sama"
|
||||
"select.sameText" = "Pilih properti teks yang sama"
|
||||
"select.set" = "Pilih"
|
||||
"selection.applyColorStyle" = "Terapkan gaya warna ke pilihan"
|
||||
"selection.colors" = "Warna pilihan"
|
||||
"selection.selectColor" = "Pilih layer dengan warna ini"
|
||||
"selection.setPaint" = "Ubah warna pilihan"
|
||||
"shape.arrow" = "Panah"
|
||||
"shape.ellipse" = "Elips"
|
||||
"shape.line" = "Garis"
|
||||
|
||||
@@ -647,6 +647,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "インターフェースデザインとプロトタイピング。Rust でゼロから書かれた、無料でオープンソースのクリーンルーム設計のデザインツール。ArtCraft Crafting Apps の一つ。"
|
||||
"Intersect" = "交差"
|
||||
"Intersects" = "交差"
|
||||
"Invalid color" = "無効なカラー"
|
||||
"Invite link" = "招待リンク"
|
||||
"Invite link copied" = "招待リンクをコピー済み"
|
||||
"It asks permission to:" = "次の権限を求めている:"
|
||||
@@ -813,6 +814,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "ブランチはない。ブランチはファイルのコピーで、変更を試してからレビューしてマージできる。"
|
||||
"No category" = "カテゴリなし"
|
||||
"No changes" = "変更なし"
|
||||
"No color styles" = "カラースタイルがありません"
|
||||
"No colour variables fit here." = "ここに使えるカラー変数はない。"
|
||||
"No colour variables yet. Create them in Local variables." = "カラー変数はまだない。ローカル変数で作成。"
|
||||
"No comments yet" = "コメントはまだない"
|
||||
@@ -1172,12 +1174,14 @@ status = "draft — needs native review"
|
||||
"Select layer" = "レイヤーを選択"
|
||||
"Select layers and they space themselves as their content changes" = "レイヤーを選択すると、内容の変化に合わせて間隔を自動調整"
|
||||
"Select layers to apply the style to" = "スタイルを適用するレイヤーを選択"
|
||||
"Select layers with this color" = "このカラーのレイヤーを選択"
|
||||
"Select overlapping shapes" = "重なったシェイプを選択"
|
||||
"Select points to edit their position, radius and mirroring" = "ポイントを選択すると位置、半径、対称を編集できる"
|
||||
"Select slot" = "スロットを選択"
|
||||
"Selection" = "選択範囲"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "選択ボックス、{w} × {h}、位置 {x}, {y}:矢印キーで移動、Shift+矢印キーまたは {cmd}+矢印キーでサイズ変更、Enter で選択、Escape でキャンセル"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "選択ボックス:矢印キーで移動、Shift+矢印キーまたは {cmd}+矢印キーでサイズ変更、Enter で選択、Escape でキャンセル"
|
||||
"Selection colors" = "選択範囲のカラー"
|
||||
"Send backward" = "背面へ"
|
||||
"Send report" = "報告を送信"
|
||||
"Send to back" = "最背面へ"
|
||||
@@ -2008,6 +2012,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "同じ線を選択"
|
||||
"select.sameText" = "同じテキストプロパティを選択"
|
||||
"select.set" = "選択"
|
||||
"selection.applyColorStyle" = "選択範囲にカラースタイルを適用"
|
||||
"selection.colors" = "選択範囲のカラー"
|
||||
"selection.selectColor" = "このカラーのレイヤーを選択"
|
||||
"selection.setPaint" = "選択範囲のカラーを変更"
|
||||
"shape.arrow" = "矢印"
|
||||
"shape.ellipse" = "楕円"
|
||||
"shape.line" = "線"
|
||||
|
||||
@@ -619,6 +619,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "인터페이스 디자인과 프로토타이핑. Rust로 처음부터 작성한 무료 오픈 소스 클린룸 디자인 도구이며 ArtCraft Crafting Apps의 하나."
|
||||
"Intersect" = "교차"
|
||||
"Intersects" = "교차"
|
||||
"Invalid color" = "잘못된 색상"
|
||||
"Invite link" = "초대 링크"
|
||||
"Invite link copied" = "초대 링크 복사됨"
|
||||
"It asks permission to:" = "다음 권한을 요청함:"
|
||||
@@ -773,6 +774,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "브랜치 없음. 브랜치는 변경을 시험해 본 뒤 검토하고 병합하는 파일 사본임."
|
||||
"No category" = "카테고리 없음"
|
||||
"No changes" = "변경 사항 없음"
|
||||
"No color styles" = "색상 스타일 없음"
|
||||
"No colour variables fit here." = "여기에 맞는 색상 변수가 없음."
|
||||
"No colour variables yet. Create them in Local variables." = "아직 색상 변수가 없음. 로컬 변수에서 만드세요."
|
||||
"No comments yet" = "아직 댓글 없음"
|
||||
@@ -1119,12 +1121,14 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "같은 항목 모두 선택"
|
||||
"Select layer" = "레이어 선택"
|
||||
"Select layers to apply the style to" = "스타일을 적용할 레이어를 선택하세요"
|
||||
"Select layers with this color" = "이 색상의 레이어 선택"
|
||||
"Select overlapping shapes" = "겹친 도형 선택"
|
||||
"Select points to edit their position, radius and mirroring" = "점을 선택해 위치, 반경, 대칭 편집"
|
||||
"Select slot" = "슬롯 선택"
|
||||
"Selection" = "선택 항목"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "선택 상자, {w} × {h}, 위치 {x}, {y}: 화살표로 이동, Shift+화살표 또는 {cmd}+화살표로 크기 조정, Enter로 선택, Escape로 취소"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "선택 상자: 화살표로 이동, Shift+화살표 또는 {cmd}+화살표로 크기 조정, Enter로 선택, Escape로 취소"
|
||||
"Selection colors" = "선택 항목 색상"
|
||||
"Send backward" = "뒤로 보내기"
|
||||
"Send report" = "신고 보내기"
|
||||
"Send to back" = "맨 뒤로 보내기"
|
||||
@@ -1926,6 +1930,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "같은 선 선택"
|
||||
"select.sameText" = "같은 텍스트 속성 선택"
|
||||
"select.set" = "선택"
|
||||
"selection.applyColorStyle" = "선택 항목에 색상 스타일 적용"
|
||||
"selection.colors" = "선택 항목 색상"
|
||||
"selection.selectColor" = "이 색상의 레이어 선택"
|
||||
"selection.setPaint" = "선택 항목 색상 변경"
|
||||
"shape.arrow" = "화살표"
|
||||
"shape.ellipse" = "타원"
|
||||
"shape.line" = "선"
|
||||
|
||||
@@ -618,6 +618,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Design de interfaces e prototipagem. Uma ferramenta de design gratuita e de código aberto, com implementação independente (clean-room), escrita do zero em Rust, parte dos Crafting Apps da ArtCraft."
|
||||
"Intersect" = "Interseção"
|
||||
"Intersects" = "Interseções"
|
||||
"Invalid color" = "Cor inválida"
|
||||
"Invite link" = "Link de convite"
|
||||
"Invite link copied" = "Link de convite copiado"
|
||||
"It asks permission to:" = "Ele pede permissão para:"
|
||||
@@ -772,6 +773,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Nenhuma branch. Uma branch é uma cópia do arquivo para testar alterações e depois revisá-las e mesclá-las."
|
||||
"No category" = "Sem categoria"
|
||||
"No changes" = "Nenhuma alteração"
|
||||
"No color styles" = "Nenhum estilo de cor"
|
||||
"No colour variables fit here." = "Nenhuma variável de cor se encaixa aqui."
|
||||
"No colour variables yet. Create them in Local variables." = "Ainda não há variáveis de cor. Crie-as em Variáveis locais."
|
||||
"No comments yet" = "Ainda não há comentários"
|
||||
@@ -1118,12 +1120,14 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "Selecionar tudo com o mesmo"
|
||||
"Select layer" = "Selecionar camada"
|
||||
"Select layers to apply the style to" = "Selecione as camadas às quais aplicar o estilo"
|
||||
"Select layers with this color" = "Selecionar camadas com esta cor"
|
||||
"Select overlapping shapes" = "Selecione formas sobrepostas"
|
||||
"Select points to edit their position, radius and mirroring" = "Selecione pontos para editar posição, raio e espelhamento"
|
||||
"Select slot" = "Selecionar slot"
|
||||
"Selection" = "Seleção"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Caixa de seleção, {w} por {h} em {x}, {y}: as setas a movem, Shift+setas ou {cmd}+setas a redimensionam, Enter seleciona, Esc cancela"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Caixa de seleção: as setas a movem, Shift+setas ou {cmd}+setas a redimensionam, Enter seleciona, Esc cancela"
|
||||
"Selection colors" = "Cores da seleção"
|
||||
"Send backward" = "Enviar para trás"
|
||||
"Send report" = "Enviar denúncia"
|
||||
"Send to back" = "Enviar para o plano de fundo"
|
||||
@@ -1925,6 +1929,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "Selecionar mesmo traço"
|
||||
"select.sameText" = "Selecionar mesmas propriedades de texto"
|
||||
"select.set" = "Selecionar"
|
||||
"selection.applyColorStyle" = "Aplicar estilo de cor à seleção"
|
||||
"selection.colors" = "Cores da seleção"
|
||||
"selection.selectColor" = "Selecionar camadas com esta cor"
|
||||
"selection.setPaint" = "Alterar cor da seleção"
|
||||
"shape.arrow" = "Seta"
|
||||
"shape.ellipse" = "Elipse"
|
||||
"shape.line" = "Linha"
|
||||
|
||||
@@ -639,6 +639,7 @@ name = "Русский"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Дизайн интерфейсов и прототипирование. Бесплатный инструмент с открытым исходным кодом, независимо созданный с нуля на Rust, из семейства ArtCraft Crafting Apps."
|
||||
"Intersect" = "Пересечение"
|
||||
"Intersects" = "Пересечения"
|
||||
"Invalid color" = "Недопустимый цвет"
|
||||
"Invite link" = "Ссылка приглашения"
|
||||
"Invite link copied" = "Ссылка приглашения скопирована"
|
||||
"It asks permission to:" = "Запрашивает разрешение на:"
|
||||
@@ -805,6 +806,7 @@ name = "Русский"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Веток нет. Ветка — копия файла, в которой можно попробовать изменения, затем проверить и объединить их."
|
||||
"No category" = "Без категории"
|
||||
"No changes" = "Нет изменений"
|
||||
"No color styles" = "Нет цветовых стилей"
|
||||
"No colour variables fit here." = "Нет подходящих цветовых переменных."
|
||||
"No colour variables yet. Create them in Local variables." = "Цветовых переменных пока нет. Создайте их в разделе «Локальные переменные»."
|
||||
"No comments yet" = "Комментариев пока нет"
|
||||
@@ -1163,12 +1165,14 @@ name = "Русский"
|
||||
"Select all with same" = "Выделить всё с одинаковым"
|
||||
"Select layer" = "Выделить слой"
|
||||
"Select layers to apply the style to" = "Выделите слои для применения стиля"
|
||||
"Select layers with this color" = "Выделить слои с этим цветом"
|
||||
"Select overlapping shapes" = "Выделите пересекающиеся фигуры"
|
||||
"Select points to edit their position, radius and mirroring" = "Выделите точки для изменения положения, радиуса и зеркальности"
|
||||
"Select slot" = "Выделить слот"
|
||||
"Selection" = "Выделение"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Область выделения {w} на {h} в {x}, {y}: стрелки — перемещение, Shift+стрелки или {cmd}+стрелки — размер, Enter — выделить, Escape — отменить"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Область выделения: стрелки — перемещение, Shift+стрелки или {cmd}+стрелки — размер, Enter — выделить, Escape — отменить"
|
||||
"Selection colors" = "Цвета выделения"
|
||||
"Send backward" = "На уровень назад"
|
||||
"Send report" = "Отправить жалобу"
|
||||
"Send to back" = "На задний план"
|
||||
@@ -1994,6 +1998,10 @@ name = "Русский"
|
||||
"select.sameStroke" = "Выделить с одинаковой обводкой"
|
||||
"select.sameText" = "Выделить с одинаковыми свойствами текста"
|
||||
"select.set" = "Выделить"
|
||||
"selection.applyColorStyle" = "Применить цветовой стиль к выделению"
|
||||
"selection.colors" = "Цвета выделения"
|
||||
"selection.selectColor" = "Выделить слои с этим цветом"
|
||||
"selection.setPaint" = "Изменить цвет выделения"
|
||||
"shape.arrow" = "Стрелка"
|
||||
"shape.ellipse" = "Эллипс"
|
||||
"shape.line" = "Линия"
|
||||
|
||||
@@ -649,6 +649,7 @@ name = "Svenska"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Gränssnittsdesign och prototyper. Ett fritt clean-room-designverktyg med öppen källkod, skrivet från grunden i Rust och en del av ArtCraft Crafting Apps."
|
||||
"Intersect" = "Skärning"
|
||||
"Intersects" = "Skär"
|
||||
"Invalid color" = "Ogiltig färg"
|
||||
"Invite link" = "Inbjudningslänk"
|
||||
"Invite link copied" = "Inbjudningslänken har kopierats"
|
||||
"It asks permission to:" = "Den ber om behörighet att:"
|
||||
@@ -815,6 +816,7 @@ name = "Svenska"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Inga grenar. En gren är en kopia av filen där du kan prova ändringar och sedan granska och sammanfoga dem."
|
||||
"No category" = "Ingen kategori"
|
||||
"No changes" = "Inga ändringar"
|
||||
"No color styles" = "Inga färgformatmallar"
|
||||
"No colour variables fit here." = "Inga färgvariabler passar här."
|
||||
"No colour variables yet. Create them in Local variables." = "Inga färgvariabler än. Skapa dem i Lokala variabler."
|
||||
"No comments yet" = "Inga kommentarer än"
|
||||
@@ -1176,12 +1178,14 @@ name = "Svenska"
|
||||
"Select all with same" = "Markera alla med samma"
|
||||
"Select layer" = "Markera lager"
|
||||
"Select layers to apply the style to" = "Markera de lager som formatmallen ska användas på"
|
||||
"Select layers with this color" = "Markera lager med den här färgen"
|
||||
"Select overlapping shapes" = "Markera överlappande former"
|
||||
"Select points to edit their position, radius and mirroring" = "Markera punkter för att redigera deras position, radie och spegling"
|
||||
"Select slot" = "Markera plats"
|
||||
"Selection" = "Markering"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Markeringsruta, {w} gånger {h} vid {x}, {y}: piltangenterna flyttar den, Shift+pilar eller {cmd}+pilar ändrar storleken, Enter markerar, Escape avbryter"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Markeringsruta: piltangenterna flyttar den, Shift+pilar eller {cmd}+pilar ändrar storleken, Enter markerar, Escape avbryter"
|
||||
"Selection colors" = "Markeringsfärger"
|
||||
"Send backward" = "Flytta bakåt"
|
||||
"Send report" = "Skicka rapport"
|
||||
"Send to back" = "Flytta längst bak"
|
||||
@@ -2016,6 +2020,10 @@ name = "Svenska"
|
||||
"select.sameStroke" = "Markera samma kontur"
|
||||
"select.sameText" = "Markera samma textegenskaper"
|
||||
"select.set" = "Markera"
|
||||
"selection.applyColorStyle" = "Använd färgformatmall på markeringen"
|
||||
"selection.colors" = "Markeringsfärger"
|
||||
"selection.selectColor" = "Markera lager med den här färgen"
|
||||
"selection.setPaint" = "Ändra markeringens färg"
|
||||
"shape.arrow" = "Pil"
|
||||
"shape.ellipse" = "Ellips"
|
||||
"shape.line" = "Linje"
|
||||
|
||||
@@ -642,6 +642,7 @@ name = "Türkçe"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Arayüz tasarımı ve prototipleme. Rust ile sıfırdan yazılmış, ücretsiz, açık kaynaklı, temiz oda yöntemiyle geliştirilmiş bir tasarım aracı; ArtCraft Crafting Apps ailesinin bir parçası."
|
||||
"Intersect" = "Kesiştir"
|
||||
"Intersects" = "Kesişir"
|
||||
"Invalid color" = "Geçersiz renk"
|
||||
"Invite link" = "Davet bağlantısı"
|
||||
"Invite link copied" = "Davet bağlantısı kopyalandı"
|
||||
"It asks permission to:" = "Şunlar için izin istiyor:"
|
||||
@@ -808,6 +809,7 @@ name = "Türkçe"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Dal yok. Dal, değişiklikleri denemek, sonra gözden geçirip birleştirmek için dosyanın bir kopyasıdır."
|
||||
"No category" = "Kategori yok"
|
||||
"No changes" = "Değişiklik yok"
|
||||
"No color styles" = "Renk stili yok"
|
||||
"No colour variables fit here." = "Buraya uyan renk değişkeni yok."
|
||||
"No colour variables yet. Create them in Local variables." = "Henüz renk değişkeni yok. Onları Yerel değişkenler'de oluşturun."
|
||||
"No comments yet" = "Henüz yorum yok"
|
||||
@@ -1165,12 +1167,14 @@ name = "Türkçe"
|
||||
"Select all with same" = "Aynı olanların tümünü seç"
|
||||
"Select layer" = "Katman seç"
|
||||
"Select layers to apply the style to" = "Stilin uygulanacağı katmanları seçin"
|
||||
"Select layers with this color" = "Bu renkteki katmanları seç"
|
||||
"Select overlapping shapes" = "Çakışan şekilleri seç"
|
||||
"Select points to edit their position, radius and mirroring" = "Konumlarını, yarıçaplarını ve yansıtmalarını düzenlemek için noktaları seçin"
|
||||
"Select slot" = "Yuvayı seç"
|
||||
"Selection" = "Seçim"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Seçim kutusu, {x}, {y} konumunda {w} × {h}: oklar onu taşır, Shift+oklar veya {cmd}+oklar boyutlandırır, Enter seçer, Escape iptal eder"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Seçim kutusu: oklar onu taşır, Shift+oklar veya {cmd}+oklar boyutlandırır, Enter seçer, Escape iptal eder"
|
||||
"Selection colors" = "Seçim renkleri"
|
||||
"Send backward" = "Geriye gönder"
|
||||
"Send report" = "Raporu gönder"
|
||||
"Send to back" = "En arkaya gönder"
|
||||
@@ -1996,6 +2000,10 @@ name = "Türkçe"
|
||||
"select.sameStroke" = "Aynı konturu seç"
|
||||
"select.sameText" = "Aynı metin özelliklerini seç"
|
||||
"select.set" = "Seç"
|
||||
"selection.applyColorStyle" = "Seçime renk stili uygula"
|
||||
"selection.colors" = "Seçim renkleri"
|
||||
"selection.selectColor" = "Bu renkteki katmanları seç"
|
||||
"selection.setPaint" = "Seçimin rengini değiştir"
|
||||
"shape.arrow" = "Ok"
|
||||
"shape.ellipse" = "Elips"
|
||||
"shape.line" = "Çizgi"
|
||||
|
||||
@@ -620,6 +620,7 @@ status = "draft — needs native review"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "界面设计与原型制作。一款以 Rust 从零编写的免费、开源、净室设计工具,属于 ArtCraft Crafting Apps 系列。"
|
||||
"Intersect" = "相交"
|
||||
"Intersects" = "相交"
|
||||
"Invalid color" = "无效颜色"
|
||||
"Invite link" = "邀请链接"
|
||||
"Invite link copied" = "已复制邀请链接"
|
||||
"It asks permission to:" = "它请求以下权限:"
|
||||
@@ -774,6 +775,7 @@ status = "draft — needs native review"
|
||||
"No branches. A branch is a copy of the file to try changes in, then review and merge." = "没有分支。分支是文件的副本,可在其中尝试更改,再审阅并合并。"
|
||||
"No category" = "无类别"
|
||||
"No changes" = "无更改"
|
||||
"No color styles" = "没有颜色样式"
|
||||
"No colour variables fit here." = "没有适用于此处的颜色变量。"
|
||||
"No colour variables yet. Create them in Local variables." = "还没有颜色变量。请在“本地变量”中创建。"
|
||||
"No comments yet" = "还没有评论"
|
||||
@@ -1120,12 +1122,14 @@ status = "draft — needs native review"
|
||||
"Select all with same" = "选择所有相同"
|
||||
"Select layer" = "选择图层"
|
||||
"Select layers to apply the style to" = "选择要应用样式的图层"
|
||||
"Select layers with this color" = "选择使用此颜色的图层"
|
||||
"Select overlapping shapes" = "选择重叠的形状"
|
||||
"Select points to edit their position, radius and mirroring" = "选择点以编辑其位置、半径和镜像"
|
||||
"Select slot" = "选择插槽"
|
||||
"Selection" = "所选项"
|
||||
"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "选框,{w} × {h},位于 {x}, {y}:方向键移动,Shift+方向键或 {cmd}+方向键调整大小,Enter 选择,Esc 取消"
|
||||
"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "选框:方向键移动,Shift+方向键或 {cmd}+方向键调整大小,Enter 选择,Esc 取消"
|
||||
"Selection colors" = "所选项颜色"
|
||||
"Send backward" = "下移一层"
|
||||
"Send report" = "发送举报"
|
||||
"Send to back" = "置于底层"
|
||||
@@ -1927,6 +1931,10 @@ status = "draft — needs native review"
|
||||
"select.sameStroke" = "选择相同描边"
|
||||
"select.sameText" = "选择相同文本属性"
|
||||
"select.set" = "选择"
|
||||
"selection.applyColorStyle" = "将颜色样式应用于所选项"
|
||||
"selection.colors" = "所选项颜色"
|
||||
"selection.selectColor" = "选择使用此颜色的图层"
|
||||
"selection.setPaint" = "更改所选项颜色"
|
||||
"shape.arrow" = "箭头"
|
||||
"shape.ellipse" = "椭圆"
|
||||
"shape.line" = "直线"
|
||||
|
||||
@@ -18,6 +18,11 @@ use crate::widgets;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PickerTarget {
|
||||
PageBackground,
|
||||
SelectionColor {
|
||||
targets: Vec<figcraft_engine::selection_colors::PaintTarget>,
|
||||
selection: Vec<NodeId>,
|
||||
document: u64,
|
||||
},
|
||||
Paint {
|
||||
fills: bool,
|
||||
index: usize,
|
||||
@@ -72,6 +77,13 @@ pub fn range_fills(doc: &figcraft_doc::Document, id: NodeId, start: usize) -> Op
|
||||
fn current_paint(app: &FigApp, target: &PickerTarget) -> Option<Paint> {
|
||||
let doc = app.session.doc().ok()?;
|
||||
match target {
|
||||
PickerTarget::SelectionColor { targets, selection, document } => {
|
||||
let ds = app.session.ds().ok()?;
|
||||
if ds.uid != *document || ds.selection != *selection {
|
||||
return None;
|
||||
}
|
||||
targets.first()?.paint(doc)
|
||||
}
|
||||
PickerTarget::PageBackground => match &doc.node(app.session.page().ok()?)?.kind {
|
||||
NodeKind::Page { background, .. } => Some(Paint::solid(*background)),
|
||||
_ => None,
|
||||
@@ -121,6 +133,7 @@ fn write(app: &mut FigApp, target: &PickerTarget, paint: &Paint, live: bool) {
|
||||
let _ = app.session.begin_interaction("Color");
|
||||
}
|
||||
let r = match target {
|
||||
PickerTarget::SelectionColor { targets, .. } => app.session.execute("selection.setPaint", &json!({"targets": targets, "paint": paint})),
|
||||
PickerTarget::PageBackground => {
|
||||
let c = paint.solid_color().unwrap_or(Color::DEFAULT_PAGE);
|
||||
app.session.execute("page.setBackground", &json!({"color": c.to_css_hex()}))
|
||||
@@ -434,6 +447,9 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
};
|
||||
let current = if stale { None } else { current_paint(app, &target) };
|
||||
let Some(mut paint) = current else {
|
||||
if matches!(target, PickerTarget::SelectionColor { .. }) {
|
||||
finish(app);
|
||||
}
|
||||
app.ui.picker = None;
|
||||
return;
|
||||
};
|
||||
@@ -872,6 +888,7 @@ fn pattern_sources(app: &FigApp, target: &PickerTarget) -> Vec<(NodeId, String)>
|
||||
let Ok(doc) = app.session.doc() else { return Vec::new() };
|
||||
let painted = match target {
|
||||
PickerTarget::Paint { ids, .. } => ids.clone(),
|
||||
PickerTarget::SelectionColor { targets, .. } => targets.iter().map(|t| t.id).collect(),
|
||||
PickerTarget::TextRange { id, .. } => vec![*id],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
@@ -1129,6 +1146,54 @@ impl WithStops for Gradient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod selection_color_tests {
|
||||
use super::*;
|
||||
use crate::NoServices;
|
||||
use figcraft_engine::{Session, selection_colors::collect};
|
||||
|
||||
fn setup() -> (FigApp, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
let a = NodeId(s.execute("shape.rectangle", &json!({"width":40,"height":40,"props":{"fill":"#ff0000"}})).unwrap()["id"].as_u64().unwrap());
|
||||
let b = NodeId(s.execute("shape.rectangle", &json!({"width":40,"height":40,"props":{"fill":"#0000ff"}})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.set", &json!({"ids":[a,b]})).unwrap();
|
||||
let mut app = FigApp::new(s, Box::new(NoServices));
|
||||
let targets = collect(app.session.doc().unwrap(), &[a, b])[0].targets.clone();
|
||||
crate::selection_colors::open_picker(&mut app, targets);
|
||||
(app, a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_live_changes_keep_frozen_targets_and_commit_once() {
|
||||
let (mut app, a, b) = setup();
|
||||
let target = app.ui.picker.clone().unwrap();
|
||||
let before = app.session.undo_depth();
|
||||
for color in ["#0000ff", "#00ff00"] {
|
||||
write(&mut app, &target, &Paint::solid(Color::from_hex(color).unwrap()), true);
|
||||
}
|
||||
assert!(app.session.recent_colors.is_empty(), "previews must not be recorded");
|
||||
finish(&mut app);
|
||||
assert_eq!(app.session.recent_colors, vec![Color::from_hex("#00ff00").unwrap()]);
|
||||
assert_eq!(app.session.undo_depth().0, before.0 + 1);
|
||||
assert_eq!(current_paint(&app, &target).unwrap().solid_color().unwrap().to_hex(), "00FF00");
|
||||
assert_eq!(app.session.doc().unwrap().node(b).unwrap().fills[0].solid_color().unwrap().to_hex(), "0000FF");
|
||||
app.session.undo().unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().node(a).unwrap().fills[0].solid_color().unwrap().to_hex(), "FF0000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_does_not_follow_selection_or_document_switches() {
|
||||
let (mut app, _, _) = setup();
|
||||
let target = app.ui.picker.clone().unwrap();
|
||||
app.session.execute("select.none", &json!({})).unwrap();
|
||||
assert!(current_paint(&app, &target).is_none());
|
||||
let (mut app, _, _) = setup();
|
||||
let target = app.ui.picker.clone().unwrap();
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
assert!(current_paint(&app, &target).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -51,6 +51,7 @@ pub mod render_worker;
|
||||
pub mod right;
|
||||
pub mod scale_panel;
|
||||
pub mod section_panel;
|
||||
pub mod selection_colors;
|
||||
pub mod shortcuts;
|
||||
pub mod shortcuts_panel;
|
||||
pub mod style_picker;
|
||||
|
||||
@@ -163,7 +163,7 @@ impl Win {
|
||||
}
|
||||
}
|
||||
|
||||
const CMD: Modifiers = Modifiers::COMMAND.plus(Modifiers::CTRL);
|
||||
const CMD: Modifiers = if cfg!(target_os = "macos") { Modifiers::COMMAND } else { Modifiers::COMMAND.plus(Modifiers::CTRL) };
|
||||
|
||||
fn key(key: Key, pressed: bool, modifiers: Modifiers) -> Event {
|
||||
Event::Key { key, physical_key: Some(key), pressed, repeat: false, modifiers }
|
||||
|
||||
@@ -380,6 +380,7 @@ fn design(app: &mut FigApp, ui: &mut Ui) {
|
||||
widgets::separator(ui);
|
||||
effects(app, ui, &doc, &sel);
|
||||
widgets::separator(ui);
|
||||
crate::selection_colors::section(app, ui, &doc, &sel);
|
||||
if sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like())) {
|
||||
grids(app, ui, &doc, &sel);
|
||||
widgets::separator(ui);
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
//! Selection colors lives below Effects. A picker edits frozen paint slots, so color groups
|
||||
//! merging while scrubbing cannot accidentally recolor other objects.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Id, Ui};
|
||||
use figcraft_color::Color;
|
||||
use figcraft_doc::{Document, NodeId, StyleKind};
|
||||
use figcraft_engine::selection_colors::{PaintTarget, SelectionColor, collect};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::a11y::Tip;
|
||||
use crate::colorpicker::PickerTarget;
|
||||
use crate::i18n::tr;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{FigApp, widgets};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Cache {
|
||||
document: u64,
|
||||
doc: Arc<Document>,
|
||||
ids: Vec<NodeId>,
|
||||
colors: Arc<Vec<SelectionColor>>,
|
||||
}
|
||||
|
||||
fn colors(ui: &Ui, document: u64, doc: &Arc<Document>, ids: &[NodeId], interacting: bool) -> Arc<Vec<SelectionColor>> {
|
||||
let key = Id::new("selection_colors_cache");
|
||||
if let Some(cache) = ui.data(|d| d.get_temp::<Cache>(key))
|
||||
&& cache.document == document
|
||||
&& cache.ids == ids
|
||||
&& (interacting || Arc::ptr_eq(&cache.doc, doc))
|
||||
{
|
||||
return cache.colors;
|
||||
}
|
||||
// A drag replaces the document snapshot every frame. Keep its existing rows until the
|
||||
// gesture ends; a cold cache or a changed document/selection waits too. Engine commands
|
||||
// still discover and change the complete selection, independently of this panel cache.
|
||||
if interacting {
|
||||
return Arc::new(Vec::new());
|
||||
}
|
||||
let colors = Arc::new(collect(doc, ids));
|
||||
ui.data_mut(|d| d.insert_temp(key, Cache { document, doc: doc.clone(), ids: ids.to_vec(), colors: colors.clone() }));
|
||||
colors
|
||||
}
|
||||
|
||||
pub(crate) fn open_picker(app: &mut FigApp, targets: Vec<PaintTarget>) {
|
||||
let Ok(ds) = app.session.ds() else { return };
|
||||
app.ui.picker = Some(PickerTarget::SelectionColor { targets, selection: ds.selection.clone(), document: ds.uid });
|
||||
}
|
||||
|
||||
pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, ids: &[NodeId]) {
|
||||
let Ok(ds) = app.session.ds() else { return };
|
||||
let interacting = ds.interaction.is_some();
|
||||
let colors = colors(ui, ds.uid, doc, ids, interacting);
|
||||
if colors.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
widgets::section_header(ui, tr("Selection colors"), &[], false);
|
||||
// Bound the panel's height; a document with many distinct colors stays scrollable.
|
||||
ui.add_enabled_ui(!interacting, |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("selection_colors_rows").max_height(240.0).show_rows(ui, 32.0, colors.len(), |ui, range| {
|
||||
for group in &colors[range] {
|
||||
ui.push_id((&group.targets, "selection_color"), |ui| {
|
||||
crate::right::row(ui, 32.0, |ui| {
|
||||
let swatch = widgets::swatch(ui, &group.paint, 24.0).tip(tr("Edit colour"));
|
||||
if swatch.clicked() {
|
||||
open_picker(app, group.targets.clone());
|
||||
}
|
||||
let width = (ui.available_width() - 116.0).max(40.0);
|
||||
let linked_name = group
|
||||
.style
|
||||
.as_ref()
|
||||
.and_then(|id| doc.style(id).map(|s| s.name.as_str()))
|
||||
.or_else(|| group.variable.as_ref().and_then(|id| doc.variables.get(id).map(|v| v.name.as_str())));
|
||||
if let Some(name) = linked_name {
|
||||
if widgets::dropdown(ui, name, width, None).clicked() {
|
||||
open_picker(app, group.targets.clone());
|
||||
}
|
||||
} else if let Some(color) = group.paint.solid_color() {
|
||||
if let Some(hex) = widgets::text_field_named(ui, ui.id().with("hex"), &color.to_hex(), width, tr("Color")) {
|
||||
if let Some(color) = Color::from_hex(&hex) {
|
||||
let mut paint = group.paint.clone();
|
||||
paint.kind = figcraft_color::PaintKind::Solid { color: color.with_alpha(1.0) };
|
||||
if color.a < 1.0 {
|
||||
paint.opacity = color.a;
|
||||
}
|
||||
app.run("selection.setPaint", json!({"targets": group.targets, "paint": paint}));
|
||||
} else {
|
||||
app.toast(tr("Invalid color"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let response = widgets::dropdown(ui, &widgets::paint_name(&group.paint), width, None);
|
||||
if response.clicked() {
|
||||
open_picker(app, group.targets.clone());
|
||||
}
|
||||
}
|
||||
let alpha = format!("{}%", figcraft_doc::trim_num(f64::from(group.paint.opacity) * 100.0));
|
||||
if let Some(value) = widgets::text_field_named(ui, ui.id().with("opacity"), &alpha, 44.0, tr("Opacity"))
|
||||
&& let Ok(value) = value.trim().trim_end_matches('%').parse::<f32>()
|
||||
&& value.is_finite()
|
||||
{
|
||||
let mut paint = group.paint.clone();
|
||||
paint.opacity = (value / 100.0).clamp(0.0, 1.0);
|
||||
app.run("selection.setPaint", json!({"targets": group.targets, "paint": paint}));
|
||||
}
|
||||
let styles = icons::button(ui, Icon::Styles, 24.0, false, tr("Color styles"));
|
||||
crate::right::menu_popup(&styles).show(|ui| {
|
||||
let mut any = false;
|
||||
for style in &doc.styles {
|
||||
if !matches!(style.kind, StyleKind::Paint { .. }) {
|
||||
continue;
|
||||
}
|
||||
any = true;
|
||||
if crate::right::menu_item(ui, &style.name, "") {
|
||||
app.run("selection.applyColorStyle", json!({"targets": group.targets, "style": style.id}));
|
||||
app.ui.picker = None;
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
widgets::label(ui, tr("No color styles"), t.text_secondary);
|
||||
}
|
||||
});
|
||||
if icons::button(ui, Icon::Move, 24.0, false, tr("Select layers with this color")).clicked() {
|
||||
app.run("selection.selectColor", json!({"targets": group.targets}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
widgets::separator(ui);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::NoServices;
|
||||
use egui::{Event, Pos2, RawInput, Rect, Shape, vec2};
|
||||
use figcraft_engine::Session;
|
||||
|
||||
struct Panel {
|
||||
ctx: egui::Context,
|
||||
app: FigApp,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
fn new() -> (Self, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
let mut ids = Vec::new();
|
||||
for color in ["#ff0000", "#0000ff"] {
|
||||
let value = s.execute("shape.rectangle", &json!({"width":40,"height":40,"props":{"fill":color}})).unwrap();
|
||||
ids.push(NodeId(value["id"].as_u64().unwrap()));
|
||||
}
|
||||
s.execute("select.set", &json!({"ids":ids})).unwrap();
|
||||
let mut p = Self { ctx: egui::Context::default(), app: FigApp::new(s, Box::new(NoServices)), time: 0.0 };
|
||||
crate::theme::install_fonts(&p.ctx, false, 0);
|
||||
crate::theme::apply(&p.ctx, crate::theme::Tokens::light());
|
||||
for _ in 0..3 {
|
||||
p.frame(vec![]);
|
||||
}
|
||||
(p, ids[0], ids[1])
|
||||
}
|
||||
fn frame(&mut self, events: Vec<Event>) -> Vec<(String, Pos2)> {
|
||||
self.time += 1.0 / 60.0;
|
||||
let input = RawInput {
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(280.0, 600.0))),
|
||||
time: Some(self.time),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let mut output = self.ctx.run_ui(input, |ui| {
|
||||
let ds = self.app.session.ds().unwrap();
|
||||
let doc = ds.doc.clone();
|
||||
let ids = ds.selection.clone();
|
||||
section(&mut self.app, ui, &doc, &ids);
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
fn texts(shape: &Shape, out: &mut Vec<(String, Pos2)>) {
|
||||
match shape {
|
||||
Shape::Text(t) => out.push((t.galley.text().to_string(), t.pos + t.galley.size() / 2.0)),
|
||||
Shape::Vec(v) => {
|
||||
for s in v {
|
||||
texts(s, out)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut text = Vec::new();
|
||||
for s in &output.shapes {
|
||||
texts(&s.shape, &mut text);
|
||||
}
|
||||
text
|
||||
}
|
||||
fn click(&mut self, pos: Pos2) {
|
||||
for pressed in [true, false] {
|
||||
self.frame(vec![
|
||||
Event::PointerMoved(pos),
|
||||
Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() },
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_colors_panel_opens_the_existing_picker_and_hides_for_one_color() {
|
||||
let (mut p, a, b) = Panel::new();
|
||||
let texts = p.frame(vec![]);
|
||||
assert!(texts.iter().any(|(s, _)| s == "Selection colors"));
|
||||
let red = texts.iter().find(|(s, _)| s == "FF0000").unwrap().1;
|
||||
p.click(Pos2::new(28.0, red.y));
|
||||
let Some(PickerTarget::SelectionColor { targets, .. }) = &p.app.ui.picker else { panic!("the swatch must open its picker") };
|
||||
assert_eq!(targets.iter().map(|t| t.id).collect::<Vec<_>>(), [a]);
|
||||
p.app.run("fill.set", json!({"id":b,"color":"#ff0000"}));
|
||||
assert!(!p.frame(vec![]).iter().any(|(s, _)| s == "Selection colors"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_keeps_cached_colors_during_interaction_and_refreshes_after_commit() {
|
||||
let (mut p, a, _) = Panel::new();
|
||||
let key = Id::new("selection_colors_cache");
|
||||
let before = p.ctx.data(|d| d.get_temp::<Cache>(key)).unwrap();
|
||||
p.app.session.begin_interaction("Color").unwrap();
|
||||
for hex in ["#00ff00", "#ffff00", "#00ffff"] {
|
||||
p.app.run("fill.set", json!({"id":a,"color":hex}));
|
||||
p.frame(vec![]);
|
||||
let during = p.ctx.data(|d| d.get_temp::<Cache>(key)).unwrap();
|
||||
assert!(Arc::ptr_eq(&before.colors, &during.colors), "a live edit must not rediscover paints");
|
||||
}
|
||||
p.app.session.commit_interaction().unwrap();
|
||||
let texts = p.frame(vec![]);
|
||||
assert!(texts.iter().any(|(s, _)| s == "00FFFF"));
|
||||
let after = p.ctx.data(|d| d.get_temp::<Cache>(key)).unwrap();
|
||||
assert!(!Arc::ptr_eq(&before.colors, &after.colors));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_defers_discovery_without_a_matching_cache_during_interaction() {
|
||||
let (mut p, a, _) = Panel::new();
|
||||
let key = Id::new("selection_colors_cache");
|
||||
p.ctx.data_mut(|d| d.remove::<Cache>(key));
|
||||
p.app.session.begin_interaction("Color").unwrap();
|
||||
p.app.run("fill.set", json!({"id":a,"color":"#00ff00"}));
|
||||
assert!(!p.frame(vec![]).iter().any(|(s, _)| s == "Selection colors"));
|
||||
assert!(p.ctx.data(|d| d.get_temp::<Cache>(key)).is_none());
|
||||
p.app.session.cancel_interaction().unwrap();
|
||||
assert!(p.frame(vec![]).iter().any(|(s, _)| s == "FF0000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_color_button_selects_only_the_matching_layers() {
|
||||
let (mut p, a, _) = Panel::new();
|
||||
let red = p.frame(vec![]).into_iter().find(|(s, _)| s == "FF0000").unwrap().1;
|
||||
p.click(Pos2::new(252.0, red.y));
|
||||
assert_eq!(p.app.session.selection(), [a]);
|
||||
assert!(p.app.ui.last_error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_displays_the_linked_style_name_and_the_other_color() {
|
||||
let (mut p, a, b) = Panel::new();
|
||||
p.app.run("style.create", json!({"type":"paint","name":"Brand","value":"#ff0000"}));
|
||||
p.app.run("style.apply", json!({"id":a,"style":"Brand","target":"fill"}));
|
||||
let texts = p.frame(vec![]);
|
||||
assert!(texts.iter().any(|(s, _)| s == "Brand"));
|
||||
assert!(texts.iter().any(|(s, _)| s == "0000FF"));
|
||||
let colors = collect(p.app.session.doc().unwrap(), &[a, b]);
|
||||
assert_eq!(colors.len(), 2);
|
||||
}
|
||||
}
|
||||
+21
@@ -1356,6 +1356,27 @@ through the outlines of its layers.
|
||||
clears it; no `preset` reads it). The desktop app reads pressure from pen input on Windows; in the
|
||||
browser from stylus touch events that report a force; macOS and Linux tablets report none.
|
||||
|
||||
### Selection colors
|
||||
|
||||
`selection.colors {ids?}` returns `{colors: [{paint, style, variable, targets}]}` across the
|
||||
selection and its descendants, deduplicating overlapping roots. Only visible solid and gradient
|
||||
paints are listed; masks and boolean operands are omitted. Styles and variables distinguish
|
||||
otherwise equal paints. Each target is `{id, fills, index, range}`; `range` is null for a node
|
||||
paint or `[start, end]` in character offsets for an effective text-range fill.
|
||||
|
||||
Pass the returned target list to:
|
||||
|
||||
- `selection.setPaint {targets, paint}` to replace only those slots (same paint syntax as `fill.set`).
|
||||
- `selection.selectColor {targets}` to select their distinct owning layers, without a document undo step.
|
||||
- `selection.applyColorStyle {targets, style}` to apply a document paint style. As with `style.apply`,
|
||||
this replaces the matching node's entire fill/stroke list and links the style. Text ranges copy
|
||||
resolved paints and keep their typography, without a paint-style link.
|
||||
|
||||
Keep the targets fixed throughout a live interaction. A color becoming equal to another color
|
||||
must not add slots to the gesture. Invalid or missing targets fail atomically. Direct paint edits
|
||||
detach bindings on changed paint values (opacity alone keeps the color binding); a text-range edit preserves the node bindings other
|
||||
characters still inherit. Each mutating command is one undo step (an interaction can group them).
|
||||
|
||||
### Paint, effect and layout grid lists
|
||||
|
||||
A layer's fills, strokes, effects and layout grids are lists kept **bottom first**: index 0 paints
|
||||
|
||||
Reference in New Issue
Block a user