Add files via upload

This commit is contained in:
mortezanaeemi-ops authored and GitHub committed 2026-11-01 13:53:11 +03:30
1 parent a5ebc01d2c
commit 6969232326
2 files changed
+203

No files matched your search

+104
View File
@@ -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))
}
}
+99
View File
@@ -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:?}");
}