mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Brushes: dragging a brush reorders it within its type
The reference app reorders a brush that is dragged to a new place in
its Brushes panel, among the brushes of its type only. VectorCraft's
Brushes panel could not reorder brushes, and no command moved one.
brush.move {name | names, to?, type?} moves brushes to index to among
the brushes of their type, in brush.list order, as one undo step. A
move that keeps the order records no step. Brushes of different types,
or a type other than theirs, are refused with "brushes move only within
their type". In the Brushes panel, a brush dragged onto another brush
of its type goes before or after it by the half of the tile or row
under the pointer, and a bar marks the slot while it is held there. A
brush dropped on a brush of another type stays where it is, and the
status bar shows the refusal.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -17,7 +17,7 @@ use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
|
||||
use super::edit::{outermost, selected_roots};
|
||||
use super::pathops::{remove_each, separate};
|
||||
use super::swatch::names_param;
|
||||
use super::swatch::{anchor, names_param};
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
|
||||
@@ -76,6 +76,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
brush_delete
|
||||
),
|
||||
cmd!(
|
||||
"brush.move",
|
||||
"Move Brush",
|
||||
[],
|
||||
None,
|
||||
"{name? | names?: [brush names of one type, in order], to?: index among the brushes of that type in brush.list order (they go before the brush of that type now there that isn't moving; default: after the last one), type?: calligraphic|scatter|art|pattern|bristle (the type they go into; default: theirs)} reorder brushes within their type, as one undo step (none when the order stays as it was); brushes of different types, or a type that isn't theirs, are refused (brushes move only within their type) → {moved}",
|
||||
has_doc,
|
||||
brush_move
|
||||
),
|
||||
cmd!(
|
||||
"brush.duplicate",
|
||||
"Duplicate Brush",
|
||||
@@ -466,6 +475,54 @@ fn brush_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Moves the brushes in `name` or `names`, in that order, to index `to` among the brushes of their
|
||||
/// type in library order: before the first brush of that type at or after `to` that isn't moving,
|
||||
/// else after the last brush of that type that isn't moving. Brushes of different types, or a `type`
|
||||
/// that isn't theirs, are an error. A move that leaves the order as it was records no undo step.
|
||||
fn brush_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "brush.move";
|
||||
const WITHIN: &str = "brushes move only within their type";
|
||||
let names = names_param(p, C)?;
|
||||
check_numbers(p, &["to"], C)?;
|
||||
let to = match p.get("to").filter(|v| !v.is_null()) {
|
||||
Some(v) => Some(v.as_u64().map(|i| usize::try_from(i).unwrap_or(usize::MAX)).ok_or_else(|| bad(C, "`to` must be a whole number"))?),
|
||||
None => None,
|
||||
};
|
||||
let old = brush::library(&s.doc()?.doc);
|
||||
let mut lib = old.clone();
|
||||
// `names_param` refuses an empty list, so the loop sets `kind`.
|
||||
let mut kind = "";
|
||||
for name in &names {
|
||||
let b = lib.iter().find(|b| &b.name == name).ok_or_else(|| bad(C, format!("no brush named `{name}`")))?;
|
||||
if !kind.is_empty() && kind != b.kind.type_id() {
|
||||
return Err(bad(C, WITHIN));
|
||||
}
|
||||
kind = b.kind.type_id();
|
||||
}
|
||||
if p.get("type").filter(|v| !v.is_null()).is_some_and(|t| t.as_str() != Some(kind)) {
|
||||
return Err(bad(C, WITHIN));
|
||||
}
|
||||
let same: Vec<&Brush> = lib.iter().filter(|b| b.kind.type_id() == kind).collect();
|
||||
let before = anchor(&same, to, &names, |b| &b.name);
|
||||
let first = lib.iter().position(|b| names.contains(&b.name)).unwrap_or(lib.len());
|
||||
let mut items = vec![];
|
||||
for name in &names {
|
||||
if let Some(i) = lib.iter().position(|b| &b.name == name) {
|
||||
items.push(lib.remove(i));
|
||||
}
|
||||
}
|
||||
let at = match before {
|
||||
Some(a) => lib.iter().position(|b| b.name == a),
|
||||
None => lib.iter().rposition(|b| b.kind.type_id() == kind).map(|i| i + 1),
|
||||
};
|
||||
let at = at.unwrap_or(first).min(lib.len());
|
||||
lib.splice(at..at, items);
|
||||
if lib != old {
|
||||
save_library(s, "Move Brush", lib, &[])?;
|
||||
}
|
||||
Ok(json!({ "moved": names.len() }))
|
||||
}
|
||||
|
||||
/// Adds a copy of each brush in `name` or `names` at the end of the library. `newName`, given
|
||||
/// without `names`, names the copy.
|
||||
fn brush_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -351,7 +351,7 @@ fn swatch_new_group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// The name of the item that items moving to index `to` of `list` go before: the first one at or
|
||||
/// after `to` that isn't moving (`None`: the end).
|
||||
fn anchor<T>(list: &[T], to: Option<usize>, moving: &[String], name: impl Fn(&T) -> &str) -> Option<String> {
|
||||
pub(super) fn anchor<T>(list: &[T], to: Option<usize>, moving: &[String], name: impl Fn(&T) -> &str) -> Option<String> {
|
||||
list.iter().skip(to?).map(&name).find(|n| !moving.iter().any(|m| m == n)).map(str::to_string)
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,62 @@ fn brush_options_rename_duplicate_and_delete_update_strokes() {
|
||||
assert!(s.execute("brush.delete", &json!({"name": "5 pt. Round"})).is_err());
|
||||
}
|
||||
|
||||
/// `brush.move`, which the Brushes panel runs when a brush is dragged, reorders brushes among those of
|
||||
/// their type in one undo step and refuses to move one into another type.
|
||||
#[test]
|
||||
fn brush_move_reorders_brushes_within_their_type() {
|
||||
let mut s = session();
|
||||
let names = |s: &mut Session, ty: &str| -> Vec<String> {
|
||||
let list = s.execute("brush.list", &json!({})).unwrap();
|
||||
let of_type = list["brushes"].as_array().unwrap().iter().filter(|b| ty.is_empty() || b["type"] == ty);
|
||||
of_type.map(|b| b["name"].as_str().unwrap().to_string()).collect()
|
||||
};
|
||||
let undo = |s: &Session| s.doc().unwrap().history.undo.len();
|
||||
let before = names(&mut s, "");
|
||||
assert_eq!(names(&mut s, "calligraphic"), ["3 pt. Round", "6 pt. Flat", "10 pt. Oval"]);
|
||||
let steps = undo(&s);
|
||||
// To index 0 of the calligraphic brushes: before the first; the other types stay where they are.
|
||||
let r = s.execute("brush.move", &json!({"name": "10 pt. Oval", "to": 0})).unwrap();
|
||||
assert_eq!(r["moved"], json!(1));
|
||||
assert_eq!(names(&mut s, "")[..3], ["10 pt. Oval", "3 pt. Round", "6 pt. Flat"]);
|
||||
assert_eq!(names(&mut s, "")[3..], before[3..]);
|
||||
assert_eq!(undo(&s), steps + 1);
|
||||
// Several go in the order given, as one step.
|
||||
s.execute("brush.move", &json!({"names": ["6 pt. Flat", "3 pt. Round"], "to": 0, "type": "calligraphic"})).unwrap();
|
||||
assert_eq!(names(&mut s, "calligraphic"), ["6 pt. Flat", "3 pt. Round", "10 pt. Oval"]);
|
||||
assert_eq!(undo(&s), steps + 2);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(names(&mut s, "calligraphic"), ["10 pt. Oval", "3 pt. Round", "6 pt. Flat"]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(names(&mut s, ""), before);
|
||||
// Into the scatter brushes, with a scatter brush, or with a `to` that isn't a whole number: an
|
||||
// error, and nothing moves.
|
||||
for (p, why) in [
|
||||
(json!({"name": "3 pt. Round", "type": "scatter"}), "brushes move only within their type"),
|
||||
(json!({"names": ["3 pt. Round", "Dots"], "to": 0}), "brushes move only within their type"),
|
||||
(json!({"name": "3 pt. Round", "to": "1"}), "`to` must be a number, not \"1\""),
|
||||
(json!({"name": "3 pt. Round", "to": 1.5}), "`to` must be a whole number"),
|
||||
(json!({"name": "Nope", "to": 0}), "no brush named `Nope`"),
|
||||
] {
|
||||
let r = s.execute("brush.move", &p);
|
||||
assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if msg == why), "{p}: {r:?}");
|
||||
}
|
||||
assert_eq!((names(&mut s, ""), undo(&s)), (before.clone(), steps));
|
||||
// A move that keeps the order records no step.
|
||||
s.execute("brush.move", &json!({"name": "3 pt. Round", "to": 0})).unwrap();
|
||||
assert_eq!((names(&mut s, ""), undo(&s)), (before.clone(), steps));
|
||||
// Without `to`, or past the last one: after the last brush of the type, even one listed after
|
||||
// brushes of other types.
|
||||
s.execute("brush.new", &json!({"type": "calligraphic", "name": "Quill"})).unwrap();
|
||||
s.execute("brush.move", &json!({"name": "3 pt. Round"})).unwrap();
|
||||
assert_eq!(names(&mut s, "calligraphic"), ["6 pt. Flat", "10 pt. Oval", "Quill", "3 pt. Round"]);
|
||||
assert_eq!(names(&mut s, "").last().map(String::as_str), Some("3 pt. Round"));
|
||||
s.execute("brush.move", &json!({"name": "6 pt. Flat", "to": 99})).unwrap();
|
||||
assert_eq!(names(&mut s, "calligraphic"), ["10 pt. Oval", "Quill", "3 pt. Round", "6 pt. Flat"]);
|
||||
s.execute("brush.move", &json!({"name": "Leaves", "to": 1})).unwrap();
|
||||
assert_eq!(names(&mut s, "scatter"), ["Dots", "Leaves", "Confetti"]);
|
||||
}
|
||||
|
||||
/// New Brush, Duplicate Brush and Brush Options trim the name they are given, as the Brush Options
|
||||
/// dialog does; the trimmed name is the one compared with the brushes there are, and a blank name
|
||||
/// gives the name the command gives when there is none.
|
||||
|
||||
@@ -1826,6 +1826,7 @@
|
||||
Move Slices Преместване на резени
|
||||
Move Spine Point Преместване на точка от оста
|
||||
Reshape Spine Segment Промяна на формата на сегмент от оста
|
||||
Move Brush Преместване на четка
|
||||
Move Swatch Преместване на образец
|
||||
Move Up Преместване нагоре
|
||||
Move… Преместване…
|
||||
@@ -3918,6 +3919,7 @@ axis Both И двете
|
||||
@msg no bounds няма граници
|
||||
@msg no brush named `{name}` няма четка с име `{name}`
|
||||
@msg no brush selected няма избрана четка
|
||||
@msg brushes move only within their type четките се преместват само в рамките на своя тип
|
||||
@msg no catalog няма каталог
|
||||
@msg no changes since the last save няма промени от последното запазване
|
||||
@msg no clock to keep a heartbeat by няма часовник за поддържане на ритъм
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3558,6 +3558,7 @@ theme Appearance Motiv
|
||||
Delete Swatch Odstranit položku vzorníku
|
||||
Duplicate Swatch Duplikovat položku vzorníku
|
||||
Merge Swatches Sloučit položky vzorníku
|
||||
Move Brush Přesunout štětec
|
||||
Move Swatch Přesunout položku vzorníku
|
||||
New Color Group Nová skupina barev
|
||||
New Swatch Nová položka vzorníku
|
||||
@@ -4308,6 +4309,7 @@ imageTrace Outline Obrys
|
||||
@msg no bounds žádné hranice
|
||||
@msg no brush named `{name}` štětec s názvem `{name}` neexistuje
|
||||
@msg no brush selected není vybrán žádný štětec
|
||||
@msg brushes move only within their type štětce lze přesouvat jen v rámci jejich typu
|
||||
@msg no catalog chybí katalog
|
||||
@msg no changes since the last save od posledního uložení nejsou žádné změny
|
||||
@msg no clock to keep a heartbeat by chybí hodiny, podle kterých by šlo udržovat signál aktivity
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3309,6 +3309,7 @@ theme Appearance Udseende
|
||||
Delete Swatch Slet farveprøve
|
||||
Duplicate Swatch Duplikér farveprøve
|
||||
Merge Swatches Flet farveprøver
|
||||
Move Brush Flyt pensel
|
||||
Move Swatch Flyt farveprøve
|
||||
New Color Group Ny farvegruppe
|
||||
New Swatch Ny farveprøve
|
||||
@@ -4029,6 +4030,7 @@ imageTrace Outline Kontur
|
||||
@msg no bounds ingen grænser
|
||||
@msg no brush named `{name}` ingen pensel med navnet `{name}`
|
||||
@msg no brush selected ingen pensel er markeret
|
||||
@msg brushes move only within their type pensler kan kun flyttes inden for deres egen type
|
||||
@msg no catalog intet katalog
|
||||
@msg no changes since the last save ingen ændringer siden sidste gemning
|
||||
@msg no clock to keep a heartbeat by intet ur at holde hjerteslag efter
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1883,6 +1883,7 @@ file-filter Templates Vorlagen
|
||||
Move Slices Slices verschieben
|
||||
Move Spine Point Achsenpunkt verschieben
|
||||
Reshape Spine Segment Achsensegment umformen
|
||||
Move Brush Pinsel verschieben
|
||||
Move Swatch Farbfeld verschieben
|
||||
Move Up Nach oben verschieben
|
||||
Move… Verschieben…
|
||||
@@ -3944,6 +3945,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg no bounds keine Begrenzung
|
||||
@msg no brush named `{name}` kein Pinsel namens `{name}`
|
||||
@msg no brush selected kein Pinsel ausgewählt
|
||||
@msg brushes move only within their type Pinsel lassen sich nur innerhalb ihrer Art verschieben
|
||||
@msg this kind of brush has no options dialog yet für diese Pinselart gibt es noch kein Optionsdialogfeld
|
||||
@msg no catalog kein Katalog
|
||||
@msg no changes since the last save keine Änderungen seit dem letzten Speichern
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2581,6 +2581,7 @@ theme Appearance Εμφάνιση
|
||||
Move Slices Μετακίνηση τομών
|
||||
Move Spine Point Μετακίνηση σημείου κεντρικής καμπύλης
|
||||
Reshape Spine Segment Αναδιαμόρφωση τμήματος κεντρικής καμπύλης
|
||||
Move Brush Μετακίνηση πινέλου
|
||||
Move Swatch Μετακίνηση δείγματος χρώματος
|
||||
Move Up Μετακίνηση επάνω
|
||||
My Themes Τα θέματά μου
|
||||
@@ -3974,6 +3975,7 @@ theme Appearance Εμφάνιση
|
||||
@msg no bounds δεν υπάρχουν όρια
|
||||
@msg no brush named `{name}` δεν υπάρχει πινέλο με όνομα `{name}`
|
||||
@msg no brush selected δεν έχει επιλεγεί πινέλο
|
||||
@msg brushes move only within their type τα πινέλα μετακινούνται μόνο μέσα στον τύπο τους
|
||||
@msg no catalog δεν υπάρχει κατάλογος
|
||||
@msg no changes since the last save δεν υπάρχουν αλλαγές μετά την τελευταία αποθήκευση
|
||||
@msg no clock to keep a heartbeat by δεν υπάρχει ρολόι για τον παλμό διατήρησης σύνδεσης
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2021 and column 19.
|
@@ -1874,6 +1874,7 @@
|
||||
Move Slices Mover sectores
|
||||
Move Spine Point Mover punto del eje
|
||||
Reshape Spine Segment Remodelar segmento del eje
|
||||
Move Brush Mover pincel
|
||||
Move Swatch Mover muestra
|
||||
Move Up Subir
|
||||
Move… Mover…
|
||||
@@ -4006,6 +4007,7 @@ axis Both Ambos
|
||||
@msg no bounds sin límites
|
||||
@msg no brush named `{name}` no hay ningún pincel llamado `{name}`
|
||||
@msg no brush selected no hay ningún pincel seleccionado
|
||||
@msg brushes move only within their type los pinceles solo se pueden mover dentro de su tipo
|
||||
@msg no catalog no hay catálogo
|
||||
@msg no changes since the last save no hay cambios desde la última vez que se guardó
|
||||
@msg no clock to keep a heartbeat by no hay reloj para mantener la señal de actividad
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3486,6 +3486,7 @@ theme Appearance Ulkoasu
|
||||
Delete Swatch Poista värimalli
|
||||
Duplicate Swatch Monista värimalli
|
||||
Merge Swatches Yhdistä värimallit
|
||||
Move Brush Siirrä sivellintä
|
||||
Move Swatch Siirrä värimallia
|
||||
New Color Group Uusi väriryhmä
|
||||
New Swatch Uusi värimalli
|
||||
@@ -4223,6 +4224,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg no bounds ei rajoja
|
||||
@msg no brush named `{name}` ei sivellintä nimeltä `{name}`
|
||||
@msg no brush selected sivellintä ei ole valittu
|
||||
@msg brushes move only within their type siveltimiä voi siirtää vain oman tyyppinsä sisällä
|
||||
@msg no catalog ei luetteloa
|
||||
@msg no changes since the last save ei muutoksia viimeisen tallennuksen jälkeen
|
||||
@msg no clock to keep a heartbeat by ei kelloa sykkeen ylläpitoon
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1873,6 +1873,7 @@
|
||||
Move Slices Déplacer les tranches
|
||||
Move Spine Point Déplacer le point de la dorsale
|
||||
Reshape Spine Segment Modeler le segment de la dorsale
|
||||
Move Brush Déplacer la forme
|
||||
Move Swatch Déplacer la nuance
|
||||
Move Up Monter
|
||||
Move… Déplacer…
|
||||
@@ -4003,6 +4004,7 @@ axis Both Les deux
|
||||
@msg no bounds aucune limite
|
||||
@msg no brush named `{name}` aucune forme nommée `{name}`
|
||||
@msg no brush selected aucune forme sélectionnée
|
||||
@msg brushes move only within their type les formes ne se déplacent qu’à l’intérieur de leur type
|
||||
@msg no catalog aucun catalogue
|
||||
@msg no changes since the last save aucune modification depuis le dernier enregistrement
|
||||
@msg no clock to keep a heartbeat by aucune horloge pour entretenir le signal d’activité
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2366,6 +2366,7 @@ file-filter Templates Sablonok
|
||||
Move Rows Sorok áthelyezése
|
||||
Move Slices Szeletek áthelyezése
|
||||
Move Spine Point Gerincpont áthelyezése
|
||||
Move Brush Ecset áthelyezése
|
||||
Move Swatch Színminta áthelyezése
|
||||
Move to Group Áthelyezés csoportba
|
||||
Move Up Feljebb
|
||||
@@ -4643,6 +4644,7 @@ imageTrace Outline Körvonal
|
||||
@msg no bounds nincsenek határok
|
||||
@msg no brush named `{name}` nincs ilyen nevű ecset: `{name}`
|
||||
@msg no brush selected nincs kijelölt ecset
|
||||
@msg brushes move only within their type az ecsetek csak a saját típusukon belül helyezhetők át
|
||||
@msg no catalog nincs katalógus
|
||||
@msg no changes since the last save a legutóbbi mentés óta nincs módosítás
|
||||
@msg no clock to keep a heartbeat by nincs óra az életjel időzítéséhez
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3309,6 +3309,7 @@ theme Appearance Tampilan
|
||||
Delete Swatch Hapus sampel
|
||||
Duplicate Swatch Duplikatkan sampel
|
||||
Merge Swatches Gabungkan sampel
|
||||
Move Brush Pindahkan kuas
|
||||
Move Swatch Pindahkan sampel
|
||||
New Color Group Grup warna baru
|
||||
New Swatch Sampel baru
|
||||
@@ -4029,6 +4030,7 @@ imageTrace Outline Kontur
|
||||
@msg no bounds tidak ada batas
|
||||
@msg no brush named `{name}` tidak ada kuas bernama `{name}`
|
||||
@msg no brush selected tidak ada kuas yang dipilih
|
||||
@msg brushes move only within their type kuas hanya dapat dipindahkan di dalam jenisnya sendiri
|
||||
@msg no catalog tidak ada katalog
|
||||
@msg no changes since the last save tidak ada perubahan sejak penyimpanan terakhir
|
||||
@msg no clock to keep a heartbeat by tidak ada jam untuk menjaga detak
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3782 and column 19.
|
@@ -1901,6 +1901,7 @@ file-filter Templates Modelli
|
||||
Move Slices Sposta sezioni
|
||||
Move Spine Point Sposta punto dorsale
|
||||
Reshape Spine Segment Rimodella segmento della dorsale
|
||||
Move Brush Sposta pennello
|
||||
Move Swatch Sposta campione
|
||||
Move Up Sposta su
|
||||
Move… Sposta…
|
||||
@@ -4046,6 +4047,7 @@ axis Both Entrambi
|
||||
@msg no bounds nessun limite
|
||||
@msg no brush named `{name}` nessun pennello denominato `{name}`
|
||||
@msg no brush selected nessun pennello selezionato
|
||||
@msg brushes move only within their type i pennelli si possono spostare solo all'interno del loro tipo
|
||||
@msg no catalog nessun catalogo
|
||||
@msg no changes since the last save nessuna modifica dall'ultimo salvataggio
|
||||
@msg no clock to keep a heartbeat by nessun orologio con cui mantenere il segnale di attività
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2023,6 +2023,7 @@
|
||||
Move Slices スライスを移動
|
||||
Move Spine Point スパインポイントを移動
|
||||
Reshape Spine Segment スパインセグメントをシェイプ変形
|
||||
Move Brush ブラシを移動
|
||||
Move Swatch スウォッチを移動
|
||||
Move Up 上へ移動
|
||||
Move in Perspective 遠近で移動
|
||||
@@ -3862,6 +3863,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg Expand: no brush strokes selected 拡張: ブラシストロークが選択されていません
|
||||
@msg no brush named `{name}` `{name}` という名前のブラシが見つかりません
|
||||
@msg no brush selected ブラシが選択されていません
|
||||
@msg brushes move only within their type ブラシは同じ種類の中でのみ移動できます
|
||||
@msg no symbol instances selected シンボルインスタンスが選択されていません
|
||||
@msg select a symbol instance シンボルインスタンスを選択してください
|
||||
@msg the art has no bounds アートに境界がありません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3113,6 +3113,7 @@ theme Appearance იერსახე
|
||||
Delete Swatch ფერის ნიმუშის წაშლა
|
||||
Duplicate Swatch ფერის ნიმუშის დუბლირება
|
||||
Merge Swatches ფერის ნიმუშების შერწყმა
|
||||
Move Brush ფუნჯის გადატანა
|
||||
Move Swatch ფერის ნიმუშის გადატანა
|
||||
New Color Group ფერების ახალი ჯგუფი
|
||||
New Swatch ფერის ახალი ნიმუში
|
||||
@@ -3787,6 +3788,7 @@ effect Regular ჩვეულებრივი
|
||||
@msg no bounds საზღვრები არ არის
|
||||
@msg no brush named `{name}` ფუნჯი სახელით `{name}` არ არის
|
||||
@msg no brush selected ფუნჯი მონიშნული არ არის
|
||||
@msg brushes move only within their type ფუნჯების გადატანა მხოლოდ საკუთარი ტიპის ფარგლებში შეიძლება
|
||||
@msg no catalog კატალოგი არ არის
|
||||
@msg no changes since the last save ბოლო შენახვის შემდეგ ცვლილებები არ არის
|
||||
@msg no clock to keep a heartbeat by პულსის შესანარჩუნებელი საათი არ არის
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2018,6 +2018,7 @@
|
||||
Move Slices 슬라이스 이동
|
||||
Move Spine Point 스파인 포인트 이동
|
||||
Reshape Spine Segment 스파인 세그먼트 모양 변경
|
||||
Move Brush 브러시 이동
|
||||
Move Swatch 견본 이동
|
||||
Move Up 위로 이동
|
||||
Move in Perspective 원근 이동
|
||||
@@ -3794,6 +3795,7 @@ theme Appearance 외관
|
||||
@msg Expand: no brush strokes selected 확장: 선택한 브러시 선이 없습니다
|
||||
@msg no brush named `{name}` `{name}` 브러시가 없습니다
|
||||
@msg no brush selected 선택한 브러시가 없습니다
|
||||
@msg brushes move only within their type 브러시는 같은 유형 안에서만 이동할 수 있습니다
|
||||
@msg no symbol instances selected 선택한 심볼 인스턴스가 없습니다
|
||||
@msg select a symbol instance 심볼 인스턴스를 선택하세요
|
||||
@msg the art has no bounds 아트웍에 경계가 없습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3323,6 +3323,7 @@ theme Appearance Vormgeving
|
||||
Delete Swatch Kleurstaal verwijderen
|
||||
Duplicate Swatch Kleurstaal dupliceren
|
||||
Merge Swatches Kleurstalen samenvoegen
|
||||
Move Brush Kwast verplaatsen
|
||||
Move Swatch Kleurstaal verplaatsen
|
||||
New Color Group Nieuwe kleurgroep
|
||||
New Swatch Nieuw kleurstaal
|
||||
@@ -4041,6 +4042,7 @@ imageTrace Outline Contour
|
||||
@msg no bounds geen grenzen
|
||||
@msg no brush named `{name}` geen penseel met de naam `{name}`
|
||||
@msg no brush selected geen penseel geselecteerd
|
||||
@msg brushes move only within their type penselen kunnen alleen binnen hun eigen type worden verplaatst
|
||||
@msg no catalog geen catalogus
|
||||
@msg no changes since the last save geen wijzigingen sinds de laatste keer opslaan
|
||||
@msg no clock to keep a heartbeat by geen klok om een hartslag bij te houden
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3416,6 +3416,7 @@ theme Appearance Wygląd
|
||||
Delete Swatch Usuń próbkę
|
||||
Duplicate Swatch Powiel próbkę
|
||||
Merge Swatches Scal próbki
|
||||
Move Brush Przesuń pędzel
|
||||
Move Swatch Przesuń próbkę
|
||||
New Color Group Nowa grupa kolorów
|
||||
New Swatch Nowa próbka
|
||||
@@ -4130,6 +4131,7 @@ imageTrace Outline Kontur
|
||||
@msg no bounds brak granic
|
||||
@msg no brush named `{name}` brak pędzla o nazwie `{name}`
|
||||
@msg no brush selected nie zaznaczono pędzla
|
||||
@msg brushes move only within their type pędzle można przesuwać tylko w obrębie ich typu
|
||||
@msg no catalog brak katalogu
|
||||
@msg no changes since the last save brak zmian od ostatniego zapisu
|
||||
@msg no clock to keep a heartbeat by brak zegara do podtrzymywania sygnału aktywności
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1572,6 +1572,7 @@
|
||||
Move Slices Mover Fatias
|
||||
Move Spine Point Mover Ponto de Coluna
|
||||
Reshape Spine Segment Remodelar Segmento da Coluna
|
||||
Move Brush Mover Pincel
|
||||
Move Swatch Mover Amostra
|
||||
Move Up Mover para Cima
|
||||
Move in Perspective Mover em Perspectiva
|
||||
@@ -3586,6 +3587,7 @@ axis Both Ambos
|
||||
@msg no bounds sem limites
|
||||
@msg no brush named `{name}` nenhum pincel chamado `{name}`
|
||||
@msg no brush selected nenhum pincel selecionado
|
||||
@msg brushes move only within their type os pincéis só podem ser movidos dentro do seu tipo
|
||||
@msg no catalog sem catálogo
|
||||
@msg no changes since the last save nenhuma alteração desde o último salvamento
|
||||
@msg no clock to keep a heartbeat by sem relógio para manter o sinal de atividade
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1899,6 +1899,7 @@ file-filter Templates Шаблоны
|
||||
Move Slices Переместить фрагменты
|
||||
Move Spine Point Переместить точку остова
|
||||
Reshape Spine Segment Изменить форму сегмента остова
|
||||
Move Brush Переместить кисть
|
||||
Move Swatch Переместить образец
|
||||
Move Up Переместить вверх
|
||||
Move… Переместить…
|
||||
@@ -4042,6 +4043,7 @@ axis Both Оба
|
||||
@msg no bounds нет границ
|
||||
@msg no brush named `{name}` нет кисти с именем `{name}`
|
||||
@msg no brush selected кисть не выбрана
|
||||
@msg brushes move only within their type кисти можно перемещать только в пределах их типа
|
||||
@msg no catalog нет каталога
|
||||
@msg no changes since the last save нет изменений с момента последнего сохранения
|
||||
@msg no clock to keep a heartbeat by нет часов для поддержания сигнала
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1826,6 +1826,7 @@
|
||||
Move Slices Flytta skivor
|
||||
Move Spine Point Flytta ryggradspunkt
|
||||
Reshape Spine Segment Omforma ryggradssegment
|
||||
Move Brush Flytta pensel
|
||||
Move Swatch Flytta färgruta
|
||||
Move Up Flytta upp
|
||||
Move… Flytta…
|
||||
@@ -3923,6 +3924,7 @@ axis Both Båda
|
||||
@msg no bounds inga gränser
|
||||
@msg no brush named `{name}` ingen pensel med namnet `{name}`
|
||||
@msg no brush selected ingen pensel är markerad
|
||||
@msg brushes move only within their type penslar kan bara flyttas inom sin egen typ
|
||||
@msg no catalog ingen katalog
|
||||
@msg no changes since the last save inga ändringar sedan senaste sparningen
|
||||
@msg no clock to keep a heartbeat by ingen klocka att styra hjärtslagen efter
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1696,6 +1696,22 @@ fn align_reflect_and_shear_param_errors_are_translated() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The error of `brush.move` for a brush dragged onto a brush of another type in the Brushes panel,
|
||||
/// whose text the scan above doesn't read, is translated whole in each language of
|
||||
/// [`COMPLETE_MESSAGES`] and in Bulgarian, Danish, Indonesian and Dutch.
|
||||
#[test]
|
||||
fn brush_move_param_error_is_translated() {
|
||||
use serde_json::json;
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let e = s.execute("brush.move", &json!({"name": "3 pt. Round", "type": "scatter"})).unwrap_err();
|
||||
let vectorcraft_engine::EngineError::BadParams { msg, .. } = &e else { panic!("{e}") };
|
||||
for code in COMPLETE_MESSAGES.iter().chain(&["bg", "da", "id", "nl"]) {
|
||||
let shown = message(Lang::from_code(code).unwrap(), &e.to_string());
|
||||
assert!(!shown.contains(msg.as_str()) && !shown.contains("invalid parameters") && shown.contains("`brush.move`"), "{code}: {shown}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The warning that a data set with no value for a bound variable gives, which the status bar
|
||||
/// shows and the scan above doesn't read, is translated in each language of [`COMPLETE_MESSAGES`],
|
||||
/// with the data set's name kept.
|
||||
|
||||
@@ -1895,6 +1895,7 @@ file-filter Templates Шаблони
|
||||
Move Slices Перемістити фрагменти
|
||||
Move Spine Point Перемістити точку осі
|
||||
Reshape Spine Segment Змінити форму сегмента осі
|
||||
Move Brush Перемістити пензель
|
||||
Move Swatch Перемістити зразок
|
||||
Move Up Перемістити вгору
|
||||
Move… Перемістити…
|
||||
@@ -4030,6 +4031,7 @@ axis Both Обидві
|
||||
@msg no bounds немає кордонів
|
||||
@msg no brush named `{name}` немає пензля з ім’ям `{name}`
|
||||
@msg no brush selected пензель не вибрано
|
||||
@msg brushes move only within their type пензлі можна переміщувати лише в межах їхнього типу
|
||||
@msg no catalog немає каталогу
|
||||
@msg no changes since the last save немає змін з моменту останнього збереження
|
||||
@msg no clock to keep a heartbeat by немає годинника для підтримки сигналу
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 726 and column 40.
|
@@ -2176,6 +2176,7 @@ file-filter Templates Mẫu
|
||||
Move Rows Di chuyển hàng
|
||||
Move Slices Di chuyển lát cắt
|
||||
Move Spine Point Di chuyển điểm trục
|
||||
Move Brush Di chuyển cọ
|
||||
Move Swatch Di chuyển mẫu màu
|
||||
Move to Group Di chuyển vào nhóm
|
||||
Move Up Di chuyển lên
|
||||
@@ -4290,6 +4291,7 @@ theme Appearance Giao diện
|
||||
@msg no bounds không có giới hạn
|
||||
@msg no brush named `{name}` không có cọ tên `{name}`
|
||||
@msg no brush selected chưa chọn cọ
|
||||
@msg brushes move only within their type chỉ có thể di chuyển cọ trong cùng loại của nó
|
||||
@msg no catalog không có danh mục
|
||||
@msg no changes since the last save không có thay đổi nào kể từ lần lưu cuối
|
||||
@msg no clock to keep a heartbeat by không có đồng hồ để duy trì nhịp tim
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3982 and column 10.
|
@@ -1794,6 +1794,7 @@ file-filter Templates 模板
|
||||
@msg x and y must be finite x 和 y 必须是有限值
|
||||
@msg t, dx and dy must be finite t、dx 和 dy 必须是有限值
|
||||
@msg handle must be in or out handle 必须是 in 或 out
|
||||
Move Brush 移动画笔
|
||||
Move Swatch 移动色板
|
||||
Move Up 上移
|
||||
Move… 移动…
|
||||
@@ -3619,6 +3620,7 @@ imageTrace Outline 轮廓
|
||||
@msg Expand: no brush strokes selected 扩展:未选择画笔描边
|
||||
@msg no brush named `{name}` 没有名为 `{name}` 的画笔
|
||||
@msg no brush selected 未选择画笔
|
||||
@msg brushes move only within their type 画笔只能在同一类型内移动
|
||||
@msg no symbol instances selected 未选择符号实例
|
||||
@msg select a symbol instance 请选择一个符号实例
|
||||
@msg the art has no bounds 图稿没有边界
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1622,6 +1622,7 @@ file-filter Templates 範本
|
||||
@msg Expand: no brush strokes selected 展開:未選取筆刷筆畫
|
||||
@msg no brush named `{name}` 沒有名為 `{name}` 的筆刷
|
||||
@msg no brush selected 未選取筆刷
|
||||
@msg brushes move only within their type 筆刷只能在同一類型內移動
|
||||
@msg no symbol instances selected 未選取符號實體
|
||||
@msg select a symbol instance 請選取符號實體
|
||||
@msg the art has no bounds 圖稿沒有邊界
|
||||
@@ -2641,6 +2642,7 @@ file-filter Templates 範本
|
||||
@msg x and y must be finite x 和 y 必須是有限值
|
||||
@msg t, dx and dy must be finite t、dx 和 dy 必須是有限值
|
||||
@msg handle must be in or out handle 必須是 in 或 out
|
||||
Move Brush 移動筆刷
|
||||
Move Swatch 移動色票
|
||||
Move Up 上移
|
||||
Move… 移動…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -122,6 +122,43 @@ fn shown<'a>(list: &'a [(String, String)], hidden: &[String]) -> Vec<&'a str> {
|
||||
kinds.flat_map(|(ty, _)| list.iter().filter(move |b| b.1 == *ty).map(|b| b.0.as_str())).collect()
|
||||
}
|
||||
|
||||
/// A brush dragged over the tile or row `resp` of brush `name`, the `i`th brush of type `ty`
|
||||
/// (`list`: rows stack downwards), lands before or after it by the pointer's half; a bar marks the
|
||||
/// slot when the dragged brush is of type `ty` too (`types`: the (name, type) of every brush).
|
||||
/// Released there, it sets `moved` to the `brush.move` params, which refuse a brush of another type.
|
||||
fn reorder_drop(
|
||||
ui: &Ui,
|
||||
resp: &egui::Response,
|
||||
types: &[(String, String)],
|
||||
(name, ty, i): (&str, &str, usize),
|
||||
list: bool,
|
||||
moved: &mut Option<Value>,
|
||||
) {
|
||||
let Some(drag) = resp.dnd_hover_payload::<PanelDrag>() else { return };
|
||||
let PanelDrag::Brush { name: moving, .. } = &*drag else { return };
|
||||
if moving == name {
|
||||
// Dropped on itself: it stays where it is.
|
||||
resp.dnd_release_payload::<PanelDrag>();
|
||||
return;
|
||||
}
|
||||
let r = resp.rect;
|
||||
let at = ui.input(|i| i.pointer.interact_pos()).unwrap_or(r.center());
|
||||
let after = if list { at.y > r.center().y } else { at.x > r.center().x };
|
||||
if types.iter().any(|b| &b.0 == moving && b.1 == ty) {
|
||||
let accent = egui::Stroke::new(2.0, Tokens::get(ui.ctx()).accent);
|
||||
if list {
|
||||
let y = if after { r.bottom() } else { r.top() };
|
||||
ui.painter().line_segment([egui::pos2(r.left(), y), egui::pos2(r.right(), y)], accent);
|
||||
} else {
|
||||
let x = if after { r.right() + 1.5 } else { r.left() - 1.5 };
|
||||
ui.painter().line_segment([egui::pos2(x, r.top()), egui::pos2(x, r.bottom())], accent);
|
||||
}
|
||||
}
|
||||
if resp.dnd_release_payload::<PanelDrag>().is_some() {
|
||||
*moved = Some(json!({ "name": moving, "to": i + usize::from(after), "type": ty }));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (list, current) = brushes(app);
|
||||
@@ -133,6 +170,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
list.iter().filter_map(|(n, _)| app.run("brush.get", json!({"name": n})).ok().map(|v| (n.clone(), v))).collect();
|
||||
let mut clicked: Option<(String, egui::Modifiers)> = None;
|
||||
let mut options: Option<String> = None;
|
||||
let mut moved: Option<Value> = None;
|
||||
let list_rect = widgets::list_box(ui, |ui| {
|
||||
ui.set_min_height(110.0);
|
||||
ui.set_width(ui.available_width());
|
||||
@@ -190,13 +228,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
};
|
||||
if list_view {
|
||||
for (name, _) in group {
|
||||
pick(row(ui, name, vec2(ui.available_width(), 26.0), true), name);
|
||||
for (i, (name, _)) in group.into_iter().enumerate() {
|
||||
let resp = row(ui, name, vec2(ui.available_width(), 26.0), true);
|
||||
reorder_drop(ui, &resp, &list, (name, ty, i), true, &mut moved);
|
||||
pick(resp, name);
|
||||
}
|
||||
} else {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (name, _) in group {
|
||||
pick(row(ui, name, vec2(64.0, 28.0), false), name);
|
||||
for (i, (name, _)) in group.into_iter().enumerate() {
|
||||
let resp = row(ui, name, vec2(64.0, 28.0), false);
|
||||
reorder_drop(ui, &resp, &list, (name, ty, i), false, &mut moved);
|
||||
pick(resp, name);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -211,6 +253,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
// A brush dragged within the list moves among the brushes of its type.
|
||||
if let Some(p) = moved
|
||||
&& let Err(e) = app.run("brush.move", p)
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
let highlighted = sel_brush.clone().or(current.clone());
|
||||
match clicked {
|
||||
// Shift and Cmd/Ctrl only select, as in the Swatches panel: they start from the
|
||||
@@ -636,4 +684,54 @@ mod tests {
|
||||
let drag = egui::DragAndDrop::payload::<PanelDrag>(&ctx);
|
||||
assert!(matches!(drag.as_deref(), Some(PanelDrag::Brush { name, def }) if *name == after[0].0 && def["name"] == json!(name)), "{drag:?}");
|
||||
}
|
||||
|
||||
/// A brush dragged onto the left half of another brush of its type goes before it, with a bar
|
||||
/// marking the slot while it is held there, as one undo step; dropped on a brush of another
|
||||
/// type, it stays where it is and the status bar says why.
|
||||
#[test]
|
||||
fn a_brush_dragged_onto_another_of_its_type_moves_before_it() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
let out = frame_with(&ctx, &mut app, vec![], egui::Modifiers::NONE, 0.0, show);
|
||||
let tiles: Vec<Rect> = clickable(&out).into_iter().map(|c| c.1).filter(|r| r.width() == 64.0).collect();
|
||||
let (list, _) = brushes(&mut app);
|
||||
let order: Vec<String> = shown(&list, &[]).into_iter().map(str::to_string).collect();
|
||||
assert_eq!(order[..4], ["3 pt. Round", "6 pt. Flat", "10 pt. Oval", "Dots"]);
|
||||
let accent = Tokens::get(&ctx).accent;
|
||||
let bars = |out: &egui::FullOutput| -> Vec<f32> {
|
||||
let lines = out.shapes.iter().filter_map(|c| match &c.shape {
|
||||
egui::Shape::LineSegment { points, stroke } if stroke.color == accent => Some(points[0].x),
|
||||
_ => None,
|
||||
});
|
||||
lines.collect()
|
||||
};
|
||||
// Press on a tile, drag it to `to`, hold it there for a frame (its output returned) and release.
|
||||
let drag = |app: &mut VectorcraftApp, from: Pos2, to: Pos2, time: f64| -> egui::FullOutput {
|
||||
frame_with(&ctx, app, vec![egui::Event::PointerMoved(from), button(from, true)], egui::Modifiers::NONE, time, show);
|
||||
frame_with(&ctx, app, vec![egui::Event::PointerMoved(from + vec2(20.0, 0.0))], egui::Modifiers::NONE, time + 0.1, show);
|
||||
frame_with(&ctx, app, vec![egui::Event::PointerMoved(to)], egui::Modifiers::NONE, time + 0.2, show);
|
||||
let held = frame_with(&ctx, app, vec![], egui::Modifiers::NONE, time + 0.3, show);
|
||||
frame_with(&ctx, app, vec![button(to, false)], egui::Modifiers::NONE, time + 0.4, show);
|
||||
held
|
||||
};
|
||||
let undo = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
|
||||
let steps = undo(&app);
|
||||
let calligraphic =
|
||||
|app: &mut VectorcraftApp| -> Vec<String> { brushes(app).0.into_iter().filter(|b| b.1 == "calligraphic").map(|b| b.0).collect() };
|
||||
// "10 pt. Oval" onto the left half of "3 pt. Round".
|
||||
let held = drag(&mut app, tiles[2].center(), tiles[0].center() - vec2(16.0, 0.0), 1.0);
|
||||
assert_eq!(bars(&held), [tiles[0].left() - 1.5]);
|
||||
assert_eq!(calligraphic(&mut app), ["10 pt. Oval", "3 pt. Round", "6 pt. Flat"]);
|
||||
assert_eq!(undo(&app), steps + 1);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(calligraphic(&mut app), ["3 pt. Round", "6 pt. Flat", "10 pt. Oval"]);
|
||||
// "3 pt. Round" onto "Dots", a scatter brush: no bar, and nothing moves.
|
||||
let held = drag(&mut app, tiles[0].center(), tiles[3].center(), 3.0);
|
||||
assert!(bars(&held).is_empty(), "{:?}", bars(&held));
|
||||
assert_eq!(brushes(&mut app).0, list);
|
||||
assert_eq!(undo(&app), steps);
|
||||
assert!(app.ui.status.contains("brushes move only within their type"), "{}", app.ui.status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,11 @@ in the app runs `paint.setFill` (or `paint.setStroke`, the active proxy) with `i
|
||||
The Swatches panel menu's commands: `swatch.addUsedColors {selection?, global?}`, `swatch.unused` (a query: the
|
||||
names Select All Unused selects), `swatch.merge {names}` (the first is kept), `swatch.ungroup {name}` and
|
||||
`swatch.sortByKind`.
|
||||
`brush.move {name | names, to?, type?}` reorders brushes among the brushes of their type, as dragging one onto
|
||||
another of its type in the Brushes panel does. `to` is an index among the brushes of that type in `brush.list`
|
||||
order: the brushes go before the first brush of that type at or after `to` that is not moving, and after the last
|
||||
brush of that type when there is none or `to` is left out. A move that keeps the order records no undo step.
|
||||
Brushes of different types, or a `type` other than theirs, are refused with "brushes move only within their type".
|
||||
Document colour mode: `file.new {colorMode: "cmyk"}` starts a CMYK document with CMYK default swatches, and RGB
|
||||
colors applied to it (`paint.setFill`/`setStroke` colors, gradient stops and freeform points, `paint.editGradient`
|
||||
stops, `paint.freeform.addPoint`/`setPoint`, `paint.sampleColor`, `object.mesh.setPointColor`/`addLine`, the
|
||||
|
||||
@@ -588,6 +588,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
A Space+Shift drag rotates the view whatever the tool, as a Rotate View tool drag does, and Shift does not round the angle to 15°.
|
||||
- **Swatches panel clicks:** Ctrl/Cmd-clicking New Swatch makes a spot color, and Ctrl/Cmd+Shift-clicking it makes a global process color.
|
||||
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, onto the Swatches panel a pattern swatch and onto the Brushes panel an Art brush; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it.
|
||||
In the Brushes panel, a brush dragged onto another brush of its type goes before or after it, by the half of the tile or row the pointer is over, and a bar marks the slot while the brush is held there (`brush.move`); the move is one undo step. Brushes move only within their type, as the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page states ("You can move brushes only within their type"): a brush dropped on a brush of another type stays where it is, and the status bar shows "brushes move only within their type".
|
||||
In the Swatches panel, Alt-dragging a swatch onto another swatch of its kind (not a tint swatch) replaces that swatch's color, gradient or pattern, and the swatch keeps its name and place.
|
||||
- **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, the Selection, Direct Selection and Group Selection tools the Move dialog (Enter opens these seven tools' dialogs too; Move opens on the offset of the last drag or Move and with the checkboxes it was last confirmed with, so Enter, Enter repeats a move or a copy at the same spacing, as [an Adobe Community thread](https://community.adobe.com/questions-652/move-tool-771221) describes), Gradient, Eyedropper, Blend and the Liquify tools their panels or options, the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools their Tool Options (Fidelity, fill, Keep selected, the tolerances, the brush size), and (#812) the Flare tool its Flare Tool Options, the Artboard tool the active artboard's Artboard Options, the graph tools Graph Type for the selected graph, the Symbolism tools the Symbolism Tools Options (Diameter, Intensity, Symbol Set Density) and the Magic Wand tool its panel.
|
||||
In the Pencil and Paintbrush Tool Options, Edit selected paths within 0 px turns editing off, so a stroke that starts exactly on an end of a selected open path draws a new path; Close paths when ends are within 0 px turns closing off, so a stroke that ends exactly where it started stays open.
|
||||
|
||||
Reference in New Issue
Block a user