mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 13:52:34 +00:00
Add files via upload
This commit is contained in:
1 parent
a5ebc01d2c
commit
6969232326
2 files changed
+203
No files matched your search
@@ -48,6 +48,7 @@ const MAX_ASSET_PART: usize = 256;
|
||||
/// vector networks its layers name by `vectorNetworkRef` (for [`crate::rest::import_with_networks`]).
|
||||
pub(super) fn to_rest(msg: &Kv, name: &str, max_nodes: usize) -> (Value, Vec<String>, Vec<Option<VectorNetwork>>) {
|
||||
let mut m = Mapper::new(msg, max_nodes);
|
||||
let _canon = VarCanon::install(&m);
|
||||
let doc = m.document();
|
||||
// Large values are moved in, never passed through `json!`, which serialises its arguments:
|
||||
// a deep copy of the whole tree, briefly holding it twice.
|
||||
@@ -365,6 +366,30 @@ impl<'a> Mapper<'a> {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// The collection a `VariableSetID` names, as one collection per library asset: a file that
|
||||
/// took several updates of a library holds a copy of the collection for each version it met,
|
||||
/// all with the same asset `key`. Variables and explicit modes may name any of those copies,
|
||||
/// while the mode ids stay the same across them, so every copy maps to the newest one (the
|
||||
/// one [`Self::variables`] keeps, with the modes of the others added). Without this, a page set
|
||||
/// to a mode an older copy didn't have yet lost its mode, and the layers fell back to the
|
||||
/// default mode of the newest copy.
|
||||
pub(super) fn collection_id(&self, v: &Kv) -> Option<String> {
|
||||
let id = self.resource_id(v)?;
|
||||
Some(self.canonical_collection(&id).unwrap_or(id))
|
||||
}
|
||||
|
||||
/// The newest live copy of collection node `id`'s library asset, when it has a key.
|
||||
pub(super) fn canonical_collection(&self, id: &str) -> Option<String> {
|
||||
let key = self.nodes.get(id)?.get("key").and_then(Kv::str).filter(|k| !k.is_empty())?;
|
||||
let live_set = |n: &Kv| n.get("type").and_then(Kv::str) == Some("VARIABLE_SET") && n.get("isSoftDeleted").and_then(Kv::bool) != Some(true);
|
||||
self.assets
|
||||
.get(key)?
|
||||
.iter()
|
||||
.filter(|(_, nid)| self.nodes.get(nid.as_str()).is_some_and(|n| live_set(n)))
|
||||
.max_by_key(|(ver, _)| version_rank(ver))
|
||||
.map(|(_, nid)| nid.clone())
|
||||
}
|
||||
|
||||
/// A note about what this file's mapping left out or changed. Counted, as the REST importer
|
||||
/// counts its own: these notes are met once per layer, so the kinds were named once each with
|
||||
/// no sign of how much of the file they covered -- a file whose every text layer lost a
|
||||
@@ -384,3 +409,82 @@ impl<'a> Mapper<'a> {
|
||||
self.max_nodes.saturating_sub(self.reserved)
|
||||
}
|
||||
}
|
||||
|
||||
/// A library version (`"40727:1183"`) as numbers to order by; unparsable parts rank lowest.
|
||||
fn version_rank(v: &str) -> (u64, u64) {
|
||||
let mut parts = v.split(':').map(|p| p.trim().parse::<u64>().unwrap_or(0));
|
||||
(parts.next().unwrap_or(0), parts.next().unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Library variables, one per asset. A file that took several updates of a library keeps a copy
|
||||
/// of a variable for each version it met (same `key`, another `version`), and layers and other
|
||||
/// variables name them by that asset (`assetRef {key, version}`) rather than by guid. Each
|
||||
/// reference goes to the newest copy: it has the values of every mode the library has now, where
|
||||
/// an older one lacks the modes added since (a layer under a page set to such a mode fell back to
|
||||
/// the default one). Kept per thread while a file is mapped: the paint and variable readers that
|
||||
/// meet references are free functions.
|
||||
#[derive(Default)]
|
||||
pub(super) struct VarCanon {
|
||||
by_key: HashMap<String, String>,
|
||||
by_id: HashMap<String, String>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static VAR_CANON: std::cell::RefCell<Option<VarCanon>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Removes the thread's table when the mapping is done.
|
||||
pub(super) struct VarCanonGuard;
|
||||
|
||||
impl Drop for VarCanonGuard {
|
||||
fn drop(&mut self) {
|
||||
VAR_CANON.with(|c| c.borrow_mut().take());
|
||||
}
|
||||
}
|
||||
|
||||
impl VarCanon {
|
||||
fn install(m: &Mapper) -> VarCanonGuard {
|
||||
let mut newest: HashMap<String, ((u64, u64), String)> = HashMap::new();
|
||||
let mut copies: Vec<(String, String)> = Vec::new();
|
||||
for (id, n) in &m.nodes {
|
||||
if n.get("type").and_then(Kv::str) != Some("VARIABLE") || n.get("isSoftDeleted").and_then(Kv::bool) == Some(true) {
|
||||
continue;
|
||||
}
|
||||
let Some(key) = n.get("key").and_then(Kv::str).filter(|k| !k.is_empty()) else { continue };
|
||||
let rank = version_rank(n.get("version").and_then(Kv::str).unwrap_or(""));
|
||||
copies.push((id.clone(), key.to_string()));
|
||||
let e = newest.entry(key.to_string()).or_insert_with(|| (rank, id.clone()));
|
||||
if rank > e.0 || (rank == e.0 && *id > e.1) {
|
||||
*e = (rank, id.clone());
|
||||
}
|
||||
}
|
||||
let mut t = VarCanon::default();
|
||||
for (id, key) in copies {
|
||||
if let Some((_, canon)) = newest.get(&key) {
|
||||
t.by_id.insert(id, canon.clone());
|
||||
}
|
||||
}
|
||||
t.by_key = newest.into_iter().map(|(k, (_, id))| (k, id)).collect();
|
||||
VAR_CANON.with(|c| *c.borrow_mut() = Some(t));
|
||||
VarCanonGuard
|
||||
}
|
||||
|
||||
/// The variable a reference (`{guid}` or `{assetRef}`) names, as its newest copy.
|
||||
pub(super) fn resolve(r: &Kv) -> Option<String> {
|
||||
let by_guid = r.get("guid").and_then(guid).filter(|g| g != NO_STYLE);
|
||||
VAR_CANON.with(|c| {
|
||||
let c = c.borrow();
|
||||
let Some(t) = c.as_ref() else { return by_guid.clone() };
|
||||
if let Some(g) = &by_guid {
|
||||
return Some(t.by_id.get(g).cloned().unwrap_or_else(|| g.clone()));
|
||||
}
|
||||
let key = r.get("assetRef")?.get("key").and_then(Kv::str)?;
|
||||
t.by_key.get(key).cloned()
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether variable node `id` is an older copy another one replaces.
|
||||
pub(super) fn superseded(id: &str) -> bool {
|
||||
VAR_CANON.with(|c| c.borrow().as_ref().and_then(|t| t.by_id.get(id)).is_some_and(|canon| canon != id))
|
||||
}
|
||||
}
|
||||
@@ -1487,3 +1487,102 @@ fn dump_samples() {
|
||||
|
||||
#[path = "board_import_tests.rs"]
|
||||
mod board;
|
||||
|
||||
/// A file that took several updates of a library holds a copy of its collection and of each of
|
||||
/// its variables per version it met (same asset key, another version). Layers and modes name any
|
||||
/// of them by asset, never by guid: they all become the newest copy, which has every mode — a page
|
||||
/// set to a mode only the newer copies have keeps it, and fills bound to an older copy of a
|
||||
/// variable follow its newest values.
|
||||
#[test]
|
||||
fn library_copies_of_several_versions_become_the_newest() {
|
||||
let mode = |id, name: &'static str, pos: &'static str| o(vec![("id", g(id)), ("name", s(name)), ("sortPosition", s(pos))]);
|
||||
let by_asset = |key: &'static str, version: &'static str| o(vec![("assetRef", o(vec![("key", s(key)), ("version", s(version))]))]);
|
||||
let color_value = |c| o(vec![("value", o(vec![("colorValue", c)])), ("dataType", e("COLOR"))]);
|
||||
let values = |pairs: Vec<(u64, Kv<'static>)>| {
|
||||
o(vec![("entries", arr(pairs.into_iter().map(|(m, c)| o(vec![("modeID", g(m)), ("variableData", color_value(c))])).collect()))])
|
||||
};
|
||||
let asset_alias =
|
||||
|key: &'static str, version: &'static str| o(vec![("value", o(vec![("alias", by_asset(key, version))])), ("dataType", e("ALIAS"))]);
|
||||
let nodes = vec![
|
||||
node(0, None, "DOCUMENT", "Document", vec![]),
|
||||
// The page is set to "Purple", a mode the oldest copy of the collection didn't have yet.
|
||||
node(
|
||||
1,
|
||||
Some((0, "!")),
|
||||
"CANVAS",
|
||||
"Page",
|
||||
vec![(
|
||||
"variableModeBySetMap",
|
||||
o(vec![("entries", arr(vec![o(vec![("variableSetID", by_asset("products", "10:1")), ("variableModeID", g(92))])]))]),
|
||||
)],
|
||||
),
|
||||
node(2, Some((0, "~")), "CANVAS", "Internal", vec![("internalOnly", Kv::Bool(true))]),
|
||||
node(
|
||||
4,
|
||||
Some((2, "!")),
|
||||
"VARIABLE_SET",
|
||||
"Products",
|
||||
vec![("variableSetModes", arr(vec![mode(90, "Blue", "!")])), ("key", s("products")), ("version", s("10:1"))],
|
||||
),
|
||||
node(
|
||||
6,
|
||||
Some((2, "#")),
|
||||
"VARIABLE_SET",
|
||||
"Products",
|
||||
vec![("variableSetModes", arr(vec![mode(90, "Blue", "!"), mode(92, "Purple", "\"")])), ("key", s("products")), ("version", s("20:1"))],
|
||||
),
|
||||
node(
|
||||
5,
|
||||
Some((2, "\"")),
|
||||
"VARIABLE",
|
||||
"button-bg",
|
||||
vec![
|
||||
("variableSetID", by_asset("products", "10:1")),
|
||||
("variableResolvedType", e("COLOR")),
|
||||
("variableDataValues", values(vec![(90, rgb(0.0, 0.0, 1.0))])),
|
||||
("key", s("button-bg")),
|
||||
("version", s("10:1")),
|
||||
],
|
||||
),
|
||||
node(
|
||||
7,
|
||||
Some((2, "$")),
|
||||
"VARIABLE",
|
||||
"button-bg",
|
||||
vec![
|
||||
("variableSetID", by_asset("products", "20:1")),
|
||||
("variableResolvedType", e("COLOR")),
|
||||
("variableDataValues", values(vec![(90, rgb(0.0, 0.0, 1.0)), (92, rgb(0.5, 0.0, 0.5))])),
|
||||
("key", s("button-bg")),
|
||||
("version", s("20:1")),
|
||||
],
|
||||
),
|
||||
// The button names the older copy of the variable.
|
||||
node(
|
||||
10,
|
||||
Some((1, "!")),
|
||||
"FRAME",
|
||||
"Button",
|
||||
vec![
|
||||
("size", xy(100.0, 40.0)),
|
||||
(
|
||||
"fillPaints",
|
||||
arr(vec![o(vec![("type", e("SOLID")), ("color", rgb(0.0, 0.0, 1.0)), ("colorVar", asset_alias("button-bg", "10:1"))])]),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
let (doc, warnings) = import(&canvas(&schema(), &o(vec![("nodeChanges", arr(nodes))])), "x").unwrap();
|
||||
assert_eq!(doc.variables.collections.len(), 1, "{:?}", doc.variables.collections);
|
||||
let products = &doc.variables.collections[0];
|
||||
assert_eq!(products.modes.iter().map(|m| m.name.as_str()).collect::<Vec<_>>(), ["Blue", "Purple"]);
|
||||
assert_eq!(doc.variables.variables.len(), 1);
|
||||
let var = &doc.variables.variables[0];
|
||||
assert_eq!(var.collection, products.id);
|
||||
assert_eq!(var.values.len(), 2, "the newest copy's values: {:?}", var.values);
|
||||
let page = doc.nodes().find(|n| n.name == "Page").unwrap();
|
||||
assert_eq!(page.var_modes.get(&products.id).map(String::as_str), Some("1:92"));
|
||||
let button = named(&doc, "Button");
|
||||
assert_eq!(button.bound.values().next(), Some(&var.id), "{:?}", button.bound);
|
||||
assert!(!warnings.iter().any(|w| w.contains("variable modes of collections")), "{warnings:?}");
|
||||
}
|
||||
Reference in new issue
Block a user