Symbols: Symbol Options › Enable Guides for 9-Slice Scaling (#2342)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-17 16:38:41 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 276eeacae6
commit 3eed742639
29 changed files with 266 additions and 26 deletions
+33
View File
@@ -22,10 +22,12 @@ mod inline;
pub mod intertwine;
pub mod shared;
mod symbol_export_type;
mod symbol_nine_slice;
mod symbol_registration;
pub use inline::{SYMBOL_HALF, SYMBOL_SIZES};
pub use shared::Shared;
pub use symbol_export_type::{SYMBOL_EXPORT_TYPE, SymbolExportType};
pub use symbol_nine_slice::{SYMBOL_NINE_SLICE, SymbolNineSlice};
pub use symbol_registration::{SYMBOL_REGISTRATION, SymbolRegistration};
pub mod links;
pub mod live;
@@ -1688,6 +1690,37 @@ impl Document {
self.clear_symbol_export_type(from);
self.set_symbol_export_type(to, kind);
}
/// Symbol Options › 9-slice guides for `name`: `None` when 9-slice scaling is off (default).
pub fn symbol_nine_slice(&self, name: &str) -> Option<SymbolNineSlice> {
self.unknown.get(SYMBOL_NINE_SLICE).and_then(|m| m.get(name)).and_then(SymbolNineSlice::from_value)
}
/// Turn 9-slice scaling on with `guides`, or off with `None`, for `name`.
pub fn set_symbol_nine_slice(&mut self, name: &str, guides: Option<SymbolNineSlice>) {
let Some(g) = guides else {
let Some(o) = self.unknown.get_mut(SYMBOL_NINE_SLICE).and_then(|m| m.as_object_mut()) else { return };
o.remove(name);
if o.is_empty() {
self.unknown.remove(SYMBOL_NINE_SLICE);
}
return;
};
let m = self.unknown.entry(SYMBOL_NINE_SLICE.to_string()).or_insert_with(|| serde_json::json!({}));
if let Some(o) = m.as_object_mut() {
o.insert(name.into(), g.to_value());
}
}
/// Move the stored 9-slice guides when a symbol is renamed.
pub fn rename_symbol_nine_slice(&mut self, from: &str, to: &str) {
if from == to {
return;
}
let g = self.symbol_nine_slice(from);
self.set_symbol_nine_slice(from, None);
self.set_symbol_nine_slice(to, g);
}
}
fn default_graphic_styles() -> Vec<GraphicStyle> {
+57
View File
@@ -0,0 +1,57 @@
//! Symbol Options › Enable Guides for 9-Slice Scaling: four guides that cut a symbol into a 3 × 3
//! grid so that, in Animate / Flash handoff, the corners keep their size while the edges and the
//! centre stretch
//! ([helpx Symbol Options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)).
//!
//! The guides are stored as fractions (0..1) of the symbol's natural size, measured from the left
//! and top edges and from the right and bottom edges, so they survive resizing the art. Like
//! [`crate::SymbolExportType`] this is handoff metadata: instances place the same on the canvas.
//! Stored under [`SYMBOL_NINE_SLICE`] keyed by symbol name; symbols without 9-slice have no entry.
use serde_json::{Value, json};
/// Document unknown key: `{ symbolName: [left, top, right, bottom] }`.
pub const SYMBOL_NINE_SLICE: &str = "symbolNineSlice";
/// The four 9-slice guides as fractions of the symbol's natural size.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SymbolNineSlice {
/// Left guide, from the left edge.
pub left: f64,
/// Top guide, from the top edge.
pub top: f64,
/// Right guide, from the right edge.
pub right: f64,
/// Bottom guide, from the bottom edge.
pub bottom: f64,
}
impl Default for SymbolNineSlice {
/// The centre third of each axis is the stretchy cell.
fn default() -> Self {
let t = 1.0 / 3.0;
Self { left: t, top: t, right: t, bottom: t }
}
}
impl SymbolNineSlice {
/// Guides from `[left, top, right, bottom]`; `None` unless each is finite, within 0..1, and the
/// two on each axis leave a gap between them.
pub fn new(left: f64, top: f64, right: f64, bottom: f64) -> Option<Self> {
let ok = |v: f64| v.is_finite() && (0.0..=1.0).contains(&v);
(ok(left) && ok(top) && ok(right) && ok(bottom) && left + right < 1.0 && top + bottom < 1.0).then_some(Self { left, top, right, bottom })
}
pub fn to_value(self) -> Value {
json!([self.left, self.top, self.right, self.bottom])
}
pub fn from_value(v: &Value) -> Option<Self> {
let a = v.as_array()?;
let n = |i: usize| a.get(i).and_then(Value::as_f64);
if a.len() != 4 {
return None;
}
Self::new(n(0)?, n(1)?, n(2)?, n(3)?)
}
}
+48 -7
View File
@@ -11,7 +11,7 @@ use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_brush::{self as brush, Brush};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol, SymbolExportType, SymbolRegistration};
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration};
use vectorcraft_geom::{Affine, Point, Vec2};
use super::edit::{outermost, selected_roots};
@@ -110,7 +110,7 @@ pub fn specs() -> Vec<CommandSpec> {
"New Symbol…",
[],
None,
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration), exportType?: movieClip (default)|graphic} make a symbol from the selection and replace it with an instance → {name, id, registration, exportType}",
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration), exportType?: movieClip (default)|graphic, nineSlice?: bool, nineSliceGuides?: array} make a symbol from the selection and replace it with an instance; nineSlice is Symbol Options › Enable Guides for 9-Slice Scaling, nineSliceGuides [left, top, right, bottom] as fractions 0..1 of the natural size (default a third each) → {name, id, registration, exportType, nineSlice}",
has_selection,
symbol_new
),
@@ -119,7 +119,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Symbol Options…",
[],
None,
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic} rename a symbol and/or set Symbol Options › Registration and Export Type (default name: current) → {name, registration, exportType}",
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic, nineSlice?: bool, nineSliceGuides?: array} rename a symbol and/or set Symbol Options › Registration and Export Type and 9-slice guides (nineSliceGuides [left, top, right, bottom] fractions 0..1; default name: current) → {name, registration, exportType, nineSlice}",
has_doc,
symbol_options
),
@@ -558,6 +558,33 @@ fn export_type_param(p: &Value, cmd: &str) -> Result<Option<SymbolExportType>> {
}
}
/// `nineSlice` (bool) and `nineSliceGuides` ([left, top, right, bottom] fractions of the natural
/// size): `None` leaves the symbol alone, `Some(None)` turns 9-slice off, `Some(Some(g))` turns it
/// on with guides `g` (the symbol's current ones, else the centre third, when none are given).
fn nine_slice_param(p: &Value, cmd: &str, current: Option<SymbolNineSlice>) -> Result<Option<Option<SymbolNineSlice>>> {
let on = match p.get("nineSlice") {
None | Some(Value::Null) => None,
Some(v) => Some(v.as_bool().ok_or_else(|| bad(cmd, "nineSlice must be true or false"))?),
};
let guides =
match p.get("nineSliceGuides") {
None | Some(Value::Null) => None,
Some(v) => Some(SymbolNineSlice::from_value(v).ok_or_else(|| {
bad(cmd, "nineSliceGuides must be [left, top, right, bottom], each 0..1, with left + right < 1 and top + bottom < 1")
})?),
};
Ok(match (on, guides) {
(Some(false), _) => Some(None),
(None, None) => None,
(_, Some(g)) => Some(Some(g)),
(Some(true), None) => Some(Some(current.unwrap_or_default())),
})
}
fn nine_slice_json(d: &Document, name: &str) -> Value {
d.symbol_nine_slice(name).map_or(Value::Null, SymbolNineSlice::to_value)
}
fn rename_natural_size(d: &mut Document, from: &str, to: &str) {
if from == to {
return;
@@ -614,6 +641,7 @@ fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
"size": [w, h],
"registration": d.symbol_registration(&sym.name).0,
"exportType": d.symbol_export_type(&sym.name).id(),
"nineSlice": nine_slice_json(d, &sym.name),
})
})
.collect();
@@ -644,6 +672,7 @@ fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
let name = s.doc()?.doc.unique_symbol_name(str_param(p, "name").filter(|n| !n.trim().is_empty()).unwrap_or("New Symbol"));
let reg = registration_param(p, C)?.unwrap_or(SymbolRegistration::CENTER);
let export = export_type_param(p, C)?.unwrap_or_default();
let nine = nine_slice_param(p, C, None)?.flatten();
// Keep the art where it was: the geometric centre stays at `c`, whatever the registration is.
let place = place_xf(c + reg.offset_from_center(size.0, size.1), size, reg);
let id = s.edit("New Symbol", |d, sel| {
@@ -652,12 +681,15 @@ fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
set_natural_size(d, &name, Some(size));
d.set_symbol_registration(&name, reg);
d.set_symbol_export_type(&name, export);
d.set_symbol_nine_slice(&name, nine);
d.unknown.insert(CURRENT_SYMBOL.into(), json!(name));
let id = replace_with_instance(d, &ids, &name, place)?;
sel.set([id]);
Ok(id)
})?;
Ok(json!({ "name": name, "id": id.0, "registration": reg.0, "exportType": export.id() }))
Ok(
json!({ "name": name, "id": id.0, "registration": reg.0, "exportType": export.id(), "nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value) }),
)
}
fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
@@ -665,9 +697,10 @@ fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
let name = resolve_symbol(&s.doc()?.doc, p, C)?;
let export = export_type_param(p, C)?;
let reg = registration_param(p, C)?;
let nine = nine_slice_param(p, C, s.doc()?.doc.symbol_nine_slice(&name))?;
let new_name = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty()).map(str::to_string);
if new_name.is_none() && export.is_none() && reg.is_none() {
return Err(bad(C, "pass newName, registration and/or exportType"));
if new_name.is_none() && export.is_none() && reg.is_none() && nine.is_none() {
return Err(bad(C, "pass newName, registration, exportType and/or nineSlice"));
}
if let Some(ref n) = new_name
&& n != &name
@@ -691,6 +724,7 @@ fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
rename_natural_size(d, &name, n);
d.rename_symbol_registration(&name, n);
d.rename_symbol_export_type(&name, n);
d.rename_symbol_nine_slice(&name, n);
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
d.unknown.insert(CURRENT_SYMBOL.into(), json!(n));
}
@@ -701,6 +735,9 @@ fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(kind) = export {
d.set_symbol_export_type(&final_name, kind);
}
if let Some(g) = nine {
d.set_symbol_nine_slice(&final_name, g);
}
Ok(())
})?;
let d = &s.doc()?.doc;
@@ -708,6 +745,7 @@ fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
"name": final_name,
"registration": d.symbol_registration(&final_name).0,
"exportType": d.symbol_export_type(&final_name).id(),
"nineSlice": nine_slice_json(d, &final_name),
}))
}
@@ -866,6 +904,7 @@ fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
set_natural_size(d, &name, None);
d.set_symbol_registration(&name, SymbolRegistration::CENTER);
d.clear_symbol_export_type(&name);
d.set_symbol_nine_slice(&name, None);
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
d.unknown.remove(CURRENT_SYMBOL);
}
@@ -883,15 +922,17 @@ fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let size = natural_size(d, &name);
let reg = d.symbol_registration(&name);
let export = d.symbol_export_type(&name);
let nine = d.symbol_nine_slice(&name);
s.edit("Duplicate Symbol", |d, _| {
let art = d.reid(&sym.art);
d.symbols.push(Symbol { name: new.clone(), art: Arc::new(art) });
set_natural_size(d, &new, Some(size));
d.set_symbol_registration(&new, reg);
d.set_symbol_export_type(&new, export);
d.set_symbol_nine_slice(&new, nine);
Ok(())
})?;
Ok(json!({ "name": new, "registration": reg.0, "exportType": export.id() }))
Ok(json!({ "name": new, "registration": reg.0, "exportType": export.id(), "nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value) }))
}
fn symbol_replace(s: &mut Session, p: &Value) -> Result<Value> {
+23
View File
@@ -702,3 +702,26 @@ fn symbol_registration_places_the_chosen_point_on_xy() {
assert!(close(bounds(&s, p), Rect::new(300.0, 200.0, 380.0, 240.0), 1e-9), "{:?}", bounds(&s, p));
assert!(s.execute("symbol.new", &json!({"registration": 9})).is_err());
}
/// Symbol Options › 9-slice guides: off by default, stored per symbol, follow rename/duplicate, validated.
#[test]
fn symbol_nine_slice_guides_round_trip() {
let mut s = session();
let (_, name, _) = make_symbol(&mut s);
assert!(s.execute("symbol.list", &json!({})).unwrap()["symbols"][0]["nineSlice"].is_null());
assert!(s.doc().unwrap().doc.unknown.get(vectorcraft_doc::SYMBOL_NINE_SLICE).is_none());
assert!(s.execute("symbol.options", &json!({"name": name, "nineSliceGuides": [0.6, 0.2, 0.6, 0.2]})).is_err());
assert!(s.execute("symbol.options", &json!({"name": name, "nineSlice": "yes"})).is_err());
let r = s.execute("symbol.options", &json!({"name": name, "nineSlice": true})).unwrap();
assert_eq!(r["nineSlice"].as_array().unwrap().len(), 4);
s.execute("symbol.options", &json!({"name": name, "nineSliceGuides": [0.25, 0.1, 0.25, 0.1]})).unwrap();
let copy = s.execute("symbol.duplicate", &json!({"name": name})).unwrap();
assert_eq!(copy["nineSlice"], json!([0.25, 0.1, 0.25, 0.1]));
s.execute("symbol.options", &json!({"name": name, "newName": "Panel"})).unwrap();
let g = s.doc().unwrap().doc.symbol_nine_slice("Panel").unwrap();
assert_eq!((g.left, g.top), (0.25, 0.1));
assert!(s.doc().unwrap().doc.symbol_nine_slice(&name).is_none());
s.execute("symbol.options", &json!({"name": "Panel", "nineSlice": false})).unwrap();
s.execute("symbol.delete", &json!({"name": "Box copy"})).unwrap();
assert!(s.doc().unwrap().doc.unknown.get(vectorcraft_doc::SYMBOL_NINE_SLICE).is_none());
}
+63 -14
View File
@@ -2,11 +2,12 @@
//! from the selection (`symbol.new`), or for the current symbol in the Symbols panel
//! (`symbol.options`).
//!
//! Fields: `name`, `exportType` (`movieClip` | `graphic`), `registration` (0..8, the 9-point grid;
//! Fields: `name`, `nineSlice` (bool) with `sliceLeft` / `sliceTop` / `sliceRight` / `sliceBottom`
//! (percent of the symbol's size), `exportType` (`movieClip` | `graphic`), `registration` (0..8, the 9-point grid;
//! 4 = centre), `__edit` (true when editing an existing symbol). Help:
//! <https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html>.
use serde_json::{Value, json};
use serde_json::{Map, Value, json};
use super::swatch_options::{grid, label};
use super::{DialogSpec, form};
@@ -17,6 +18,26 @@ use crate::widgets;
/// The dialog kind of Symbol Options.
pub const KIND: &str = "symbolOptions";
/// The 9-slice guide fields, in `[left, top, right, bottom]` order.
const SLICE_KEYS: [&str; 4] = ["sliceLeft", "sliceTop", "sliceRight", "sliceBottom"];
/// The 9-slice dialog fields for `guides` (percent of the symbol's size; a third each when off).
fn slice_fields(guides: Option<vectorcraft_doc::SymbolNineSlice>) -> Map<String, Value> {
let g = guides.unwrap_or_default();
let mut m = Map::new();
m.insert("nineSlice".into(), json!(guides.is_some()));
for (key, v) in SLICE_KEYS.iter().zip([g.left, g.top, g.right, g.bottom]) {
m.insert((*key).to_string(), json!((v * 100.0).round()));
}
m
}
/// The guides the dialog's fields describe, as the `nineSlice` / `nineSliceGuides` params.
fn slice_params(d: &Dialog) -> Value {
let [l, t, r, b] = SLICE_KEYS.map(|key| d.f64(key, 100.0 / 3.0) / 100.0);
if d.bool("nineSlice") { json!({ "nineSlice": true, "nineSliceGuides": [l, t, r, b] }) } else { json!({ "nineSlice": false }) }
}
/// Export Type choices: (value, label). Illustrator's default is Movie Clip.
const EXPORT_TYPES: [(&str, &str); 2] = [("movieClip", "Movie Clip"), ("graphic", "Graphic")];
@@ -29,7 +50,11 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
return Err("nothing selected".into());
}
let name = st.doc.unique_symbol_name("New Symbol");
app.ui.dialog = Some(Dialog::new(KIND, json!({ "name": name, "exportType": "movieClip", "registration": 4, "__edit": false })));
let mut fields = json!({ "name": name, "exportType": "movieClip", "registration": 4, "__edit": false });
if let Some(o) = fields.as_object_mut() {
o.extend(slice_fields(None));
}
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(Value::Null)
}
@@ -51,16 +76,17 @@ pub fn open_edit(app: &mut VectorcraftApp, name: Option<&str>) -> Result<Value,
};
let export = st.doc.symbol_export_type(&name).id();
let registration = st.doc.symbol_registration(&name).0;
app.ui.dialog = Some(Dialog::new(
KIND,
json!({
"name": name,
"exportType": export,
"registration": registration,
"__edit": true,
"__from": name,
}),
));
let mut fields = json!({
"name": name,
"exportType": export,
"registration": registration,
"__edit": true,
"__from": name,
});
if let Some(o) = fields.as_object_mut() {
o.extend(slice_fields(st.doc.symbol_nine_slice(&name)));
}
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(Value::Null)
}
@@ -83,6 +109,19 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
d.fields.insert("registration".into(), json!(i));
}
ui.end_row();
label(ui, "");
form::check(ui, d, "nineSlice", tl!("Enable Guides for 9-Slice Scaling"));
ui.end_row();
if d.bool("nineSlice") {
for (key, text) in SLICE_KEYS.into_iter().zip([tl!("Left:"), tl!("Top:"), tl!("Right:"), tl!("Bottom:")]) {
label(ui, text);
let v = d.f64(key, 100.0 / 3.0);
if let Some(n) = widgets::plain_field(ui, ("dlg-field", key), v, "%", 0, 64.0) {
d.fields.insert(key.into(), json!(n.clamp(0.0, 100.0)));
}
ui.end_row();
}
}
});
false
}
@@ -100,15 +139,25 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let r = if edit {
let from = d.fields.get("__from").and_then(Value::as_str).unwrap_or(name);
let mut params = json!({ "name": from, "exportType": export, "registration": registration });
merge(&mut params, slice_params(d));
if from != name {
params["newName"] = json!(name);
}
app.run("symbol.options", params)
} else {
app.run("symbol.new", json!({ "name": name, "exportType": export, "registration": registration }))
let mut params = json!({ "name": name, "exportType": export, "registration": registration });
merge(&mut params, slice_params(d));
app.run("symbol.new", params)
};
if r.is_ok() {
app.ui.dialog = None;
}
r
}
/// Copy the keys of the object `extra` into the object `into`.
fn merge(into: &mut Value, extra: Value) {
if let (Some(a), Value::Object(b)) = (into.as_object_mut(), extra) {
a.extend(b);
}
}
+1
View File
@@ -5050,3 +5050,4 @@ theme Appearance Външен вид
Movie Clip Филмов клип
@msg name is empty името е празно
@msg no symbol named `{n}` няма символ с име `{n}`
Enable Guides for 9-Slice Scaling Включване на водачи за 9-slice мащабиране
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5068,6 +5068,7 @@ mark-style Japanese Japonské
Movie Clip Filmový klip
@msg name is empty název je prázdný
@msg no symbol named `{n}` žádný symbol s názvem `{n}`
Enable Guides for 9-Slice Scaling Povolit vodítka pro 9-slice škálování
@msg a mask has more than one art block maska má více než jeden blok kresby
@msg mask art ends outside a mask dictionary kresba masky končí mimo slovník masky
@msg cancelled; nothing was changed zrušeno; nic nebylo změněno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4629,6 +4629,7 @@ effect Regular Regelmäßig
Movie Clip Movieclip
@msg name is empty Name ist leer
@msg no symbol named `{n}` kein Symbol namens `{n}`
Enable Guides for 9-Slice Scaling Hilfslinien für 9-Slice-Skalierung aktivieren
@msg a mask has more than one art block eine Maske enthält mehr als einen Grafikblock
@msg mask art ends outside a mask dictionary die Maskengrafik endet außerhalb eines Maskenwörterbuchs
@msg cancelled; nothing was changed abgebrochen; es wurde nichts geändert
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5020,6 +5020,7 @@ effect Points Σημεία
Movie Clip Κλιπ ταινίας
@msg name is empty το όνομα είναι κενό
@msg no symbol named `{n}` κανένα σύμβολο με όνομα `{n}`
Enable Guides for 9-Slice Scaling Ενεργοποίηση οδηγών για κλιμάκωση 9-slice
@msg a mask has more than one art block η μάσκα έχει περισσότερα από ένα μπλοκ γραφικών
@msg mask art ends outside a mask dictionary τα γραφικά της μάσκας τελειώνουν έξω από το λεξικό της μάσκας
@msg cancelled; nothing was changed ακυρώθηκε· δεν άλλαξε τίποτα
Can't render this file because it contains an unexpected character in line 1922 and column 19.
+1
View File
@@ -5010,6 +5010,7 @@ mark-style Japanese Japonesas
Movie Clip Clip de película
@msg name is empty el nombre está vacío
@msg no symbol named `{n}` no hay ningún símbolo llamado `{n}`
Enable Guides for 9-Slice Scaling Activar guías para escalado de 9 sectores
@msg a mask has more than one art block una máscara tiene más de un bloque de ilustración
@msg mask art ends outside a mask dictionary la ilustración de la máscara termina fuera de un diccionario de máscara
@msg cancelled; nothing was changed cancelado; no se ha cambiado nada
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5032,6 +5032,7 @@ mark-style Japanese Japanilainen
Movie Clip Elokuvaleike
@msg name is empty nimi on tyhjä
@msg no symbol named `{n}` symbolia nimeltä `{n}` ei ole
Enable Guides for 9-Slice Scaling Ota käyttöön 9-slice-skaalauksen apuviivat
@msg a mask has more than one art block maskissa on useampi kuin yksi grafiikkalohko
@msg mask art ends outside a mask dictionary maskin grafiikka päättyy maskin sanakirjan ulkopuolelle
@msg cancelled; nothing was changed peruutettu; mitään ei muutettu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5067,6 +5067,7 @@ theme Appearance Apparence
Movie Clip Clip
@msg name is empty le nom est vide
@msg no symbol named `{n}` aucun symbole nommé `{n}`
Enable Guides for 9-Slice Scaling Activer les repères pour la mise à l’échelle 9-slice
@msg a mask has more than one art block un masque contient plusieurs blocs graphiques
@msg mask art ends outside a mask dictionary les éléments graphiques du masque se terminent en dehors d’un dictionnaire de masque
@msg cancelled; nothing was changed annulé ; rien n'a été modifié
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5054,6 +5054,7 @@ imageTrace Outline Körvonal
Movie Clip Mozgóklip
@msg name is empty a név üres
@msg no symbol named `{n}` nincs `{n}` nevű szimbólum
Enable Guides for 9-Slice Scaling Segédvonalak engedélyezése a 9-slice méretezéshez
@msg a mask has more than one art block egy maszk egynél több grafikai blokkot tartalmaz
@msg mask art ends outside a mask dictionary a maszk grafikája a maszk szótárán kívül ér véget
@msg cancelled; nothing was changed megszakítva; semmi sem változott
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4667,6 +4667,7 @@ lighting Light Luce
Movie Clip Clip filmato
@msg name is empty il nome è vuoto
@msg no symbol named `{n}` nessun simbolo denominato `{n}`
Enable Guides for 9-Slice Scaling Abilita guide per il ridimensionamento 9-slice
@msg a mask has more than one art block una maschera contiene più di un blocco grafico
@msg mask art ends outside a mask dictionary la grafica della maschera termina fuori da un dizionario di maschera
@msg cancelled; nothing was changed annullato; non è stato modificato nulla
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4708,6 +4708,7 @@ lighting Light ライト
Movie Clip ムービークリップ
@msg name is empty 名前が空です
@msg no symbol named `{n}` `{n}` という名前のシンボルはありません
Enable Guides for 9-Slice Scaling 9スライス拡大・縮小用ガイドを有効にする
@msg a mask has more than one art block マスクに複数のアートブロックがあります
@msg mask art ends outside a mask dictionary マスクのアートがマスク辞書の外で終わっています
@msg cancelled; nothing was changed キャンセルしました。何も変更されていません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4631,6 +4631,7 @@ lighting Light 광원
Movie Clip 무비 클립
@msg name is empty 이름이 비어 있습니다
@msg no symbol named `{n}` 이름이 `{n}`인 심볼이 없습니다
Enable Guides for 9-Slice Scaling 9-슬라이스 크기 조절용 안내선 사용
@msg a mask has more than one art block 마스크에 아트 블록이 두 개 이상 있습니다
@msg mask art ends outside a mask dictionary 마스크 아트가 마스크 사전 밖에서 끝납니다
@msg cancelled; nothing was changed 취소됨; 변경된 내용이 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4914,6 +4914,7 @@ mark-style Japanese Japońskie
Movie Clip Klip filmowy
@msg name is empty nazwa jest pusta
@msg no symbol named `{n}` brak symbolu o nazwie `{n}`
Enable Guides for 9-Slice Scaling Włącz prowadnice skalowania 9-slice
@msg a mask has more than one art block maska ma więcej niż jeden blok grafiki
@msg mask art ends outside a mask dictionary grafika maski kończy się poza słownikiem maski
@msg cancelled; nothing was changed anulowano; nic nie zostało zmienione
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4985,6 +4985,7 @@ mark-style Japanese Japonesas
# Symbol Options › Export Type
@msg name is empty o nome está vazio
@msg no symbol named `{n}` nenhum símbolo chamado `{n}`
Enable Guides for 9-Slice Scaling Ativar guias para dimensionamento 9-slice
@msg a mask has more than one art block uma máscara contém mais de um bloco de arte
@msg mask art ends outside a mask dictionary a arte da máscara termina fora de um dicionário de máscara
@msg cancelled; nothing was changed cancelado; nada foi alterado
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4663,6 +4663,7 @@ lighting Light Свет
Movie Clip Видеоклип
@msg name is empty имя пустое
@msg no symbol named `{n}` нет символа с именем `{n}`
Enable Guides for 9-Slice Scaling Включить направляющие для 9-slice масштабирования
@msg a mask has more than one art block маска содержит более одного блока графики
@msg mask art ends outside a mask dictionary графика маски заканчивается за пределами словаря маски
@msg cancelled; nothing was changed отменено; ничего не изменено
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5024,6 +5024,7 @@ theme Appearance Utseende
Movie Clip Filmklipp
@msg name is empty namnet är tomt
@msg no symbol named `{n}` ingen symbol med namnet `{n}`
Enable Guides for 9-Slice Scaling Aktivera hjälplinjer för 9-slice-skalning
@msg a mask has more than one art block en mask innehåller mer än ett grafikblock
@msg mask art ends outside a mask dictionary maskens grafik slutar utanför en maskordlista
@msg cancelled; nothing was changed avbrutet; inget ändrades
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4647,6 +4647,7 @@ lighting Light Світло
Movie Clip Кліп
@msg name is empty ім’я порожнє
@msg no symbol named `{n}` немає символу з назвою `{n}`
Enable Guides for 9-Slice Scaling Увімкнути напрямні для 9-slice масштабування
@msg a mask has more than one art block маска містить більше одного блоку графіки
@msg mask art ends outside a mask dictionary графіка маски закінчується за межами словника маски
@msg cancelled; nothing was changed скасовано; нічого не змінено
Can't render this file because it contains an unexpected character in line 723 and column 40.
+1
View File
@@ -4647,6 +4647,7 @@ theme Appearance Giao diện
Movie Clip Clip phim
@msg name is empty tên trống
@msg no symbol named `{n}` không có biểu tượng nào tên `{n}`
Enable Guides for 9-Slice Scaling Bật đường dẫn cho co giãn 9-slice
@msg a mask has more than one art block mặt nạ có nhiều hơn một khối đồ họa
@msg mask art ends outside a mask dictionary đồ họa mặt nạ kết thúc bên ngoài từ điển mặt nạ
@msg cancelled; nothing was changed đã hủy; không có gì thay đổi
Can't render this file because it contains an unexpected character in line 3849 and column 10.
+1
View File
@@ -4894,6 +4894,7 @@ lighting Light 光源
Movie Clip 影片剪辑
@msg name is empty 名称为空
@msg no symbol named `{n}` 没有名为 `{n}` 的符号
Enable Guides for 9-Slice Scaling 启用 9-slice 缩放参考线
@msg cancelled; nothing was changed 已取消;未做任何更改
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it 保存会写入 {_1},而不是 {_2}:要写入该格式请使用 file.saveAsTemplate
@msg a mask has more than one art block 蒙版包含多个图稿块
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3970,3 +3970,4 @@ lighting Light 光源
Movie Clip 影片片段
@msg name is empty 名稱為空
@msg no symbol named `{n}` 沒有名為 `{n}` 的符號
Enable Guides for 9-Slice Scaling 啟用 9-slice 縮放參考線
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12
View File
@@ -293,6 +293,18 @@ mod tests {
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(symbols(&mut app), ["New Symbol", "Mark"]);
assert_eq!(app.run("symbol.list", json!({})).unwrap()["symbols"][1]["exportType"], "graphic");
// Enable Guides for 9-Slice Scaling stores the guides (percent in the dialog) and reopens as set.
assert!(app.run("symbol.list", json!({})).unwrap()["symbols"][1]["nineSlice"].is_null());
app.run("ui.symbolOptions", json!({"edit": true, "name": "Mark"})).unwrap();
let dialog = app.ui.dialog.as_mut().unwrap();
dialog.fields.insert("nineSlice".into(), json!(true));
dialog.fields.insert("sliceLeft".into(), json!(20.0));
crate::dialogs::confirm(&mut app).unwrap();
let nine = app.run("symbol.list", json!({})).unwrap()["symbols"][1]["nineSlice"].clone();
assert_eq!(nine[0], json!(0.2));
app.run("ui.symbolOptions", json!({"edit": true, "name": "Mark"})).unwrap();
assert!(app.ui.dialog.as_ref().unwrap().bool("nineSlice"));
assert_eq!(app.ui.dialog.as_ref().unwrap().f64("sliceLeft", 0.0), 20.0);
}
#[test]
+4 -2
View File
@@ -248,10 +248,12 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
- **Done:** Symbol Options › Registration (9-point; stored and used by `symbol.place`); Symbol Options ›
Export Type (Graphic or Movie Clip; default Movie Clip; `symbol.new` / `symbol.options` / Symbols panel;
canvas placement unchanged; Animate / Flash handoff metadata).
canvas placement unchanged; Animate / Flash handoff metadata); Symbol Options › Enable Guides for 9-Slice
Scaling (four guides as fractions of the symbol size, `nineSlice` / `nineSliceGuides` on `symbol.new` /
`symbol.options`, stored per symbol; handoff metadata, not drawn on the canvas, #2342).
- **Missing:** the Art brush's Overlap, auto-generated Pattern brush corners, new tile art in Pattern Brush
Options, the Key Color eyedropper; original brush, symbol and graphic-style libraries generated in code (never
Adobe's); dynamic symbols; Start Global Edit; Symbol Options 9-slice guides.
Adobe's); dynamic symbols; Start Global Edit; drawing the 9-slice guides on the canvas in symbol edit mode and scaling by them.
- **Estimate:** 15–26 h.
## G3 3D and Materials
+4 -1
View File
@@ -1105,7 +1105,10 @@ These families are in the live catalog (`command_list` / `vectorcraft://commands
already walks Image Trace. Short agent entry points:
- Symbols: `symbol.list` (each row includes `registration` 0..8 and `exportType`: `movieClip`|`graphic`),
`symbol.new {name?, registration?, exportType?, ids?}`, `symbol.options {name?, newName?, registration?, exportType?}`,
`symbol.new {name?, registration?, exportType?, nineSlice?, nineSliceGuides?, ids?}`,
`symbol.options {name?, newName?, registration?, exportType?, nineSlice?, nineSliceGuides?}` (`nineSlice` bool turns
9-slice guides on or off; `nineSliceGuides` is `[left, top, right, bottom]`, fractions 0..1 of the symbol size; rows
of `symbol.list` carry `nineSlice`: the guides or `null`),
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `symbol.replace`, `symbol.edit`, `symbol.duplicate`,
`symbol.selectInstances {name}`.
- Brushes: `brush.list`, `brush.get {name}`, `brush.apply {name, ids?}`, `brush.new {type, name?, params?, ids?}`,
+2 -1
View File
@@ -79,8 +79,9 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
| 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (other types use flat shading; one light, painter visibility); materials, several lights, shadows, ray tracing, 3D Classic, Turntable views, 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) |
| Type core | 9 | 80% | Auto-leading style resets are implemented; remaining: hyphenation and justification options, Optical Margin Alignment, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; localized legacy family-name fallback with loaded/catalog regressions (#2208) |
| Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), variable font axes, tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged |
| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, symbol libraries, Start Global Edit, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration and Export Type (Graphic / Movie Clip) are done | 5–9 | estimated |
| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, symbol libraries, Start Global Edit, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guide settings (stored, not drawn) are done | 5–9 | estimated |
| Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; simpler contour walk, identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments, so Paths is monotone on the code-drawn sweep) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them |
| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, symbol libraries, Start Global Edit, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration and Export Type (Graphic / Movie Clip) are done | 5–9 | estimated |
| Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set |
| View & navigation | 3 | 70% | New View/Edit Views, multiple windows and Arrange, Snap to Glyph | 9–14 | estimated |
| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), guides lost (#779), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC/JPEG 2000 import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests_reader_streaming.rs` and `xtask/src/pdf_deps.rs` |
+1 -1
View File
@@ -211,7 +211,7 @@ panel-menu commands such as Break Link to Symbol), so they can be given shortcut
| Pathfinder shape results keep their source names and SVG IDs | done | #1293; Minus Front keeps the back name, Minus Back the front name; Unite/Intersect/Exclude use the backmost named input; compound output carries it once; geometry and paint unchanged |
| Attributes fill-rule controls with a compound member selected | done | Edits the compound's rule; direction still edits only the member; rendered-hole and Undo/Redo regression tests (#1197) |
| Make Compound Path on a group (such as outlined type) keeps its backmost path's paint; compound paths joined into one keep their holes | done | #1537 |
| Symbols panel: New Symbol opens Symbol Options (Name) before making the symbol; Alt-click skips it | partial | #1560 (#1566); Registration 9-point grid in Symbol Options (`symbol.new` / `symbol.place`); export type, static or dynamic and 9-slice settings still missing |
| Symbols panel: New Symbol opens Symbol Options (Name) before making the symbol; Alt-click skips it | partial | #1560 (#1566); Registration 9-point grid in Symbol Options (`symbol.new` / `symbol.place`); Export Type (Graphic / Movie Clip) and Enable Guides for 9-Slice Scaling (guides stored, not drawn on the canvas); Dynamic/Static symbol type still missing |
| Pathfinder Trim, Merge and Crop remove strokes; Merge joins same-fill objects whatever their strokes | done | #1570 (#1572) |
| Pathfinder panel on one group selected alone; a greyed-out button says what it needs | partial | #1450; the group's members are combined; one plain path selected alone stays disabled (the public documentation doesn't say what the operations do to a single path) |
| Pathfinder Divide: an open cutting line splits a filled path | done | #1307; straight, crossed, bent and curved cutters, holes and frontmost appearance regression-tested; native automation: independent piece movement, one Undo/Redo, saved-document round trip and window capture; existing bounded-map fallback remains |