M6: Sketch import: shared styles and colour variables

Shared layer styles become a paint style for their fills, a paint style
for their border colours (linked to strokes) and an effect style; shared
text styles become text styles (colour stays on the layers); local and
library copies alike, unused ones listed too. Layers link to them, keeping
their own values where they differ. Color Variables (swatches, also
library ones) become colour variables in a Color Variables collection,
with fills, borders and text colours bound to them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jindřich Šaršon
2026-10-22 00:27:20 +02:00
co-authored by Claude Opus 5.5
parent 4437d49a72
commit 4be7017ed1
8 changed files with 345 additions and 15 deletions
+3
View File
@@ -16,6 +16,9 @@ mod fixture;
mod path;
#[cfg(test)]
mod path_tests;
mod shared;
#[cfg(test)]
mod shared_tests;
mod style;
#[cfg(test)]
mod style_tests;
+4 -5
View File
@@ -139,9 +139,10 @@ pub(crate) fn shape_group(cx: &mut Cx, l: &Value, parent: Affine, depth: usize,
}
let visible = group.get("visible").and_then(Value::as_bool).unwrap_or(true) && m.get("visible").and_then(Value::as_bool).unwrap_or(true);
m.insert("visible".into(), json!(visible));
if let Some(st) = l.get("style") {
if l.get("style").is_some() {
super::style::clear(m);
super::style::apply(cx, st, cw, ch, m);
m.remove("styles");
super::tree::styles(cx, l, cw, ch, m);
}
super::tree::mask(l, m);
}
@@ -168,9 +169,7 @@ pub(crate) fn shape_group(cx: &mut Cx, l: &Value, parent: Affine, depth: usize,
node.insert("booleanOperation".into(), json!(op));
node.insert("children".into(), Value::Array(chain));
super::tree::mask(l, &mut node);
if let Some(st) = l.get("style") {
super::style::apply(cx, st, w, h, &mut node);
}
super::tree::styles(cx, l, w, h, &mut node);
Some(Value::Object(node))
}
+134
View File
@@ -0,0 +1,134 @@
//! Sketch shared styles and swatches → REST style definitions and colour variables.
//!
//! A Sketch layer style holds fills, borders and effects together; FigCraft styles hold one kind
//! each, so a layer style becomes up to three styles named after it: its fills (a paint style),
//! its border colours (a paint style for strokes; widths stay on the layers) and its effects. A
//! text style becomes a text style; its colour stays on the layers, as FigCraft text styles carry
//! none. Layers link to them by `style.sharedObjectID`. Swatches (Color Variables, Sketch 69+)
//! become colour variables in one collection; colours name theirs by `swatchID`.
use std::collections::BTreeMap;
use serde_json::{Map as JsonMap, Value, json};
use super::tree::{Cx, color, str_field};
/// Most shared styles or swatches read from one file.
const MAX_SHARED: usize = 20_000;
/// The collection swatches become variables in.
const COLLECTION: &str = "sketch-color-variables";
const MODE: &str = "default";
/// A file's shared styles and swatches.
#[derive(Default)]
pub(crate) struct Shared {
/// `(id, name, style)` of layer styles, then text styles.
layer: Vec<(String, String, Value)>,
text: Vec<(String, String, Value)>,
swatches: BTreeMap<String, (String, Value)>,
}
impl Shared {
pub(crate) fn read(document: &Value) -> Shared {
let mut s = Shared::default();
let objects =
|key: &str| -> Vec<Value> { document.get(key).and_then(|c| c.get("objects")).and_then(Value::as_array).cloned().unwrap_or_default() };
let foreign = |key: &str, inner: &str| -> Vec<Value> {
document.get(key).and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default().iter().filter_map(|f| f.get(inner).cloned()).collect()
};
let entry = |o: &Value| {
Some((
str_field(o, "do_objectID")?.to_string(),
str_field(o, "name").unwrap_or("Style").chars().take(200).collect(),
o.get("value")?.clone(),
))
};
for o in objects("layerStyles").iter().chain(&foreign("foreignLayerStyles", "localSharedStyle")).take(MAX_SHARED) {
s.layer.extend(entry(o));
}
for o in objects("layerTextStyles").iter().chain(&foreign("foreignTextStyles", "localSharedStyle")).take(MAX_SHARED) {
s.text.extend(entry(o));
}
let foreign_swatches = [foreign("foreignSwatches", "localSwatch"), foreign("foreignSwatches", "localSharedSwatch")].concat();
for o in objects("sharedSwatches").iter().chain(&foreign_swatches).take(MAX_SHARED) {
if let Some((id, name, value)) = entry(o)
&& let Some(c) = color(&value)
{
s.swatches.entry(id).or_insert((name, c));
}
}
s
}
pub(crate) fn has_swatch(&self, id: &str) -> bool {
self.swatches.contains_key(id)
}
/// The REST `styleDefinitions` of every shared style.
pub(crate) fn definitions(cx: &mut Cx) -> Vec<Value> {
let layer = cx.shared.layer.clone();
let text = cx.shared.text.clone();
let mut out = Vec::new();
for (id, name, style) in &layer {
let fills = super::style::fills(cx, style, 100.0, 100.0);
if !fills.is_empty() {
out.push(json!({"id": format!("{id}:fill"), "name": name, "styleType": "FILL", "node": {"fills": fills}}));
}
let strokes = super::style::border_paints(cx, style, 100.0, 100.0);
if !strokes.is_empty() {
out.push(json!({"id": format!("{id}:stroke"), "name": format!("{name} border"), "styleType": "FILL", "node": {"fills": strokes}}));
}
let effects = super::style::effects(cx, style);
if !effects.is_empty() {
out.push(json!({"id": format!("{id}:effect"), "name": name, "styleType": "EFFECT", "node": {"effects": effects}}));
}
}
for (id, name, style) in &text {
let Some(enc) = style.get("textStyle").and_then(|t| t.get("encodedAttributes")) else { continue };
let mut ts = super::text::type_style(cx, enc);
if let Some(m) = ts.as_object_mut() {
m.remove("fills");
}
out.push(json!({"id": id, "name": name, "styleType": "TEXT", "node": {"type": "TEXT", "characters": "", "style": ts}}));
}
out
}
/// Link `node` (layer `l`) to the styles made from its shared style.
pub(crate) fn link(&self, cx_defs: &[String], l: &Value, node: &mut JsonMap<String, Value>) {
let Some(sid) = l.get("style").and_then(|s| str_field(s, "sharedObjectID")) else { return };
let mut refs = JsonMap::new();
let is_text = str_field(l, "_class") == Some("text");
if is_text && self.text.iter().any(|(id, _, _)| id == sid) {
refs.insert("text".into(), json!(sid));
} else if self.layer.iter().any(|(id, _, _)| id == sid) {
for slot in ["fill", "stroke", "effect"] {
let id = format!("{sid}:{slot}");
if cx_defs.contains(&id) {
refs.insert(slot.into(), json!(id));
}
}
}
if !refs.is_empty() {
node.insert("styles".into(), Value::Object(refs));
}
}
/// The REST `variableCollections` and `variables` for the swatches (none without swatches).
pub(crate) fn variables(&self) -> Option<(Value, Value)> {
if self.swatches.is_empty() {
return None;
}
let ids: Vec<&String> = self.swatches.keys().collect();
let collections = json!({COLLECTION: {"id": COLLECTION, "name": "Color Variables", "modes": [{"modeId": MODE, "name": "Default"}],
"defaultModeId": MODE, "variableIds": ids}});
let vars: JsonMap<String, Value> = self
.swatches
.iter()
.map(|(id, (name, c))| {
(id.clone(), json!({"id": id, "name": name, "resolvedType": "COLOR", "variableCollectionId": COLLECTION, "valuesByMode": {MODE: c}}))
})
.collect();
Some((collections, Value::Object(vars)))
}
}
+150
View File
@@ -0,0 +1,150 @@
//! Sketch shared styles → FigCraft styles; Color Variables (swatches) → colour variables.
use figcraft_doc::color::PaintKind;
use figcraft_doc::{Document, Node, StyleKind};
use serde_json::{Value, json};
use super::fixture::*;
use super::import;
const BLACK: [f64; 4] = [0.0, 0.0, 0.0, 1.0];
fn open(doc_json: Value, layers: Vec<Value>) -> (Document, Vec<String>) {
let z = sketch_zip("83.2", 139, doc_json, &[page("P", "Page", vec![artboard("A", "Board", 0.0, 0.0, 400.0, 400.0, layers)])], &[]);
import(&z, "f").unwrap()
}
fn node<'d>(doc: &'d Document, name: &str) -> &'d Node {
doc.nodes().find(|n| n.name == name).unwrap_or_else(|| panic!("no {name}"))
}
fn style<'d>(doc: &'d Document, id: &str) -> &'d figcraft_doc::Style {
doc.styles.iter().find(|s| s.id == id).unwrap_or_else(|| panic!("no style {id}: {:?}", doc.styles.iter().map(|s| &s.id).collect::<Vec<_>>()))
}
fn card_style() -> Value {
json!({"_class": "style", "fills": [solid_fill(0.0, 0.0, 1.0, 1.0)], "borders": [],
"shadows": [{"_class": "shadow", "isEnabled": true, "blurRadius": 4, "offsetX": 0, "offsetY": 2, "spread": 0, "color": color(0.0, 0.0, 0.0, 0.3)}],
"innerShadows": []})
}
fn with_shared(d: &mut Value, layer_styles: Vec<Value>, text_styles: Vec<Value>) {
d["layerStyles"]["objects"] = Value::Array(layer_styles);
d["layerTextStyles"]["objects"] = Value::Array(text_styles);
}
fn shared(id: &str, name: &str, value: Value) -> Value {
json!({"_class": "sharedStyle", "do_objectID": id, "name": name, "value": value})
}
fn linked(mut l: Value, style: Value, shared_id: &str) -> Value {
l["style"] = style;
l["style"]["sharedObjectID"] = json!(shared_id);
l
}
#[test]
fn layer_style_splits() {
let mut d = document(&["P"]);
with_shared(&mut d, vec![shared("CARD", "Card", card_style())], vec![]);
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), card_style(), "CARD")]);
let n = node(&doc, "R");
let fill = n.styles.fill.clone().expect("fill style linked");
let effect = n.styles.effect.clone().expect("effect style linked");
assert_eq!(style(&doc, &fill).name, "Card");
assert!(matches!(style(&doc, &fill).kind, StyleKind::Paint { .. }));
assert!(matches!(style(&doc, &effect).kind, StyleKind::Effect { .. }));
assert_eq!(style(&doc, &effect).name, "Card");
}
#[test]
fn layer_style_borders_become_a_stroke_style() {
let mut st = card_style();
st["borders"] =
json!([{"_class": "border", "isEnabled": true, "fillType": 0, "color": color(1.0, 0.0, 0.0, 1.0), "position": 1, "thickness": 2}]);
let mut d = document(&["P"]);
with_shared(&mut d, vec![shared("CARD", "Card", st.clone())], vec![]);
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), st, "CARD")]);
let stroke = node(&doc, "R").styles.stroke.clone().expect("stroke style linked");
assert!(style(&doc, &stroke).name.starts_with("Card"));
}
#[test]
fn unused_shared_styles_are_listed() {
let mut d = document(&["P"]);
with_shared(&mut d, vec![shared("CARD", "Card", card_style())], vec![]);
let (doc, _) = open(d, vec![]);
assert!(doc.styles.iter().any(|s| s.name == "Card"));
}
#[test]
fn text_style_links() {
let enc = json!({"MSAttributedStringFontAttribute": {"_class": "fontDescriptor", "attributes": {"name": "Helvetica-Bold", "size": 32}},
"MSAttributedStringColorAttribute": color(1.0, 0.0, 0.0, 1.0),
"paragraphStyle": {"_class": "paragraphStyle", "alignment": 0, "maximumLineHeight": 40, "minimumLineHeight": 40}});
let st = json!({"_class": "style", "textStyle": {"_class": "textStyle", "encodedAttributes": enc, "verticalAlignment": 0}});
let mut d = document(&["P"]);
with_shared(&mut d, vec![], vec![shared("H1", "Heading/H1", st.clone())]);
let t = linked(text("Title", 0.0, 0.0, 200.0, 40.0, "Hello", &[(0, 5, "Helvetica-Bold", 32.0, [1.0, 0.0, 0.0, 1.0])]), st, "H1");
let (doc, _) = open(d, vec![t]);
let n = node(&doc, "Title");
let sid = n.styles.text.clone().expect("text style linked");
let s = style(&doc, &sid);
assert_eq!(s.name, "Heading/H1");
let StyleKind::Text { style: ts } = &s.kind else { panic!("not a text style") };
assert_eq!((ts.font_family.as_str(), ts.font_style.as_str(), ts.font_size), ("Helvetica", "Bold", 32.0));
assert!(n.fills.iter().any(|p| matches!(p.kind, PaintKind::Solid { color } if color.r == 1.0 && color.g == 0.0)));
}
#[test]
fn locally_changed_style_keeps_link() {
let mut local = card_style();
local["fills"] = json!([solid_fill(0.0, 1.0, 0.0, 1.0)]);
let mut d = document(&["P"]);
with_shared(&mut d, vec![shared("CARD", "Card", card_style())], vec![]);
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), local, "CARD")]);
let n = node(&doc, "R");
assert!(n.styles.fill.is_some());
assert!(matches!(n.fills[0].kind, PaintKind::Solid { color } if color.g == 1.0 && color.b == 0.0));
}
#[test]
fn foreign_styles_too() {
let mut d = document(&["P"]);
d["foreignLayerStyles"] = json!([{"_class": "MSImmutableForeignLayerStyle", "sourceLibraryName": "Kit", "localSharedStyle": shared("FCARD", "Kit card", card_style())}]);
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), card_style(), "FCARD")]);
assert_eq!(style(&doc, node(&doc, "R").styles.fill.as_deref().unwrap()).name, "Kit card");
}
#[test]
fn swatches_are_color_variables() {
let mut d = document(&["P"]);
d["sharedSwatches"] = json!({"_class": "swatchContainer", "objects": [{"_class": "swatch", "do_objectID": "SW1", "name": "Brand/Blue", "value": color(0.06, 0.38, 1.0, 1.0)}]});
d["foreignSwatches"] = json!([{"_class": "MSImmutableForeignSwatch", "sourceLibraryName": "Kit", "localSwatch": {"_class": "swatch", "do_objectID": "SW2", "name": "Kit/Red", "value": color(1.0, 0.0, 0.0, 1.0)}}]);
let mut fill = solid_fill(0.06, 0.38, 1.0, 1.0);
fill["color"]["swatchID"] = json!("SW1");
let mut fill2 = solid_fill(1.0, 0.0, 0.0, 1.0);
fill2["color"]["swatchID"] = json!("SW2");
let (doc, _) = open(
d,
vec![styled(rect("R", 0.0, 0.0, 50.0, 50.0), "fills", json!([fill])), styled(rect("S", 0.0, 0.0, 50.0, 50.0), "fills", json!([fill2]))],
);
let coll = doc.variables.collections.iter().find(|c| c.name == "Color Variables").expect("collection");
let var = doc.variables.variables.iter().find(|v| v.name == "Brand/Blue").expect("variable");
assert_eq!(var.collection, coll.id);
assert!(doc.variables.variables.iter().any(|v| v.name == "Kit/Red"));
assert_eq!(node(&doc, "R").bound.get("fill"), Some(&var.id));
assert!(node(&doc, "S").bound.contains_key("fill"));
}
#[test]
fn unknown_shared_ids_are_ignored() {
let (doc, _) = open(document(&["P"]), vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), card_style(), "NOPE")]);
let n = node(&doc, "R");
assert!(n.styles.fill.is_none());
let mut f = solid_fill(0.0, 0.0, 0.0, 1.0);
f["color"]["swatchID"] = json!("NOPE");
let (doc, _) =
open(document(&["P"]), vec![styled(text("T", 0.0, 0.0, 10.0, 10.0, "x", &[(0, 1, "Helvetica", 9.0, BLACK)]), "fills", json!([f]))]);
assert!(node(&doc, "T").bound.is_empty());
}
+22 -3
View File
@@ -17,7 +17,7 @@ const MAX_LIST: usize = 64;
/// Write the paints, strokes, effects, opacity and blend mode of `style` into `node`, a layer of
/// `w`×`h`. Lists the style doesn't have are left as they are.
pub(crate) fn apply(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonMap<String, Value>) {
let fills: Vec<Value> = list(style, "fills").iter().filter_map(|f| paint(cx, f, w, h)).collect();
let fills = fills(cx, style, w, h);
if !fills.is_empty() {
node.insert("fills".into(), Value::Array(fills));
}
@@ -36,6 +36,16 @@ pub(crate) fn apply(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonM
}
}
/// A style's fills as REST paints.
pub(crate) fn fills(cx: &mut Cx, style: &Value, w: f64, h: f64) -> Vec<Value> {
list(style, "fills").iter().filter_map(|f| paint(cx, f, w, h)).collect()
}
/// A style's borders as REST paints.
pub(crate) fn border_paints(cx: &mut Cx, style: &Value, w: f64, h: f64) -> Vec<Value> {
list(style, "borders").iter().filter_map(|b| paint(cx, b, w, h)).collect()
}
/// Remove what [`apply`] writes, so another style can take its place.
pub(crate) fn clear(node: &mut JsonMap<String, Value>) {
for key in [
@@ -91,6 +101,9 @@ fn paint(cx: &mut Cx, f: &Value, w: f64, h: f64) -> Option<Value> {
}
};
p["visible"] = json!(f.get("isEnabled").and_then(Value::as_bool).unwrap_or(true));
if let Some(var) = swatch(cx, f.get("color")) {
p["boundVariables"] = var;
}
if let Some(ctx) = f.get("contextSettings") {
if let Some(o) = num(ctx, "opacity") {
p["opacity"] = json!(o.clamp(0.0, 1.0));
@@ -131,7 +144,7 @@ fn pt((x, y): (f64, f64)) -> Value {
/// Borders → stroke paints, and the first enabled border's thickness and position.
fn borders(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonMap<String, Value>) {
let list = list(style, "borders");
let strokes: Vec<Value> = list.iter().filter_map(|b| paint(cx, b, w, h)).collect();
let strokes = border_paints(cx, style, w, h);
if strokes.is_empty() {
return;
}
@@ -201,7 +214,7 @@ fn borders(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonMap<String
}
/// Shadows, inner shadows and the blur as REST effects.
fn effects(cx: &mut Cx, style: &Value) -> Vec<Value> {
pub(crate) fn effects(cx: &mut Cx, style: &Value) -> Vec<Value> {
let mut out = Vec::new();
for (key, ty) in [("shadows", "DROP_SHADOW"), ("innerShadows", "INNER_SHADOW")] {
for s in list(style, key) {
@@ -230,6 +243,12 @@ fn effects(cx: &mut Cx, style: &Value) -> Vec<Value> {
out
}
/// The variable binding of a colour that names a swatch the file has (`{color: alias}`).
pub(crate) fn swatch(cx: &Cx, c: Option<&Value>) -> Option<Value> {
let id = c?.get("swatchID").and_then(Value::as_str).filter(|id| cx.shared.has_swatch(id))?;
Some(json!({"color": {"type": "VARIABLE_ALIAS", "id": id}}))
}
/// Sketch's blend modes (0 normal … 17 plus lighter) by REST name.
fn blend(mode: i64) -> Option<&'static str> {
const MODES: [&str; 18] = [
+4 -1
View File
@@ -209,8 +209,11 @@ fn apply(layers: &mut [Value], ov: &BTreeMap<String, Value>, document: &Value, d
_ => {}
}
for (prop, list) in [("layerStyle", "layerStyles"), ("textStyle", "layerTextStyles")] {
if let Some(style) = get(prop).and_then(Value::as_str).and_then(|sid| shared_style(document, list, sid)) {
let sid = get(prop).and_then(Value::as_str);
if let Some((sid, style)) = sid.and_then(|sid| Some((sid, shared_style(document, list, sid)?))) {
l["style"] = style.clone();
// The layer now uses that shared style: it links to it.
l["style"]["sharedObjectID"] = json!(sid);
if prop == "textStyle"
&& let Some(enc) = style.get("textStyle").and_then(|t| t.get("encodedAttributes"))
&& let Some(runs) = l.get_mut("attributedString").and_then(|a| a.get_mut("attributes")).and_then(Value::as_array_mut)
+6 -2
View File
@@ -94,7 +94,7 @@ pub(crate) fn node(cx: &mut Cx, l: &Value, node: &mut JsonMap<String, Value>) {
}
/// A run's attributes as a REST type style (with its colour as `fills`).
fn type_style(cx: &mut Cx, a: &Value) -> Value {
pub(crate) fn type_style(cx: &mut Cx, a: &Value) -> Value {
let font = a.get("MSAttributedStringFontAttribute").and_then(|f| f.get("attributes"));
let ps = font.and_then(|f| f.get("name")).and_then(Value::as_str).unwrap_or("");
let size = font.and_then(|f| f.get("size")).and_then(Value::as_f64).filter(|s| s.is_finite() && *s > 0.0).unwrap_or(12.0);
@@ -126,7 +126,11 @@ fn type_style(cx: &mut Cx, a: &Value) -> Value {
_ => "ORIGINAL",
});
let c = a.get("MSAttributedStringColorAttribute").and_then(color).unwrap_or_else(|| json!({"r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0}));
s["fills"] = json!([{"type": "SOLID", "color": c}]);
let mut paint = json!({"type": "SOLID", "color": c});
if let Some(var) = super::style::swatch(cx, a.get("MSAttributedStringColorAttribute")) {
paint["boundVariables"] = var;
}
s["fills"] = json!([paint]);
s
}
+22 -4
View File
@@ -27,6 +27,9 @@ pub(crate) struct Cx<'a> {
/// `document.json`: shared styles and library copies.
pub document: &'a Value,
pub symbols: super::symbols::Symbols,
pub shared: super::shared::Shared,
/// Ids of the styles made from the shared styles.
pub style_defs: Vec<String>,
/// The symbols being expanded, outermost first (a symbol inside itself stops there).
pub chain: Vec<String>,
pub warnings: Vec<String>,
@@ -49,9 +52,12 @@ impl Cx<'_> {
/// Map the pages of `archive` to a REST file JSON named `name`.
pub(crate) fn to_rest(archive: &Archive, name: &str, max_nodes: usize) -> Map {
let symbols = super::symbols::Symbols::read(&archive.document, &archive.pages);
let shared = super::shared::Shared::read(&archive.document);
let mut cx = Cx {
document: &archive.document,
symbols,
shared,
style_defs: Vec::new(),
chain: Vec::new(),
warnings: Vec::new(),
networks: Vec::new(),
@@ -59,6 +65,8 @@ pub(crate) fn to_rest(archive: &Archive, name: &str, max_nodes: usize) -> Map {
nodes: 0,
max_nodes,
};
let defs = super::shared::Shared::definitions(&mut cx);
cx.style_defs = defs.iter().filter_map(|d| d.get("id").and_then(Value::as_str).map(str::to_string)).collect();
let mut canvases = Vec::new();
for (id, page) in &archive.pages {
let mut node = JsonMap::new();
@@ -75,7 +83,11 @@ pub(crate) fn to_rest(archive: &Archive, name: &str, max_nodes: usize) -> Map {
let libraries = cx.symbols.libraries.join(", ");
cx.warn(format!("symbols from the libraries {libraries} became local components on the page Library symbols"));
}
let json = json!({"name": name, "document": {"type": "DOCUMENT", "id": "0:0", "children": canvases}});
let mut json = json!({"name": name, "document": {"type": "DOCUMENT", "id": "0:0", "children": canvases}, "styleDefinitions": defs});
if let Some((collections, variables)) = cx.shared.variables() {
json["variableCollections"] = collections;
json["variables"] = variables;
}
Map { json, warnings: cx.warnings, networks: cx.networks, too_old: cx.too_old }
}
@@ -147,9 +159,7 @@ pub(crate) fn layer(cx: &mut Cx, l: &Value, parent: Affine, depth: usize, root:
},
_ => {}
}
if let Some(st) = l.get("style") {
super::style::apply(cx, st, w, h, &mut node);
}
styles(cx, l, w, h, &mut node);
if matches!(ty, "FRAME" | "GROUP") {
let kids = children(cx, l, world, (w, h), depth, false);
node.insert("children".into(), Value::Array(kids));
@@ -157,6 +167,14 @@ pub(crate) fn layer(cx: &mut Cx, l: &Value, parent: Affine, depth: usize, root:
Some(Value::Object(node))
}
/// Layer `l`'s style into `node`, and its links to the styles made from its shared style.
pub(crate) fn styles(cx: &mut Cx, l: &Value, w: f64, h: f64, node: &mut JsonMap<String, Value>) {
if let Some(st) = l.get("style") {
super::style::apply(cx, st, w, h, node);
}
cx.shared.link(&cx.style_defs, l, node);
}
/// A clipping mask's flag and kind (outline or alpha).
pub(crate) fn mask(l: &Value, node: &mut JsonMap<String, Value>) {
if bool_field(l, "hasClippingMask") == Some(true) {