Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol

This commit is contained in:
bflatastic
2026-11-01 04:53:38 +07:00
parent 3f1928efc8
commit f22caa6108
105 changed files with 1218 additions and 212 deletions
+1 -1
View File
@@ -335,7 +335,7 @@ fn scatter_shapes() -> BrushLibrary {
a.items.push(AppearanceItem::Fill(FillLayer::new(Paint::solid(gold))));
a
});
doc.symbols.push(Symbol { name: "Sparkle".into(), art: Arc::new(sparkle) });
doc.symbols.push(Symbol { dynamic: false, name: "Sparkle".into(), art: Arc::new(sparkle) });
let instance = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Sparkle".into(), xf: Affine::scale(0.3) });
let random = |a: f64, b: f64| (a, b);
let base = Scatter { colorization: Colorization::Tints, ..Default::default() };
+36 -7
View File
@@ -25,6 +25,7 @@ use super::{
use crate::EngineError;
mod brushes;
mod dynamic;
mod symbols;
pub(crate) use brushes::{apply_brush, brush_targets, stroke_paint};
@@ -32,6 +33,8 @@ use brushes::{
brush_apply, brush_delete, brush_duplicate, brush_freehand, brush_get, brush_list, brush_move, brush_new, brush_options, brush_remove,
brush_selection_art, brush_set_current, brush_unused, expand_brush,
};
use dynamic::{symbol_instance_reset, symbol_instance_set_appearance};
use symbols::selected_instances;
pub(crate) use symbols::{instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol};
use symbols::{
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_edit, symbol_list, symbol_new, symbol_options, symbol_place,
@@ -157,13 +160,21 @@ pub fn specs() -> Vec<CommandSpec> {
brush_freehand
),
// ----- symbols -----
cmd!(query "symbol.list", "Symbols", [], None, "{} → {symbols: [{name, instances, size: [w,h], registration: 0..8, exportType: movieClip|graphic}], current}", has_doc, symbol_list),
cmd!(
query "symbol.list",
"Symbols",
[],
None,
"{} → {symbols: [{name, instances, size: [w,h], registration: 0..8, exportType: movieClip|graphic, nineSlice, type: static|dynamic}], current}",
has_doc,
symbol_list
),
cmd!(
"symbol.new",
"New Symbol…",
[],
None,
"{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; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds; 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}",
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration), exportType?: movieClip (default)|graphic, nineSlice?: bool, nineSliceGuides?: array, type?: static (default)|dynamic (Symbol Options › Symbol Type)} make a symbol from the selection and replace it with an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds; 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, type}",
has_selection,
symbol_new
),
@@ -172,7 +183,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Symbol Options…",
[],
None,
"{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}",
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic, nineSlice?: bool, nineSliceGuides?: array, type?: static|dynamic} rename a symbol and/or set Symbol Options › Registration, Export Type, 9-slice guides (nineSliceGuides [left, top, right, bottom] fractions 0..1) and Symbol Type (a dynamic symbol's instances may change the appearance of objects in its art and stay linked; making it static drops those changes); default name: current → {name, registration, exportType, nineSlice, type}",
has_doc,
symbol_options
),
@@ -190,7 +201,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Break Link to Symbol",
[],
None,
"{ids?|id?} expand selected instances into plain art → {ids}",
"{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol) → {ids}",
has_selection,
symbol_break_link
),
@@ -199,7 +210,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Edit Symbol",
[],
None,
"{id?, ids?: [id…] (instead of id)} break the link of the selected instance so its art can be edited; finish with symbol.update → {name, ids}",
"{id?, ids?: [id…] (instead of id)} break the link of the selected instance so the symbol's art can be edited (without the instance's own appearance changes to a dynamic symbol); finish with symbol.update → {name, ids}",
has_selection,
symbol_edit
),
@@ -208,7 +219,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Redefine Symbol",
[],
None,
"{name, ids?|id?} replace the symbol's art with the selection (instances update; the selection becomes an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds) → {name, id}",
"{name, ids?|id?} replace the symbol's art with the selection (instances update, keeping their appearance changes to the objects at paths the new art still has; the selection becomes an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds) → {name, id}",
has_selection,
symbol_update
),
@@ -227,10 +238,28 @@ pub fn specs() -> Vec<CommandSpec> {
"Replace Symbol",
[],
None,
"{name, ids?|id?} swap the symbol of the selected instances → {ids}",
"{name, ids?|id?} swap the symbol of the selected instances (they keep their appearance changes to the objects at paths a dynamic new symbol's art has) → {ids}",
has_selection,
symbol_replace
),
cmd!(
"symbol.instance.setAppearance",
"Symbol Instance Appearance",
[],
None,
"{id?|ids?: [one id] (default: the one selected object), path: [i…] (the object in the symbol's art: child indices in paint order from the art's root down through its groups; [] the root), fill?: \"#rrggbb\"|{…paint params of paint.setFill: color, none, swatch, tint, gradient, keepModel}|null (null: the symbol's own again), stroke?: as fill, opacity?: 0..100|null, blend?: name|null} change the appearance of one object of a dynamic symbol instance's art, for that instance only: it stays linked, so editing the symbol reshapes it and keeps the change; refused for a static symbol or a path naming no object → {id, overrides: how many objects the instance changes}",
has_selection,
symbol_instance_set_appearance
),
cmd!(
"symbol.instance.reset",
"Reset Symbol Instance",
[],
None,
"{ids?|id?} drop the selected dynamic symbol instances' own appearance changes, so they show the symbol's appearance again → {ids: the instances that had changes}",
has_selection,
symbol_instance_reset
),
cmd!(
"symbol.setCurrent",
"Current Symbol",
+151
View File
@@ -0,0 +1,151 @@
//! Dynamic symbols (Symbol Options › Symbol Type): an instance of a dynamic symbol changes the
//! appearance of objects in the symbol's art and stays linked to the symbol
//! (`symbol.instance.setAppearance`, `symbol.instance.reset`). The changes live on the instance
//! ([`vectorcraft_doc::symbol_overrides`]); editing the symbol keeps those whose object is still
//! there ([`fit_overrides`]).
use std::sync::Arc;
use vectorcraft_color::BlendMode;
use vectorcraft_doc::symbol_overrides::{self, SymbolOverride};
use super::{Document, EngineError, Node, NodeId, NodeKind, Result, Session, Value, bad, check_numbers, ids_json, json, roots, selected_instances};
/// The `type` param, Symbol Options › Symbol Type: `Some(true)` for dynamic.
pub(super) fn type_param(p: &Value, cmd: &str) -> Result<Option<bool>> {
let Some(v) = p.get("type").filter(|v| !v.is_null()) else { return Ok(None) };
match v.as_str() {
Some("static") => Ok(Some(false)),
Some("dynamic") => Ok(Some(true)),
_ => Err(bad(cmd, format!("type must be static or dynamic, not {v}"))),
}
}
/// The `type` a symbol reports.
pub(super) fn type_id(dynamic: bool) -> &'static str {
if dynamic { "dynamic" } else { "static" }
}
/// Fit the appearance changes of instances `ids` to their symbols: an instance of a static (or
/// missing) symbol keeps none, an instance of a dynamic one those naming an object of its art.
pub(super) fn fit_overrides(d: &mut Document, ids: &[NodeId]) {
for id in ids {
let art = match d.node(*id) {
Some(n) if !n.extras().symbol_overrides.is_empty() => match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).filter(|s| s.dynamic).map(|s| s.art.clone()),
_ => continue,
},
_ => continue,
};
let Some(n) = d.node_mut(*id) else { continue };
let mut e = n.extras_mut();
match art {
Some(art) => symbol_overrides::retain_parts(&mut e.symbol_overrides, &art),
None => e.symbol_overrides.clear(),
}
}
}
/// The instance `id` names (else the one selected object) and its symbol's art; the symbol must
/// be dynamic.
fn dynamic_instance(s: &Session, p: &Value, cmd: &str) -> Result<(NodeId, Arc<Node>)> {
let d = &s.doc()?.doc;
let not_one = || EngineError::Other("select a symbol instance".into());
let [id] = roots(s, p)?[..] else { return Err(not_one()) };
let Some(NodeKind::SymbolInstance { symbol, .. }) = d.node(id).map(|n| &n.kind) else { return Err(not_one()) };
let sym = d.symbols.iter().find(|x| x.name == *symbol).ok_or_else(|| bad(cmd, format!("no symbol named `{n}`", n = symbol)))?;
if !sym.dynamic {
let name = &sym.name;
return Err(bad(cmd, format!("`{name}` is a static symbol: set its Symbol Type to Dynamic first")));
}
Ok((id, sym.art.clone()))
}
/// The `path` param: child indices from the art's root, naming an object of `art`.
fn path_param(p: &Value, art: &Node, cmd: &str) -> Result<Vec<u32>> {
let path: Option<Vec<u32>> =
p.get("path").and_then(Value::as_array).and_then(|a| a.iter().map(|i| i.as_u64().and_then(|i| u32::try_from(i).ok())).collect());
let path = path.ok_or_else(|| bad(cmd, "path must be a list of child indices"))?;
if symbol_overrides::part(art, &path).is_none() {
let shown = json!(path);
return Err(bad(cmd, format!("no object at path {shown} in the symbol's art")));
}
Ok(path)
}
/// `fill` or `stroke` of `p` for override `o`: absent leaves it, null clears it, a colour string or
/// an object of `paint.setFill`'s paint params sets it.
fn paint_change(s: &Session, p: &Value, key: &str, cmd: &str) -> Result<Option<Option<vectorcraft_color::Paint>>> {
let Some(v) = p.get(key) else { return Ok(None) };
if v.is_null() {
return Ok(Some(None));
}
let v = if v.is_string() { json!({ "color": v }) } else { v.clone() };
let paint = crate::cmd::paint::paint_from(s, &v)?.ok_or_else(|| bad(cmd, "give color, none, swatch or gradient"))?;
Ok(Some(Some(paint)))
}
/// The change `p` asks of the override `old` (for the object at `path`).
fn changed(s: &Session, p: &Value, old: SymbolOverride, cmd: &str) -> Result<SymbolOverride> {
let mut o = old;
if let Some(fill) = paint_change(s, p, "fill", cmd)? {
o.fill = fill;
}
if let Some(stroke) = paint_change(s, p, "stroke", cmd)? {
o.stroke = stroke;
}
check_numbers(p, &["opacity"], cmd)?;
if let Some(v) = p.get("opacity") {
o.opacity = v.as_f64().map(crate::cmd::opacitymask::percent);
}
if let Some(v) = p.get("blend") {
o.blend = match v {
Value::Null => None,
b => Some(b.as_str().and_then(BlendMode::parse).ok_or_else(|| bad(cmd, format!("unknown blend mode {b}")))?),
};
}
Ok(o)
}
pub(super) fn symbol_instance_set_appearance(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "symbol.instance.setAppearance";
if !["fill", "stroke", "opacity", "blend"].iter().any(|k| p.get(k).is_some()) {
return Err(bad(C, "give fill, stroke, opacity and/or blend"));
}
let (id, art) = dynamic_instance(s, p, C)?;
let path = path_param(p, &art, C)?;
let d = &s.doc()?.doc;
let old = d.node(id).and_then(|n| n.extras().symbol_overrides.iter().find(|o| o.path == path).cloned());
let new = changed(s, p, old.unwrap_or_else(|| SymbolOverride { path: path.clone(), ..Default::default() }), C)?;
let count = s.edit("Symbol Instance Appearance", |d, _| {
let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?;
let mut e = n.extras_mut();
let list = &mut e.symbol_overrides;
list.retain(|o| o.path != path);
if !new.is_empty() {
list.push(new);
}
Ok(list.len())
})?;
Ok(json!({ "id": id.0, "overrides": count }))
}
pub(super) fn symbol_instance_reset(s: &mut Session, p: &Value) -> Result<Value> {
let ids = selected_instances(s, p)?;
if ids.is_empty() {
return Err(EngineError::Other("no symbol instances selected".into()));
}
let d = &s.doc()?.doc;
let changed: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some_and(|n| !n.extras().symbol_overrides.is_empty())).collect();
if !changed.is_empty() {
s.edit("Reset Symbol Instance", |d, _| {
for id in &changed {
if let Some(n) = d.node_mut(*id) {
n.extras_mut().symbol_overrides.clear();
}
}
Ok(())
})?;
}
Ok(json!({ "ids": ids_json(&changed) }))
}
+46 -18
View File
@@ -1,3 +1,4 @@
use super::dynamic::{fit_overrides, type_id, type_param};
use super::{
Affine, AppearanceItem, Arc, CURRENT_SYMBOL, Color, Document, EngineError, FillLayer, HALF, Node, NodeId, NodeKind, Paint, Point, Result,
SIZES_KEY, Session, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration, Value, Vec2, bad, brush, color_value, ids_json, json,
@@ -128,6 +129,7 @@ pub(super) fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
"registration": d.symbol_registration(&sym.name).0,
"exportType": d.symbol_export_type(&sym.name).id(),
"nineSlice": nine_slice_json(d, &sym.name),
"type": type_id(sym.dynamic),
})
})
.collect();
@@ -138,7 +140,7 @@ pub(super) fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
/// current symbol.
pub(crate) fn store_symbol(d: &mut Document, art: &Node, name: &str, size: (f64, f64)) {
let art = d.reid(art);
d.symbols.push(Symbol { name: name.into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
set_natural_size(d, name, Some(size));
d.unknown.insert(CURRENT_SYMBOL.into(), json!(name));
}
@@ -179,10 +181,14 @@ pub(super) fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
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();
let dynamic = type_param(p, C)?.unwrap_or(false);
// 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| {
store_symbol(d, &art, &name, size);
if let Some(sym) = d.symbols.iter_mut().find(|x| x.name == name) {
sym.dynamic = dynamic;
}
d.set_symbol_registration(&name, reg);
d.set_symbol_export_type(&name, export);
d.set_symbol_nine_slice(&name, nine);
@@ -190,9 +196,14 @@ pub(super) fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
sel.set([id]);
Ok(id)
})?;
Ok(
json!({ "name": name, "id": id.0, "registration": reg.0, "exportType": export.id(), "nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value) }),
)
Ok(json!({
"name": name,
"id": id.0,
"registration": reg.0,
"exportType": export.id(),
"nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value),
"type": type_id(dynamic),
}))
}
pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
@@ -201,9 +212,10 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
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 dynamic = type_param(p, C)?;
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() && nine.is_none() {
return Err(bad(C, "pass newName, registration, exportType and/or nineSlice"));
if new_name.is_none() && export.is_none() && reg.is_none() && nine.is_none() && dynamic.is_none() {
return Err(bad(C, "pass newName, registration, exportType, nineSlice and/or type"));
}
if let Some(ref n) = new_name
&& n != &name
@@ -241,6 +253,13 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(g) = nine {
d.set_symbol_nine_slice(&final_name, g);
}
if let Some(dynamic) = dynamic
&& let Some(sym) = d.symbols.iter_mut().find(|x| x.name == final_name)
{
// A static symbol's instances drop their own appearance changes.
sym.dynamic = dynamic;
fit_overrides(d, &instances_of(d, Some(&final_name)));
}
Ok(())
})?;
let d = &s.doc()?.doc;
@@ -249,6 +268,7 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
"registration": d.symbol_registration(&final_name).0,
"exportType": d.symbol_export_type(&final_name).id(),
"nineSlice": nine_slice_json(d, &final_name),
"type": type_id(d.symbols.iter().any(|x| x.name == final_name && x.dynamic)),
}))
}
@@ -279,11 +299,12 @@ pub(super) fn symbol_place(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "id": id.0 }))
}
/// The plain art of instance `inst` in document space (fresh ids).
fn expanded_instance(d: &mut Document, inst: &Node) -> Option<Node> {
/// The plain art of instance `inst` in document space (fresh ids), with the instance's own
/// appearance changes to a dynamic symbol's art when `own`.
fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option<Node> {
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return None };
let sym = d.symbols.iter().find(|s| &s.name == symbol)?;
let mut art = (*sym.art).clone();
let mut art = if own { vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, inst) } else { (*sym.art).clone() };
if let Some(f) = inst.appearance.fill()
&& let Some(c) = f.paint.color()
{
@@ -302,11 +323,13 @@ fn expanded_instance(d: &mut Document, inst: &Node) -> Option<Node> {
Some(art)
}
fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {
/// Replace instances `ids` with their art ([`expanded_instance`], `own` as there). Returns the
/// art's ids.
fn break_links(d: &mut Document, ids: &[NodeId], own: bool) -> Result<Vec<NodeId>> {
let mut out = vec![];
for id in ids {
let Some(inst) = d.node(*id).cloned() else { continue };
let Some(art) = expanded_instance(d, &inst) else { continue };
let Some(art) = expanded_instance(d, &inst, own) else { continue };
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
let aid = art.id;
d.remove(*id)?;
@@ -316,7 +339,7 @@ fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {
Ok(out)
}
fn selected_instances(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
pub(super) fn selected_instances(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
let d = &s.doc()?.doc;
let mut out = vec![];
for id in roots(s, p)? {
@@ -337,7 +360,7 @@ pub(super) fn symbol_break_link(s: &mut Session, p: &Value) -> Result<Value> {
return Err(EngineError::Other("no symbol instances selected".into()));
}
let out = s.edit("Break Link to Symbol", |d, sel| {
let out = break_links(d, &ids)?;
let out = break_links(d, &ids, true)?;
sel.set(out.iter().copied());
Ok(out)
})?;
@@ -351,8 +374,9 @@ pub(super) fn symbol_edit(s: &mut Session, p: &Value) -> Result<Value> {
Some(NodeKind::SymbolInstance { symbol, .. }) => symbol.clone(),
_ => return Err(EngineError::Other("select a symbol instance".into())),
};
// The symbol's own art: one instance's appearance changes would become every instance's.
let out = s.edit("Edit Symbol", |d, sel| {
let out = break_links(d, &[first])?;
let out = break_links(d, &[first], false)?;
sel.set(out.iter().copied());
Ok(out)
})?;
@@ -377,11 +401,14 @@ pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
}
set_natural_size(d, &name, Some(size));
let fix = Affine::scale_non_uniform(size.0 / old.0, size.1 / old.1);
for i in instances_of(d, Some(&name)) {
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(i).map(|n| &mut n.kind) {
let instances = instances_of(d, Some(&name));
for i in &instances {
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(*i).map(|n| &mut n.kind) {
*xf *= fix;
}
}
// Each instance keeps its appearance changes to the objects the new art still has.
fit_overrides(d, &instances);
let reg = d.symbol_registration(&name);
let id = instance_in_place(d, &ids, &name, place_xf(c + reg.offset_from_center(size.0, size.1), size, reg))?;
sel.set([id]);
@@ -400,7 +427,7 @@ pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
s.edit("Delete Symbol", |d, _| {
let inst = instances_of(d, Some(&name));
if expand {
break_links(d, &inst)?;
break_links(d, &inst, true)?;
} else {
remove_each(d, &inst)?;
}
@@ -430,7 +457,7 @@ pub(super) fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
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) });
d.symbols.push(Symbol { dynamic: sym.dynamic, 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);
@@ -462,6 +489,7 @@ pub(super) fn symbol_replace(s: &mut Session, p: &Value) -> Result<Value> {
*xf *= Affine::scale_non_uniform(new.0 / old.0, new.1 / old.1);
}
}
fit_overrides(d, &ids);
Ok(())
})?;
Ok(json!({ "ids": ids_json(&ids) }))
+4 -2
View File
@@ -682,10 +682,12 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br
// Type, images, symbol instances, live objects (and paths whose Transform effect copies
// them with a gradient or changes their fills and strokes): through their outlines.
let symbol = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
NodeKind::SymbolInstance { symbol, .. } => {
d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, &n))
}
_ => None,
};
effects::expand_outlined(Some(&*d), &v, symbol.as_deref()).map(|mut m| {
effects::expand_outlined(Some(&*d), &v, symbol.as_ref()).map(|mut m| {
expand_pieces(d, &mut m, true, brushes, &mut stroke_art);
m
})
+1 -1
View File
@@ -181,7 +181,7 @@ fn basic_shapes() -> SymbolLibrary {
let id = doc.alloc_id();
let art = Node::path(id, path, Appearance::basic(Paint::solid(fill), Paint::solid(edge), 1.0));
let Ok((art, size, _)) = normalize(art) else { continue };
doc.symbols.push(Symbol { name: name.into(), art: Arc::new(art) });
doc.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
set_natural_size(&mut doc, name, Some(size));
}
doc.title = "Basic Shapes".into();
+1
View File
@@ -41,6 +41,7 @@ mod dashalign;
mod distort;
mod docsetup;
mod draw2;
mod dynamic_symbols;
mod editcolors;
mod effectedit;
mod emptytype;
+1 -1
View File
@@ -256,7 +256,7 @@ fn a_file_with_script_proportions_out_of_range_opens_with_them_in_range() {
}
let mut art = d.node(id).unwrap().clone();
art.id = NodeId(9000);
d.symbols.push(vectorcraft_doc::Symbol { name: "Formula".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Formula".into(), art: Arc::new(art) });
if let Some(g) = d.node_mut(graph).and_then(|n| n.extras_if_any()).and_then(|e| e.graph.as_deref_mut()) {
let position = CharPosition::Subscript(ScriptMetrics { size: 0.0, position: 1000.0 });
g.label_styles.legend = Some(vectorcraft_doc::CharStyle { position, small_caps: Some(0.0), ..vectorcraft_doc::CharStyle::default() });
+165
View File
@@ -0,0 +1,165 @@
//! Dynamic symbols (#4938): an instance changes the appearance of objects in its symbol's art and
//! stays linked; editing the symbol keeps those changes, Reset and Break Link handle them.
use serde_json::{Value, json};
use vectorcraft_doc::{Node, NodeKind};
use vectorcraft_geom::Rect;
use crate::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap();
s
}
fn id(v: &Value) -> NodeId {
NodeId(v["id"].as_u64().unwrap())
}
/// A blue square at `x` (40 pt, no stroke).
fn square(s: &mut Session, x: f64) -> NodeId {
let r = id(&s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 40, "height": 40})).unwrap());
s.execute("paint.setFill", &json!({"ids": [r.0], "color": "#0000ff"})).unwrap();
s.execute("paint.setStroke", &json!({"ids": [r.0], "none": true})).unwrap();
r
}
/// Symbol "Pair" of two blue squares (x 10..50 and 60..100) of `kind`, made in place: its instance.
fn pair(s: &mut Session, kind: &str) -> NodeId {
let (a, b) = (square(s, 10.0), square(s, 60.0));
id(&s.execute("symbol.new", &json!({"name": "Pair", "ids": [a.0, b.0], "type": kind})).unwrap())
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).unwrap().clone()
}
fn overrides(s: &Session, id: NodeId) -> usize {
node(s, id).extras().symbol_overrides.len()
}
fn pixel(s: &Session, x: u32, y: u32) -> [u8; 4] {
vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 100.0), 1.0, true).pixel(x, y)
}
const RED: [u8; 4] = [255, 0, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
#[test]
fn a_dynamic_instance_paints_one_object_its_own_way_and_stays_linked() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
assert_eq!(s.execute("symbol.list", &json!({})).unwrap()["symbols"][0]["type"], "dynamic");
let other = id(&s.execute("symbol.place", &json!({"name": "Pair", "x": 55, "y": 80})).unwrap());
let r = s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000"})).unwrap();
assert_eq!(r["overrides"], 1);
assert!(matches!(node(&s, inst).kind, NodeKind::SymbolInstance { .. }), "still an instance");
assert_eq!(pixel(&s, 80, 30), RED, "the changed object");
assert_eq!(pixel(&s, 30, 30), BLUE, "its sibling keeps the symbol's look");
assert_eq!(overrides(&s, other), 0, "other instances keep the symbol's look");
// A later change to the same object merges with the first; null takes one back.
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "opacity": 50})).unwrap();
assert_eq!(node(&s, inst).extras().symbol_overrides[0].opacity, Some(0.5));
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "opacity": null})).unwrap();
let o = node(&s, inst).extras().symbol_overrides[0].clone();
assert!(o.opacity.is_none() && o.fill.is_some());
// SVG export draws it too.
let svg = s.execute("document.export", &json!({"format": "svg"})).unwrap();
let svg = String::from_utf8(vectorcraft_format::base64_decode(svg["dataBase64"].as_str().unwrap()).unwrap()).unwrap();
assert!(svg.to_ascii_lowercase().contains("#ff0000") || svg.contains("rgb(255,0,0)"), "{svg}");
}
#[test]
fn static_symbols_and_bad_targets_are_refused() {
let mut s = session();
let inst = pair(&mut s, "static");
let set = |s: &mut Session, p: Value| s.execute("symbol.instance.setAppearance", &p);
assert!(set(&mut s, json!({"id": inst.0, "path": [0], "fill": "#ff0000"})).is_err(), "a static symbol");
s.execute("symbol.options", &json!({"name": "Pair", "type": "dynamic"})).unwrap();
for bad in [
json!({"id": inst.0, "path": [2], "fill": "#ff0000"}),
json!({"id": inst.0, "path": [0, 0], "fill": "#ff0000"}),
json!({"id": inst.0, "path": [-1], "fill": "#ff0000"}),
json!({"id": inst.0, "path": "0", "fill": "#ff0000"}),
json!({"id": inst.0, "fill": "#ff0000"}),
json!({"id": inst.0, "path": [0]}),
json!({"id": inst.0, "path": [0], "blend": "sparkle"}),
json!({"id": inst.0, "path": [0], "opacity": "half"}),
json!({"id": inst.0, "path": [0], "fill": {}}),
] {
assert!(set(&mut s, bad.clone()).is_err(), "{bad}");
}
let plain = square(&mut s, 120.0);
assert!(set(&mut s, json!({"id": plain.0, "path": [], "fill": "#ff0000"})).is_err(), "not an instance");
assert!(s.execute("symbol.options", &json!({"name": "Pair", "type": "sometimes"})).is_err());
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn redefining_the_symbol_keeps_the_changes_whose_objects_remain() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000"})).unwrap();
// New geometry with the same structure: two squares, now 10..40 and 50..80 tall.
let (a, b) = (square(&mut s, 10.0), square(&mut s, 120.0));
s.execute("symbol.update", &json!({"name": "Pair", "ids": [a.0, b.0]})).unwrap();
assert_eq!(overrides(&s, inst), 1, "the change follows the edited symbol");
// Art of one object (no children) leaves path [1] behind.
let c = square(&mut s, 10.0);
s.execute("symbol.update", &json!({"name": "Pair", "ids": [c.0]})).unwrap();
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn reset_and_static_drop_the_changes_and_undo_brings_them_back() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [0], "fill": "#ff0000", "blend": "multiply"})).unwrap();
let r = s.execute("symbol.instance.reset", &json!({"ids": [inst.0]})).unwrap();
assert_eq!(r["ids"], json!([inst.0]));
assert_eq!(overrides(&s, inst), 0);
assert_eq!(pixel(&s, 30, 30), BLUE);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(overrides(&s, inst), 1);
// Nothing to reset: no undo step.
let other = id(&s.execute("symbol.place", &json!({"name": "Pair"})).unwrap());
let undo = s.doc().unwrap().history.undo.len();
assert_eq!(s.execute("symbol.instance.reset", &json!({"ids": [other.0]})).unwrap()["ids"], json!([]));
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
// Static again: the instances show the symbol's appearance.
s.execute("symbol.options", &json!({"name": "Pair", "type": "static"})).unwrap();
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn break_link_bakes_the_changes_and_edit_symbol_shows_the_symbols_own_art() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000"})).unwrap();
let other = id(&s.execute("symbol.place", &json!({"name": "Pair", "x": 55, "y": 80})).unwrap());
s.execute("symbol.instance.setAppearance", &json!({"id": other.0, "path": [1], "fill": "#ff0000"})).unwrap();
let fills = |s: &Session, art: NodeId| -> Vec<String> {
node(s, art).children().unwrap().iter().map(|c| c.appearance.fill_paint().color().unwrap().to_hex()).collect()
};
let art = NodeId(s.execute("symbol.breakLink", &json!({"ids": [inst.0]})).unwrap()["ids"][0].as_u64().unwrap());
assert_eq!(fills(&s, art), ["#0000ff", "#ff0000"]);
let art = NodeId(s.execute("symbol.edit", &json!({"ids": [other.0]})).unwrap()["ids"][0].as_u64().unwrap());
assert_eq!(fills(&s, art), ["#0000ff", "#0000ff"], "the symbol's art, without this instance's changes");
}
#[test]
fn dynamic_symbols_and_their_changes_round_trip_and_old_documents_save_as_before() {
let mut s = session();
pair(&mut s, "static");
let json = serde_json::to_string(&s.doc().unwrap().doc).unwrap();
assert!(!json.contains("\"dynamic\"") && !json.contains("symbol_overrides"), "nothing new is written for static symbols");
let mut s = session();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000", "opacity": 25})).unwrap();
let doc = &s.doc().unwrap().doc;
let back = vectorcraft_format::load(&vectorcraft_format::save(doc, false)).unwrap();
assert!(back.symbols[0].dynamic);
assert_eq!(back.node(inst).unwrap().extras().symbol_overrides, doc.node(inst).unwrap().extras().symbol_overrides);
assert_eq!(back.symbols, doc.symbols);
}
+1 -1
View File
@@ -274,7 +274,7 @@ fn symbols_bend_with_the_envelope() {
shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)),
vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
);
d.symbols.push(Symbol { name: "Box".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Box".into(), art: Arc::new(art) });
let id = d.alloc_id();
let xf = Affine::translate((200.0, 150.0)) * Affine::scale_non_uniform(10.0, 5.0);
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Box".into(), xf }))?;
+2 -2
View File
@@ -119,9 +119,9 @@ fn groups_layers_type_images_and_symbols_keep_their_raster_effects() {
// A symbol instance with a shadow, and a symbol whose art blurs.
let mut d = Document::new(200.0, 200.0);
let art = rect(&mut d, Rect::new(0.0, 0.0, 50.0, 50.0), Color::rgb(0.9, 0.6, 0.0));
d.symbols.push(Symbol { name: "Tile".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Tile".into(), art: Arc::new(art) });
let blurred = with(rect(&mut d, Rect::new(0.0, 0.0, 40.0, 40.0), Color::rgb(0.2, 0.2, 0.8)), blur());
d.symbols.push(Symbol { name: "Soft".into(), art: Arc::new(blurred) });
d.symbols.push(Symbol { dynamic: false, name: "Soft".into(), art: Arc::new(blurred) });
let inst = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Tile".into(), xf: Affine::translate((30.0, 30.0)) });
add(&mut d, with(inst, shadow()));
let soft = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Soft".into(), xf: Affine::translate((120.0, 120.0)) });
+6 -4
View File
@@ -32,7 +32,7 @@ fn star_source(path: &str, title: &str) {
d.title = title.into();
let layer = d.layers[0].id;
let art = Node::path(vectorcraft_doc::NodeId(900), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)), look(BLUE));
d.symbols.push(vectorcraft_doc::Symbol { name: "Star".into(), art: std::sync::Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Star".into(), art: std::sync::Arc::new(art) });
let id = d.alloc_id();
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Star".into(), xf: vectorcraft_geom::Affine::IDENTITY })).unwrap();
write(path, &vectorcraft_format::save_file(&d));
@@ -45,9 +45,11 @@ fn look(c: [u8; 3]) -> Appearance {
/// The active document gets a red symbol named `Star` of its own.
fn parent_star(s: &mut Session) {
let star = Node::path(vectorcraft_doc::NodeId(901), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), look(RED));
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc)
.symbols
.push(vectorcraft_doc::Symbol { name: "Star".into(), art: std::sync::Arc::new(star) });
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).symbols.push(vectorcraft_doc::Symbol {
dynamic: false,
name: "Star".into(),
art: std::sync::Arc::new(star),
});
}
/// Place `path` (linked: the default), centred on the artboard → its id.
+2 -2
View File
@@ -380,13 +380,13 @@ fn an_object_has_the_art_of_at_most_4096_symbol_instances_drawn() {
let mut filled = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
filled.effects.push(Effect { id: "path.offsetPath".into(), params: json!({"offset": 1}), visible: true });
let square = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), filled);
d.symbols.push(Symbol { name: "S0".into(), art: Arc::new(square) });
d.symbols.push(Symbol { dynamic: false, name: "S0".into(), art: Arc::new(square) });
for k in 1..=12 {
let mut instance =
|dx: f64| Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: format!("S{}", k - 1), xf: Affine::translate((dx, 0.0)) }));
let pair = vec![instance(0.0), instance(20.0 * f64::from(1 << (k - 1)))];
let art = Node::group(d.alloc_id(), pair);
d.symbols.push(Symbol { name: format!("S{k}"), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: format!("S{k}"), art: Arc::new(art) });
}
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S12".into(), xf: Affine::IDENTITY });
let (layer, id) = (d.layers[0].id, instance.id);
+9 -9
View File
@@ -115,7 +115,7 @@ fn set_bounds_with_use_preview_bounds_keeps_the_strokes_in_the_symbol_at_their_w
let mut d = Document::new(200.0, 120.0);
let stroked = Appearance::basic(Paint::solid(Color::BLACK), Paint::solid(Color::BLACK), 2.0);
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 8.0, 8.0)), stroked);
d.symbols.push(Symbol { name: "Box".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Box".into(), art: Arc::new(art) });
let mut instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Box".into(), xf: Affine::translate((50.0, 50.0)) });
*instance.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0);
let (layer, id) = (d.layers[0].id, instance.id);
@@ -136,7 +136,7 @@ fn set_bounds_with_use_preview_bounds_keeps_the_strokes_in_the_symbol_at_their_w
let mut round = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0);
round.stroke_mut().unwrap().join = vectorcraft_doc::LineJoin::Round;
let art = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 8.0, 8.0)), round);
d.symbols.push(Symbol { name: "Ring".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Ring".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Ring".into(), xf: Affine::translate((50.0, 50.0)) });
let (layer, id) = (d.layers[0].id, instance.id);
d.insert(Some(layer), 0, instance).unwrap();
@@ -164,7 +164,7 @@ fn set_bounds_sizes_an_instance_and_a_path_whose_strokes_differ() {
Node::path(d.alloc_id(), shapes::rectangle(Rect::new(at, at, at + 10.0, at + 10.0)), stroked)
};
let art = square(&mut d, 0.0, 10.0);
d.symbols.push(Symbol { name: "Thick".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Thick".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Thick".into(), xf: Affine::translate((50.0, 50.0)) });
let path = square(&mut d, 50.0, 2.0);
let (layer, ids) = (d.layers[0].id, [instance.id, path.id]);
@@ -202,7 +202,7 @@ fn set_bounds_measures_the_objects_in_an_instances_art_one_by_one() {
*art.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
*instance.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
}
d.symbols.push(Symbol { name: "Mixed".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Mixed".into(), art: Arc::new(art) });
let mut roots = vec![if grouped { Node::group(d.alloc_id(), vec![Arc::new(instance)]) } else { instance }];
if plain {
let filled = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
@@ -271,7 +271,7 @@ fn set_bounds_measures_an_instance_as_one_object_past_4096_objects_in_its_art()
art.push(Arc::new(Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, y, x + 1.0, y + 1.0)), square)));
}
let art = Node::group(d.alloc_id(), art);
d.symbols.push(Symbol { name: "Many".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Many".into(), art: Arc::new(art) });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Many".into(), xf: Affine::translate((0.0, 100.0)) });
let (layer, id) = (d.layers[0].id, instance.id);
d.insert(Some(layer), 0, instance).unwrap();
@@ -293,7 +293,7 @@ fn set_bounds_measures_a_turned_instance_by_the_box_around_its_turned_art_box()
let mut d = Document::new(400.0, 400.0);
let filled = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
let circle = Node::path(d.alloc_id(), shapes::ellipse(Rect::new(-10.0, -10.0, 10.0, 10.0)), filled);
d.symbols.push(Symbol { name: "Disc".into(), art: Arc::new(circle) });
d.symbols.push(Symbol { dynamic: false, name: "Disc".into(), art: Arc::new(circle) });
let xf = Affine::translate((100.0, 100.0)) * Affine::rotate(30f64.to_radians());
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Disc".into(), xf });
let (layer, id) = (d.layers[0].id, instance.id);
@@ -327,7 +327,7 @@ fn far_on_artboard_2() -> (Session, NodeId) {
let mut d = Document::new(800.0, 600.0);
let rect = shapes::rectangle(Rect::new(1100.0, 50.0, 1140.0, 70.0));
let art = Node::path(d.alloc_id(), rect, Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
d.symbols.push(Symbol { name: "Far".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Far".into(), art: Arc::new(art) });
let (layer, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((100.0, 100.0)) })).unwrap();
let mut s = Session::new();
@@ -437,7 +437,7 @@ fn an_artboards_svg_keeps_an_instance_whose_brush_art_is_on_it() {
let mut stroke = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
stroke.stroke_mut().unwrap().brush = Some("3 pt. Round".into());
let line = Node::path(d.alloc_id(), shapes::line((0.0, 40.0).into(), (0.0, 60.0).into()), stroke);
d.symbols.push(Symbol { name: "Nib".into(), art: Arc::new(line) });
d.symbols.push(Symbol { dynamic: false, name: "Nib".into(), art: Arc::new(line) });
let (layer, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Nib".into(), xf: Affine::translate((-5.0, 0.0)) })).unwrap();
let mut s = Session::new();
@@ -462,7 +462,7 @@ fn an_artboards_svg_keeps_an_instance_whose_shadow_is_on_it() {
} else {
instance.appearance.effects.push(shadow);
}
d.symbols.push(Symbol { name: "Dot".into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: "Dot".into(), art: Arc::new(art) });
let layer = d.layers[0].id;
d.insert(Some(layer), 0, instance).unwrap();
let mut s = Session::new();
+1 -1
View File
@@ -247,7 +247,7 @@ fn a_file_with_type_sizes_out_of_range_opens_with_them_in_range() {
(h.size, h.h_scale, h.v_scale, h.baseline_shift, h.leading) = (f64::MAX, f64::MAX, f64::MAX, f64::MAX, Some(f64::MAX));
(k.size, k.h_scale, k.v_scale, k.baseline_shift, k.leading) = (0.001, 0.5, 0.5, 5000.0, Some(0.0));
let art = d.node(id).unwrap().clone();
d.symbols.push(vectorcraft_doc::Symbol { name: "Lines".into(), art: Arc::new(art) });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Lines".into(), art: Arc::new(art) });
let attrs = json!({"size": 0, "h_scale": 1e9, "v_scale": -1, "baseline_shift": -1e9, "leading": -1});
d.char_styles.push(vectorcraft_doc::TextStyleDef { name: "Broken".into(), attrs: attrs.as_object().unwrap().clone() });
let attrs = json!({"size": 40, "leading": null});
+2 -2
View File
@@ -4312,7 +4312,7 @@ fn brushed_document(kind: usize, art: usize) -> Document {
Node::path(doc.alloc_id(), square(), Appearance::basic(fill, Paint::None, 0.0))
};
let (in_symbol, in_pattern) = (brushed(&mut doc), brushed(&mut doc));
doc.symbols.push(Symbol { name: "s0".into(), art: std::sync::Arc::new(in_symbol) });
doc.symbols.push(Symbol { dynamic: false, name: "s0".into(), art: std::sync::Arc::new(in_symbol) });
doc.patterns.push(PatternDef::new("p0", vec![std::sync::Arc::new(in_pattern)]));
let art = match art {
0 => instance(&mut doc),
@@ -4502,7 +4502,7 @@ fn symbols_document(count: usize, levels: usize, last: &str) -> Document {
for _ in 0..levels {
art = Node::group(doc.alloc_id(), vec![std::sync::Arc::new(art)]);
}
doc.symbols.push(Symbol { name: format!("s{i}"), art: std::sync::Arc::new(art) });
doc.symbols.push(Symbol { dynamic: false, name: format!("s{i}"), art: std::sync::Arc::new(art) });
}
let first = instance(&mut doc, "s0".into());
let layer = doc.layers[0].id;