G5: Symbol Options Registration 9-point grid

Store the registration point with the symbol and place instances so that point lands on (x, y). Symbol Options exposes the same 9-point control as Transform.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Fabio William Conceição
2026-10-16 04:45:03 +07:00
committed by bflatastic
co-authored by Cursor
parent 848d902354
commit 80090e7623
23 changed files with 155 additions and 36 deletions
+26
View File
@@ -20,8 +20,10 @@ pub mod inks;
mod inline;
pub mod intertwine;
pub mod shared;
mod symbol_registration;
pub use inline::{SYMBOL_HALF, SYMBOL_SIZES};
pub use shared::Shared;
pub use symbol_registration::{SYMBOL_REGISTRATION, SymbolRegistration};
pub mod links;
pub mod live;
pub mod marks;
@@ -1518,6 +1520,30 @@ impl Document {
}
(2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string())
}
/// Symbol Options › Registration for `name` (default: centre).
pub fn symbol_registration(&self, name: &str) -> SymbolRegistration {
self.unknown
.get(SYMBOL_REGISTRATION)
.and_then(|m| m.get(name))
.and_then(|v| v.as_u64())
.and_then(SymbolRegistration::from_u64)
.unwrap_or_default()
}
/// Set or clear (when centre) the registration point stored for `name`.
pub fn set_symbol_registration(&mut self, name: &str, reg: SymbolRegistration) {
let m = self.unknown.entry(SYMBOL_REGISTRATION.to_string()).or_insert_with(|| serde_json::json!({}));
let Some(o) = m.as_object_mut() else { return };
if reg.is_center() {
o.remove(name);
if o.is_empty() {
self.unknown.remove(SYMBOL_REGISTRATION);
}
} else {
o.insert(name.into(), serde_json::json!(reg.0));
}
}
}
fn default_graphic_styles() -> Vec<GraphicStyle> {
+36
View File
@@ -0,0 +1,36 @@
//! Symbol Options › Registration: the 9-point grid (0 = top-left … 4 = centre … 8 = bottom-right)
//! that `symbol.place` uses as the point that lands on the click.
use vectorcraft_geom::Vec2;
/// Document unknown key: `{ symbolName: 0..8 }` for each symbol's registration point.
pub const SYMBOL_REGISTRATION: &str = "symbolRegistration";
/// Index on the 9-point grid (row-major, 4 = centre).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SymbolRegistration(pub u8);
impl Default for SymbolRegistration {
fn default() -> Self {
Self(4)
}
}
impl SymbolRegistration {
pub const CENTER: Self = Self(4);
pub fn from_u64(v: u64) -> Option<Self> {
(v <= 8).then_some(Self(v as u8))
}
pub fn is_center(self) -> bool {
self.0 == 4
}
/// Offset of this registration point from the symbol's centre, in natural-size units
/// (right +, down +).
pub fn offset_from_center(self, w: f64, h: f64) -> Vec2 {
let (col, row) = (self.0 % 3, self.0 / 3);
Vec2::new((f64::from(col) - 1.0) * (w * 0.5), (f64::from(row) - 1.0) * (h * 0.5))
}
}
+36 -13
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};
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol, SymbolRegistration};
use vectorcraft_geom::{Affine, Point, Vec2};
use super::edit::selected_roots;
@@ -95,13 +95,13 @@ pub fn specs() -> Vec<CommandSpec> {
brush_freehand
),
// ----- symbols -----
cmd!(query "symbol.list", "Symbols", [], None, "{} → {symbols: [{name, instances, size: [w,h]}], current}", has_doc, symbol_list),
cmd!(query "symbol.list", "Symbols", [], None, "{} → {symbols: [{name, instances, size: [w,h], registration: 0..8}], current}", has_doc, symbol_list),
cmd!(
"symbol.new",
"New Symbol…",
[],
None,
"{name?, ids?|id?} make a symbol from the selection and replace it with an instance → {name, id}",
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration)} make a symbol from the selection and replace it with an instance → {name, id, registration}",
has_selection,
symbol_new
),
@@ -110,7 +110,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Place Symbol Instance",
[],
None,
"{name?, x?, y?} place an instance centred at (x, y) (default: artboard centre; name default: current) → {id}",
"{name?, x?, y?} place an instance so its registration point is at (x, y) (default: artboard centre; name default: current) → {id}",
has_doc,
symbol_place
),
@@ -511,11 +511,19 @@ fn set_natural_size(d: &mut Document, name: &str, size: Option<(f64, f64)>) {
}
}
/// Instance transform for a symbol of natural size `(w, h)` centred at `c`.
fn place_xf(c: Point, (w, h): (f64, f64)) -> Affine {
/// Instance transform for a symbol of natural size `(w, h)` whose registration point is at `at`.
fn place_xf(at: Point, (w, h): (f64, f64), reg: SymbolRegistration) -> Affine {
let c = at - reg.offset_from_center(w, h);
Affine::translate(c.to_vec2()) * Affine::scale_non_uniform(w / (2.0 * HALF), h / (2.0 * HALF))
}
fn registration_param(p: &Value, cmd: &str) -> Result<SymbolRegistration> {
match p.get("registration") {
None => Ok(SymbolRegistration::CENTER),
Some(v) => v.as_u64().and_then(SymbolRegistration::from_u64).ok_or_else(|| bad(cmd, "registration must be 0..8")),
}
}
/// Normalise art for a symbol definition: returns (art, natural size, centre in the document).
fn normalize(mut art: Node) -> Result<(Node, (f64, f64), Point)> {
let b = art.geometric_bounds().ok_or_else(|| EngineError::Other("the art has no bounds".into()))?;
@@ -553,7 +561,12 @@ fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
.iter()
.map(|sym| {
let (w, h) = natural_size(d, &sym.name);
json!({"name": sym.name, "instances": instances_of(d, Some(&sym.name)).len(), "size": [w, h]})
json!({
"name": sym.name,
"instances": instances_of(d, Some(&sym.name)).len(),
"size": [w, h],
"registration": d.symbol_registration(&sym.name).0,
})
})
.collect();
Ok(json!({ "symbols": list, "current": current_symbol(d).or_else(|| symbol_names(d).into_iter().next()) }))
@@ -580,16 +593,20 @@ fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
let art = selection_art(s, p)?.ok_or_else(|| EngineError::Other("nothing selected".into()))?;
let (art, size, c) = normalize(art)?;
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, "symbol.new")?;
// 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| {
let art = d.reid(&art);
d.symbols.push(Symbol { name: name.clone(), art: Arc::new(art) });
set_natural_size(d, &name, Some(size));
d.set_symbol_registration(&name, reg);
d.unknown.insert(CURRENT_SYMBOL.into(), json!(name));
let id = replace_with_instance(d, &ids, &name, place_xf(c, size))?;
let id = replace_with_instance(d, &ids, &name, place)?;
sel.set([id]);
Ok(id)
})?;
Ok(json!({ "name": name, "id": id.0 }))
Ok(json!({ "name": name, "id": id.0, "registration": reg.0 }))
}
fn resolve_symbol(d: &Document, p: &Value, cmd: &str) -> Result<String> {
@@ -606,12 +623,13 @@ fn symbol_place(s: &mut Session, p: &Value) -> Result<Value> {
let d = &s.doc()?.doc;
let name = resolve_symbol(d, p, "symbol.place")?;
let centre = d.artboards.first().map(|a| a.rect.center()).unwrap_or(Point::ZERO);
let c = Point::new(f64_or(p, "x", centre.x), f64_or(p, "y", centre.y));
let at = Point::new(f64_or(p, "x", centre.x), f64_or(p, "y", centre.y));
let size = natural_size(d, &name);
let reg = d.symbol_registration(&name);
let parent = s.doc()?.target_parent()?;
let id = s.edit("Place Symbol Instance", |d, sel| {
let id = d.alloc_id();
d.insert(parent, usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: name.clone(), xf: place_xf(c, size) }))?;
d.insert(parent, usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: name.clone(), xf: place_xf(at, size, reg) }))?;
sel.set([id]);
Ok(id)
})?;
@@ -720,7 +738,8 @@ fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
*xf *= fix;
}
}
let id = replace_with_instance(d, &ids, &name, place_xf(c, size))?;
let reg = d.symbol_registration(&name);
let id = replace_with_instance(d, &ids, &name, place_xf(c + reg.offset_from_center(size.0, size.1), size, reg))?;
sel.set([id]);
Ok(id)
})?;
@@ -743,6 +762,7 @@ fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
}
d.symbols.retain(|x| x.name != name);
set_natural_size(d, &name, None);
d.set_symbol_registration(&name, SymbolRegistration::CENTER);
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
d.unknown.remove(CURRENT_SYMBOL);
}
@@ -758,10 +778,12 @@ fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let sym = d.symbols.iter().find(|x| x.name == name).cloned().ok_or_else(|| bad(C, format!("no symbol named `{name}`")))?;
let new = d.unique_symbol_name(str_param(p, "newName").unwrap_or(&format!("{name} copy")));
let size = natural_size(d, &name);
let reg = d.symbol_registration(&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);
Ok(())
})?;
Ok(json!({ "name": new }))
@@ -882,13 +904,14 @@ fn symbol_spray(s: &mut Session, p: &Value) -> Result<Value> {
placed.push(c);
}
let count = placed.len();
let reg = s.doc()?.doc.symbol_registration(&name);
let id = s.edit("Symbol Sprayer", |d, sel| {
let mut nodes = vec![];
for (i, c) in placed.iter().enumerate() {
let (_, _, js) = jitter(i + 7, *c);
let k = scale * (1.0 + js * 0.1);
let id = d.alloc_id();
nodes.push(Node::new(id, NodeKind::SymbolInstance { symbol: name.clone(), xf: place_xf(*c, size) * Affine::scale(k) }));
nodes.push(Node::new(id, NodeKind::SymbolInstance { symbol: name.clone(), xf: place_xf(*c, size, reg) * Affine::scale(k) }));
}
let gid = match set {
Some(g) => {
+17
View File
@@ -581,6 +581,7 @@ fn brushes_and_symbols_survive_vectorcraft_round_trip() {
assert_eq!(vectorcraft_brush::find(&back, "Mine").map(|b| b.kind.type_id()), Some("calligraphic"));
assert_eq!(back.symbols, doc.symbols);
assert_eq!(back.unknown.get("symbolSizes"), doc.unknown.get("symbolSizes"));
assert_eq!(back.unknown.get(vectorcraft_doc::SYMBOL_REGISTRATION), doc.unknown.get(vectorcraft_doc::SYMBOL_REGISTRATION));
let bl = back.node(l).unwrap();
assert_eq!(bl.appearance.stroke().unwrap().brush.as_deref(), Some("Mine"));
// A reloaded document places instances at the natural size.
@@ -590,3 +591,19 @@ fn brushes_and_symbols_survive_vectorcraft_round_trip() {
let b = s2.doc().unwrap().doc.node(NodeId(r["id"].as_u64().unwrap())).unwrap().geometric_bounds().unwrap();
assert!(close(b, Rect::new(60.0, 80.0, 140.0, 120.0), 1e-9));
}
/// Symbol Options › Registration: place puts the chosen 9-point grid point on (x, y).
#[test]
fn symbol_registration_places_the_chosen_point_on_xy() {
let mut s = session();
rect(&mut s, 0.0, 0.0, 80.0, 40.0);
let r = s.execute("symbol.new", &json!({"name": "Corner", "registration": 0})).unwrap();
assert_eq!(r["registration"], 0);
assert_eq!(s.execute("symbol.list", &json!({})).unwrap()["symbols"][0]["registration"], 0);
// Creating kept the art where it was (top-left still at the origin of the selection).
let inst = NodeId(r["id"].as_u64().unwrap());
assert!(close(bounds(&s, inst), Rect::new(0.0, 0.0, 80.0, 40.0), 1e-9), "{:?}", bounds(&s, inst));
let p = NodeId(s.execute("symbol.place", &json!({"name": "Corner", "x": 300, "y": 200})).unwrap()["id"].as_u64().unwrap());
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());
}
+19 -6
View File
@@ -1,7 +1,7 @@
//! Symbol Options: the name of the symbol New Symbol makes from the selection (`symbol.new`), asked
//! before the symbol is made, as the Symbols panel's New Symbol button and menu item do.
//! Symbol Options: the name and registration point of the symbol New Symbol makes from the
//! selection (`symbol.new`), as the Symbols panel's New Symbol button and menu item do.
//!
//! Fields: `name`.
//! Fields: `name`, `registration` (0..8, the 9-point grid; 4 = centre).
use serde_json::{Value, json};
@@ -9,6 +9,7 @@ use super::swatch_options::{grid, label};
use super::{DialogSpec, form};
use crate::VectorcraftApp;
use crate::state::Dialog;
use crate::widgets;
/// The dialog kind of Symbol Options.
pub const KIND: &str = "symbolOptions";
@@ -22,7 +23,7 @@ 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 })));
app.ui.dialog = Some(Dialog::new(KIND, json!({ "name": name, "registration": 4 })));
Ok(Value::Null)
}
@@ -31,13 +32,25 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
label(ui, tl!("Name:"));
form::text(ui, d, "name", 190.0);
ui.end_row();
label(ui, tl!("Registration:"));
let cur = d.fields.get("registration").and_then(Value::as_u64).unwrap_or(4) as usize;
if let Some(i) = widgets::reference_point(ui, cur) {
d.fields.insert("registration".into(), json!(i));
}
ui.end_row();
});
false
}
/// OK: make the symbol from the selection with the name typed.
/// OK: make the symbol from the selection with the name and registration chosen.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let r = app.run("symbol.new", json!({ "name": d.str("name") }));
let r = app.run(
"symbol.new",
json!({
"name": d.str("name"),
"registration": d.fields.get("registration").and_then(Value::as_u64).unwrap_or(4),
}),
);
if r.is_ok() {
app.ui.dialog = None;
}
+1 -1
View File
@@ -4858,9 +4858,9 @@ mark-style Japanese Japonské
Softness Měkkost
Ambient Light Okolní světlo
Light behind the object Světlo za objektem
Registration: Registrace:
# Dimension tool (linked dimension annotations)
Display Zobrazení
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Rozměr
Drawing : real size — 1:10 labels 1 unit on the page as 10. Výkres : skutečná velikost — 1:10 označí 1 jednotku na stránce jako 10.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4391,7 +4391,7 @@ theme Appearance Erscheinungsbild
Softness Weichheit
Ambient Light Umgebungslicht
Light behind the object Licht hinter dem Objekt
Registration: Registrierung:
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Bemaßung
Drawing : real size — 1:10 labels 1 unit on the page as 10. Zeichnung : reale Größe — 1:10 beschriftet 1 Einheit auf der Seite als 10.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4799,6 +4799,7 @@ mark-style Japanese Japonesas
Softness Suavidad
Ambient Light Luz ambiental
Light behind the object Luz detrás del objeto
Registration: Registro:
# Dimension tool (linked dimension annotations)
Create Dimension Crear cota
Dimension Tool Herramienta Cota
@@ -4808,7 +4809,6 @@ mark-style Japanese Japonesas
Precision: Precisión:
Update Dimensions Actualizar cotas
Display Visualización
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Cota
Drawing : real size — 1:10 labels 1 unit on the page as 10. Dibujo : tamaño real — 1:10 rotula 1 unidad de la página como 10.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4858,7 +4858,7 @@ theme Appearance Apparence
Softness Douceur
Ambient Light Lumière ambiante
Light behind the object Lumière derrière l'objet
Registration: Repère d'alignement :
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Cote
Drawing : real size — 1:10 labels 1 unit on the page as 10. Dessin : taille réelle — 1:10 étiquette 1 unité de la page comme 10.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4825,7 +4825,7 @@ imageTrace Outline Körvonal
Softness Lágyság
Ambient Light Környezeti fény
Light behind the object Fény az objektum mögött
Registration: Regisztráció:
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Méret
Drawing : real size — 1:10 labels 1 unit on the page as 10. Rajz : valós méret — 1:10 esetén az oldalon 1 egység 10-ként jelenik meg.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4431,7 +4431,7 @@ lighting Light Luce
Softness Morbidezza
Ambient Light Luce ambientale
Light behind the object Luce dietro l'oggetto
Registration: Registrazione:
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Quota
Drawing : real size — 1:10 labels 1 unit on the page as 10. Disegno : dimensione reale — 1:10 indica 1 unità sulla pagina come 10.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4425,6 +4425,7 @@ lighting Light ライト
Softness 柔らかさ
Ambient Light 環境光
Light behind the object オブジェクトの背後のライト
Registration: 基準点:
# Dimension tool (linked dimension annotations)
Create Dimension 寸法を作成
Dimension Tool 寸法ツール
@@ -4434,7 +4435,6 @@ lighting Light ライト
Precision: 精度:
Update Dimensions 寸法を更新
Display 表示
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 寸法
Drawing : real size — 1:10 labels 1 unit on the page as 10. 図面 : 実寸 — 1:10 では、ページ上の 1 単位を 10 と表示します。
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4395,7 +4395,7 @@ lighting Light 광원
Softness 부드러움
Ambient Light 주변광
Light behind the object 개체 뒤의 조명
Registration: 기준점:
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 치수
Drawing : real size — 1:10 labels 1 unit on the page as 10. 도면 : 실제 크기 — 1:10은 페이지의 1단위를 10으로 표시합니다.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4688,6 +4688,7 @@ mark-style Japanese Japońskie
Softness Miękkość
Ambient Light Światło otoczenia
Light behind the object Światło za obiektem
Registration: Punkt rejestracji:
Legacy (version 7) Starsza (wersja 7)
Transparent Images: Przezroczyste obrazy:
Flatten Spłaszcz
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3800,6 +3800,7 @@ axis Both Ambos
Softness Suavidade
Ambient Light Luz ambiente
Light behind the object Luz atrás do objeto
Registration: Registro:
# Dimension tool (linked dimension annotations)
Dimension Tool Ferramenta Cota
Create Dimension Criar Cota
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -4427,7 +4427,7 @@ lighting Light Свет
Softness Мягкость
Ambient Light Окружающий свет
Light behind the object Свет позади объекта
Registration: Регистрация:
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Размер
Drawing : real size — 1:10 labels 1 unit on the page as 10. Чертёж : реальный размер — при 1:10 1 единица на странице подписывается как 10.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -4411,7 +4411,7 @@ lighting Light Світло
Softness М'якість
Ambient Light Навколишнє світло
Light behind the object Світло позаду об'єкта
Registration: Реєстрація:
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Розмір
Drawing : real size — 1:10 labels 1 unit on the page as 10. Креслення : реальний розмір — при 1:10 1 одиниця на сторінці підписується як 10.
Can't render this file because it contains an unexpected character in line 718 and column 40.
+1
View File
@@ -4413,6 +4413,7 @@ theme Appearance Giao diện
Softness Độ mềm
Ambient Light Ánh sáng môi trường
Light behind the object Ánh sáng phía sau đối tượng
Registration: Điểm đăng ký:
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Kích thước
Drawing : real size — 1:10 labels 1 unit on the page as 10. Bản vẽ : kích thước thực — 1:10 ghi 1 đơn vị trên trang thành 10.
Can't render this file because it contains an unexpected character in line 3726 and column 10.
+1 -1
View File
@@ -3999,6 +3999,7 @@ lighting Light 光源
Softness 柔和度
Ambient Light 环境光
Light behind the object 光源位于对象后方
Registration: 对齐点:
# Dimension tool (linked dimension annotations)
Create Dimension 创建尺寸标注
Dimension Tool 尺寸标注工具
@@ -4008,7 +4009,6 @@ lighting Light 光源
Precision: 精度:
Update Dimensions 更新尺寸标注
Display 显示
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 尺寸
Drawing : real size — 1:10 labels 1 unit on the page as 10. 图纸 : 实际尺寸 — 1:10 会将页面上的 1 个单位标注为 10。
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -3784,6 +3784,7 @@ lighting Light 光源
Softness 柔和度
Ambient Light 環境光
Light behind the object 光源位於物件後方
Registration: 對齊點:
# Dimension tool (linked dimension annotations)
Create Dimension 建立尺寸標註
Dimension Tool 尺寸標註工具
@@ -3793,7 +3794,6 @@ lighting Light 光源
Precision: 精確度:
Update Dimensions 更新尺寸標註
Display 顯示
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 尺寸
Drawing : real size — 1:10 labels 1 unit on the page as 10. 圖紙 : 實際尺寸 — 1:10 會將頁面上的 1 個單位標示為 10。
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -449,7 +449,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.symbolOptions",
"Symbol Options…",
"",
"{} open Symbol Options for a new symbol made from the selection, what the Symbols panel's New Symbol opens (dialog `symbolOptions`, field `name`, default the first free \"New Symbol\"/\"New Symbol N\"); OK runs symbol.new {name}",
"{} open Symbol Options for a new symbol made from the selection, what the Symbols panel's New Symbol opens (dialog `symbolOptions`, fields `name` and `registration` 0..8 on the 9-point grid, default centre; default name the first free \"New Symbol\"/\"New Symbol N\"); OK runs symbol.new {name, registration}",
),
(
"tool.options",
+4 -3
View File
@@ -25,7 +25,7 @@ stage ([ROADMAP.md](../ROADMAP.md)).
| 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 23–39 | no | [effects-parity.md](effects-parity.md) |
| 9 | [G13 Illustrator 2026 (30.x) additions](#g13-illustrator-2026-30x-additions) | 15–25 | no | [target-app-parity.md](target-app-parity.md) |
| 10 | [G14 Pen hardware beyond Windows pressure](#g14-pen-hardware-beyond-windows-pressure) | 8–16 | macOS pressure | [hardware-parity.md](hardware-parity.md) |
| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 17–28 | no | [target-app-parity.md](target-app-parity.md) |
| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 16–27 | no | [target-app-parity.md](target-app-parity.md) |
| 12 | [G3 3D and Materials](#g3-3d-and-materials) | 25–45 | no | [effects-parity.md](effects-parity.md) |
| 13 | [G7 Views and windows](#g7-views-and-windows) | 15–25 | no | [ui-parity.md](ui-parity.md) |
| 14 | [G6 Automation and scripting](#g6-automation-and-scripting) | 8–14 | no | [target-app-parity.md](target-app-parity.md) |
@@ -240,8 +240,9 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
- **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.
- **Estimate:** 17–28 h.
Adobe's); dynamic symbols; Start Global Edit. Symbol Options › Registration (9-point) is stored and
used by `symbol.place`; export type and 9-slice guides still missing.
- **Estimate:** 16–27 h.
## G3 3D and Materials
+1 -1
View File
@@ -201,7 +201,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); the dialog has Name only: export type, static or dynamic, registration and 9-slice settings 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, static or dynamic and 9-slice settings 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 |