mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 06:07:06 -06:00
Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol
This commit is contained in:
@@ -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() };
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) }))
|
||||
}
|
||||
@@ -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) }))
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -41,6 +41,7 @@ mod dashalign;
|
||||
mod distort;
|
||||
mod docsetup;
|
||||
mod draw2;
|
||||
mod dynamic_symbols;
|
||||
mod editcolors;
|
||||
mod effectedit;
|
||||
mod emptytype;
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }))?;
|
||||
|
||||
@@ -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)) });
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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});
|
||||
|
||||
Reference in New Issue
Block a user