mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-09 03:51:42 +00:00
442 lines
20 KiB
Rust
442 lines
20 KiB
Rust
use super::layer::own_text;
|
|
use super::text::char_styles;
|
|
use super::*;
|
|
|
|
/// An override row names a layer of the main by its current GUID or by the `overrideKey` the
|
|
/// layer had when the row was written: the key becomes the current GUID, unless the layer is
|
|
/// current already or the key is ambiguous.
|
|
fn normalize_override_segment(path: &mut [String], index: usize, scope: &OverrideScope) {
|
|
let Some(segment) = path.get(index) else { return };
|
|
if scope.current.contains(segment) {
|
|
return;
|
|
}
|
|
if let Some(Some(current)) = scope.aliases.get(segment) {
|
|
path[index] = current.clone();
|
|
}
|
|
}
|
|
|
|
/// Overrides and derived layout by id path.
|
|
type Overrides<'a> = BTreeMap<Vec<String>, Vec<OverrideRow<'a>>>;
|
|
|
|
/// Where an override entry comes from.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
pub(super) enum Entry {
|
|
Override,
|
|
Derived,
|
|
/// Derived layout of a nested instance for a size it no longer has: its size and place no
|
|
/// longer hold, the rest (resolved colours, text) still does.
|
|
Stale,
|
|
}
|
|
|
|
/// One override or derived-layout row addressed to a layer.
|
|
#[derive(Clone)]
|
|
pub(super) struct OverrideRow<'a> {
|
|
pub(super) row: &'a Kv<'a>,
|
|
pub(super) entry: Entry,
|
|
/// How many mains are being expanded around the instance that wrote the row: the rows of
|
|
/// the enclosing (outer) instances win over those of the inner ones.
|
|
source_depth: usize,
|
|
/// Position among the rows of the instance that wrote it; later rows win.
|
|
ordinal: usize,
|
|
}
|
|
|
|
/// The layers of a main by current GUID, and the old `overrideKey` of each that has one
|
|
/// (`None` when two layers claim the same key).
|
|
pub(super) struct OverrideScope {
|
|
current: BTreeSet<String>,
|
|
aliases: BTreeMap<String, Option<String>>,
|
|
}
|
|
|
|
/// Where an instance's expanded layers are: the overrides and derived layout of the outermost
|
|
/// instance (by id path), the path of the instance being expanded, and the property values.
|
|
#[derive(Clone)]
|
|
pub(super) struct Inst<'a> {
|
|
pub(super) root: String,
|
|
pub(super) path: Vec<String>,
|
|
pub(super) overrides: Rc<Overrides<'a>>,
|
|
props: Rc<BTreeMap<String, &'a Kv<'a>>>,
|
|
/// Slot property GUID → private slot-content node GUID.
|
|
pub(super) slot_contents: Rc<BTreeMap<String, String>>,
|
|
/// Layers imported from a slot's independent content subtree.
|
|
pub(super) slot_content: bool,
|
|
/// Mains being expanded (an instance inside its own main stops there).
|
|
symbols: Vec<String>,
|
|
}
|
|
|
|
impl<'a> Mapper<'a> {
|
|
/// The layers of main `main` by current GUID and by `overrideKey`. An instance nested in the
|
|
/// main is not entered: its layers belong to another main.
|
|
fn override_scope(&mut self, main: &str) -> Rc<OverrideScope> {
|
|
if let Some(scope) = self.override_scopes.get(main) {
|
|
return Rc::clone(scope);
|
|
}
|
|
let mut current = BTreeSet::new();
|
|
let mut aliases: BTreeMap<String, Option<String>> = BTreeMap::new();
|
|
let mut visited = BTreeSet::new();
|
|
let mut pending: Vec<(String, usize)> = self.children(main).into_iter().map(|id| (id, 1)).collect();
|
|
let mut complete = true;
|
|
while let Some((id, depth)) = pending.pop() {
|
|
if depth > MAX_DEPTH || visited.len() >= self.max_nodes {
|
|
complete = false;
|
|
break;
|
|
}
|
|
if !visited.insert(id.clone()) {
|
|
continue;
|
|
}
|
|
let Some(node) = self.nodes.get(&id) else { continue };
|
|
current.insert(id.clone());
|
|
if let Some(alias) = node.get("overrideKey").and_then(guid) {
|
|
let target = aliases.entry(alias).or_insert_with(|| Some(id.clone()));
|
|
if target.as_deref().is_some_and(|target| target != id) {
|
|
*target = None;
|
|
}
|
|
}
|
|
if node.get("type").and_then(Kv::str) != Some("INSTANCE") {
|
|
pending.extend(self.children(&id).into_iter().map(|child| (child, depth + 1)));
|
|
}
|
|
}
|
|
// A scope cut short could name the wrong layer for a key: use none of the keys.
|
|
if !complete {
|
|
aliases.clear();
|
|
}
|
|
let scope = Rc::new(OverrideScope { current, aliases });
|
|
let cost = scope.current.len() + scope.aliases.len();
|
|
if cost <= self.max_nodes.saturating_sub(self.override_scope_entries) {
|
|
self.override_scope_entries += cost;
|
|
self.override_scopes.insert(main.to_string(), Rc::clone(&scope));
|
|
}
|
|
scope
|
|
}
|
|
|
|
pub(super) fn is_instance_swap(&self, def: &Kv) -> Option<()> {
|
|
matches!(self.prop_def(&guid(def)?), Some((_, "INSTANCE_SWAP"))).then_some(())
|
|
}
|
|
|
|
/// The definition a copy of it was made from, followed to the original: the values an instance
|
|
/// sets are kept under it, since an assignment and the layers that read it may name different
|
|
/// copies of one definition.
|
|
pub(super) fn prop_root(&self, def: &str) -> String {
|
|
let mut def = def;
|
|
for _ in 0..=MAX_DEPTH {
|
|
let Some(parent) = self.prop_parents.get(def) else { break };
|
|
def = parent;
|
|
}
|
|
def.to_string()
|
|
}
|
|
|
|
/// The definition `def` names: its own, or the set's for a variant's copy of it.
|
|
fn prop_def(&self, def: &str) -> Option<&(String, &'a str)> {
|
|
let mut def = def;
|
|
for _ in 0..=MAX_DEPTH {
|
|
if let Some(found) = self.prop_defs.get(def) {
|
|
return Some(found);
|
|
}
|
|
def = self.prop_parents.get(def)?;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Component property definitions and description.
|
|
pub(super) fn component(&mut self, o: &mut Obj, v: &View<'a>, id: &str) {
|
|
let mut defs = Obj::new();
|
|
for d in v.list("componentPropDefs") {
|
|
if d.get("isDeleted").and_then(Kv::bool) == Some(true) {
|
|
continue;
|
|
}
|
|
let Some((name, ty)) = d.get("id").and_then(guid).and_then(|i| self.prop_defs.get(&i).cloned()) else { continue };
|
|
let init = d.get("initialValue");
|
|
let def = match ty {
|
|
"BOOL" => json!({"type": "BOOLEAN", "defaultValue": init.and_then(|i| i.get("boolValue")).and_then(Kv::bool).unwrap_or(true)}),
|
|
"TEXT" => json!({"type": "TEXT", "defaultValue": init.and_then(prop_text).unwrap_or("")}),
|
|
"INSTANCE_SWAP" => match init.and_then(|i| i.get("guidValue")).and_then(guid) {
|
|
Some(g) => {
|
|
if self.nodes.contains_key(&g) {
|
|
self.referenced_mains.insert(g.clone());
|
|
}
|
|
json!({"type": "INSTANCE_SWAP", "defaultValue": g})
|
|
}
|
|
None => continue,
|
|
},
|
|
"VARIANT" => json!({"type": "VARIANT"}),
|
|
"SLOT" => {
|
|
self.warn_non_default_slot_config(d);
|
|
self.warn_dropped_slot_preferred_values(d);
|
|
json!({"type": "SLOT"})
|
|
}
|
|
_ => {
|
|
self.warn(format!("{} component properties are not supported and were skipped", ty.to_ascii_lowercase()));
|
|
continue;
|
|
}
|
|
};
|
|
defs.insert(name, def);
|
|
}
|
|
if !defs.is_empty() {
|
|
o.insert("componentPropertyDefinitions".into(), Value::Object(defs));
|
|
}
|
|
if let Some(d) = v.str("symbolDescription").or_else(|| v.str("description")).filter(|d| !d.is_empty()) {
|
|
let into = if o.get("type").and_then(Value::as_str) == Some("COMPONENT_SET") { &mut self.sets } else { &mut self.components };
|
|
into.insert(id.to_string(), json!({"description": d}));
|
|
}
|
|
}
|
|
|
|
/// Preferred slot values are source component keys, but this mapper has no proven way to map
|
|
/// those keys to file GUIDs. They are dropped instead of guessing native NodeIds.
|
|
fn warn_dropped_slot_preferred_values(&mut self, def: &Kv<'a>) {
|
|
let entries = def.get("preferredValues").and_then(|v| v.get("instanceSwapValues")).map(Kv::list).unwrap_or_default();
|
|
if !entries.is_empty() {
|
|
self.warn("preferred slot component keys cannot be resolved to file nodes and were skipped");
|
|
}
|
|
}
|
|
|
|
fn warn_non_default_slot_config(&mut self, def: &Kv<'a>) {
|
|
let Some(config) = def.get("slotPropConfig") else { return };
|
|
let non_default = [
|
|
("stretchChildOnInsert", Some(false)),
|
|
("displayByDefault", Some(false)),
|
|
("minChildren", None),
|
|
("maxChildren", None),
|
|
("allowPreferredValuesOnly", Some(false)),
|
|
]
|
|
.into_iter()
|
|
.any(|(field, bool_default)| {
|
|
let Some(value) = config.get(field) else { return false };
|
|
if let Some(default) = bool_default { value.bool() != Some(default) } else { value.f64() != Some(0.0) }
|
|
});
|
|
if non_default {
|
|
self.warn("non-default slot settings are not represented and were skipped");
|
|
}
|
|
}
|
|
|
|
/// A layer's component property references, and inside an instance the values they take.
|
|
pub(super) fn prop_refs(&mut self, o: &mut Obj, v: &View<'a>, inst: Option<&Inst<'a>>) {
|
|
let mut refs = Obj::new();
|
|
for e in v.get("parameterConsumptionMap").and_then(|m| m.get("entries")).map(Kv::list).unwrap_or_default() {
|
|
let field = match e.get("variableField").and_then(Kv::str) {
|
|
Some("VISIBLE") => "visible",
|
|
Some("TEXT_DATA") => "characters",
|
|
Some("OVERRIDDEN_SYMBOL_ID") => "mainComponent",
|
|
Some("SLOT_CONTENT_ID") => "slot",
|
|
_ => continue,
|
|
};
|
|
let Some(def) =
|
|
e.get("variableData").and_then(|d| d.get("value")).and_then(|x| x.get("propRefValue")).and_then(|p| p.get("defId")).and_then(guid)
|
|
else {
|
|
continue;
|
|
};
|
|
// Expanded frames must follow their main frame's source link. A direct slot reference
|
|
// makes the engine resolve ownership against the instance, where the property is not defined.
|
|
if !(field == "slot" && inst.is_some())
|
|
&& let Some((name, _)) = self.prop_def(&def)
|
|
{
|
|
refs.insert(field.into(), json!(name));
|
|
}
|
|
let Some(val) = inst.and_then(|i| i.props.get(&self.prop_root(&def))) else { continue };
|
|
match field {
|
|
"visible" => {
|
|
if let Some(b) = val.get("boolValue").and_then(Kv::bool) {
|
|
o.insert("visible".into(), json!(b));
|
|
}
|
|
}
|
|
"characters" => {
|
|
if let Some(t) = prop_text(val) {
|
|
o.insert("characters".into(), json!(t));
|
|
// Run styles count the chars they were made for: for other chars only
|
|
// the value's own apply.
|
|
if own_text(v) != Some(t) {
|
|
o.remove("characterStyleOverrides");
|
|
o.remove("styleOverrideTable");
|
|
if let Some(tv) = prop_text_data(val) {
|
|
let size = o.get("style").and_then(|s| s.get("fontSize")).and_then(Value::as_f64);
|
|
char_styles(o, tv, size, &mut |w| self.warn(w));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if !refs.is_empty() {
|
|
o.insert("componentPropertyReferences".into(), Value::Object(refs));
|
|
}
|
|
}
|
|
|
|
/// An instance's main and properties; the context its expanded layers are built in.
|
|
pub(super) fn instance(&mut self, o: &mut Obj, v: &View<'a>, id: &str, outer: Option<&Inst<'a>>, rest_id: &str) -> Option<(Inst<'a>, String)> {
|
|
let sd = v.get("symbolData");
|
|
let mut sym = v.get("overriddenSymbolID").and_then(guid).or_else(|| sd.and_then(|s| s.get("symbolID")).and_then(guid));
|
|
// An instance swap property value picks the main.
|
|
if let Some(i) = outer
|
|
&& let Some(def) = prop_ref(v, "OVERRIDDEN_SYMBOL_ID")
|
|
&& let Some(g) = i.props.get(&self.prop_root(&def)).and_then(|p| prop_guid(p))
|
|
{
|
|
sym = Some(g);
|
|
}
|
|
let Some(sym) = sym else {
|
|
self.warn("instances without a main component were imported as empty frames");
|
|
return None;
|
|
};
|
|
o.insert("componentId".into(), json!(sym));
|
|
let assignments = v.list("componentPropAssignments");
|
|
let mut props = Obj::new();
|
|
for a in assignments {
|
|
let Some((name, ty)) = a.get("defID").and_then(guid).and_then(|d| self.prop_def(&d).cloned()) else { continue };
|
|
let val = assigned(a);
|
|
let p = match ty {
|
|
"BOOL" => val.and_then(|x| x.get("boolValue")).and_then(Kv::bool).map(|b| json!({"type": "BOOLEAN", "value": b})),
|
|
"TEXT" => val.and_then(prop_text).map(|t| json!({"type": "TEXT", "value": t})),
|
|
"INSTANCE_SWAP" => val.and_then(prop_guid).map(|g| json!({"type": "INSTANCE_SWAP", "value": g})),
|
|
_ => None,
|
|
};
|
|
if let Some(p) = p {
|
|
props.insert(name, p);
|
|
}
|
|
}
|
|
if !props.is_empty() {
|
|
o.insert("componentProperties".into(), Value::Object(props));
|
|
}
|
|
if !self.nodes.contains_key(&sym) {
|
|
self.warn("instances of components that aren't in the file show no layers");
|
|
return None;
|
|
}
|
|
if !self.referenced_mains.contains(&sym) {
|
|
self.referenced_mains.insert(sym.clone());
|
|
}
|
|
if outer.is_some_and(|i| i.symbols.contains(&sym)) {
|
|
return None;
|
|
}
|
|
// Overrides by id path below this instance (the instance itself is addressed by its main's id).
|
|
let own_path: Vec<String> = outer.map(|i| [i.path.as_slice(), &[id.to_string()]].concat()).unwrap_or_default();
|
|
let scope = self.override_scope(&sym);
|
|
let mut overrides: Overrides<'a> = BTreeMap::new();
|
|
if let Some(inherited) = outer {
|
|
for (path, rows) in inherited.overrides.iter() {
|
|
let mut path = path.clone();
|
|
// Rows an outer instance wrote for this instance's layers may use their old keys.
|
|
if path.starts_with(&own_path) && path.len() > own_path.len() {
|
|
normalize_override_segment(&mut path, own_path.len(), &scope);
|
|
}
|
|
overrides.entry(path).or_default().extend(rows.iter().cloned());
|
|
}
|
|
}
|
|
// A nested instance's derived layout is for the size it has in its main: resized here, the
|
|
// sizes and places in it no longer hold and its layers follow their constraints instead.
|
|
let differs = |a: f64, b: f64| (a - b).abs() > 1e-6 * a.abs().max(b.abs()).max(1.0);
|
|
let resized = outer.is_some()
|
|
&& match (self.nodes.get(id).and_then(|b| View::of(b).size()), v.size()) {
|
|
(Some(a), Some(b)) => differs(a.0, b.0) || differs(a.1, b.1),
|
|
(a, b) => a.is_some() != b.is_some(),
|
|
};
|
|
let mut local: Overrides<'a> = BTreeMap::new();
|
|
let lists = [sd.and_then(|s| s.get("symbolOverrides")).map(Kv::list).unwrap_or_default(), v.list("derivedSymbolData")];
|
|
let source_depth = outer.map_or(0, |i| i.symbols.len());
|
|
let mut ordinal = 0;
|
|
for (layer, list) in lists.into_iter().enumerate() {
|
|
for ov in list {
|
|
let row_ordinal = ordinal;
|
|
ordinal += 1;
|
|
let mut rel: Vec<String> =
|
|
ov.get("guidPath").and_then(|g| g.get("guids")).map(Kv::list).unwrap_or_default().iter().filter_map(guid).collect();
|
|
if rel.is_empty() || rel == [sym.clone()] {
|
|
continue;
|
|
}
|
|
normalize_override_segment(&mut rel, 0, &scope);
|
|
// Explicit overrides, then derived layout (stale once the instance is resized).
|
|
let entry = match (layer, resized) {
|
|
(0, _) => Entry::Override,
|
|
(_, false) => Entry::Derived,
|
|
(_, true) => Entry::Stale,
|
|
};
|
|
local.entry([own_path.as_slice(), &rel].concat()).or_default().push(OverrideRow {
|
|
row: ov,
|
|
entry,
|
|
source_depth,
|
|
ordinal: row_ordinal,
|
|
});
|
|
}
|
|
}
|
|
for (path, rows) in local {
|
|
overrides.entry(path).or_default().extend(rows);
|
|
}
|
|
for rows in overrides.values_mut() {
|
|
// Deeper source rows apply first; outer rows win, with wire order preserved per source.
|
|
rows.sort_by_key(|row| (std::cmp::Reverse(row.source_depth), row.ordinal));
|
|
}
|
|
let mut all_props: BTreeMap<String, &'a Kv<'a>> = outer.map(|i| (*i.props).clone()).unwrap_or_default();
|
|
let mut slot_contents: BTreeMap<String, String> = outer.map(|i| (*i.slot_contents).clone()).unwrap_or_default();
|
|
for a in assignments {
|
|
if let (Some(d), Some(val)) = (a.get("defID").and_then(guid), assigned(a)) {
|
|
all_props.entry(self.prop_root(&d)).or_insert(val);
|
|
}
|
|
let Some(def) = a.get("defID").and_then(guid) else { continue };
|
|
if self.prop_def(&def).is_none_or(|(_, ty)| *ty != "SLOT") {
|
|
continue;
|
|
}
|
|
let var_value = a.get("varValue");
|
|
if var_value.and_then(|v| v.get("dataType")).and_then(Kv::str) != Some("SLOT_CONTENT_ID") {
|
|
self.warn("malformed slot content assignment was skipped");
|
|
continue;
|
|
}
|
|
let target = var_value.and_then(|v| v.get("value")).and_then(|v| v.get("slotContentIdValue")).and_then(|v| v.get("guid")).and_then(guid);
|
|
let Some(target) = target else {
|
|
self.warn("malformed slot content assignment was skipped");
|
|
continue;
|
|
};
|
|
if target == "4294967295:4294967295" {
|
|
self.warn("slot content assignment uses the empty GUID sentinel and was skipped");
|
|
continue;
|
|
}
|
|
if self.nodes.get(&target).is_none_or(|n| n.get("isSlotContent").and_then(Kv::bool) != Some(true)) {
|
|
self.warn("slot content assignment target is missing or is not slot content");
|
|
continue;
|
|
}
|
|
slot_contents.entry(self.prop_root(&def)).or_insert(target);
|
|
}
|
|
let mut symbols = outer.map(|i| i.symbols.clone()).unwrap_or_default();
|
|
symbols.push(sym.clone());
|
|
let root = outer.map_or_else(|| rest_id.to_string(), |i| i.root.clone());
|
|
Some((
|
|
Inst {
|
|
root,
|
|
path: own_path,
|
|
overrides: Rc::new(overrides),
|
|
props: Rc::new(all_props),
|
|
slot_contents: Rc::new(slot_contents),
|
|
slot_content: false,
|
|
symbols,
|
|
},
|
|
sym,
|
|
))
|
|
}
|
|
}
|
|
|
|
/// The text of a component property value.
|
|
fn prop_text<'k>(v: &'k Kv) -> Option<&'k str> {
|
|
prop_text_data(v).and_then(|t| t.get("characters")).and_then(Kv::str)
|
|
}
|
|
|
|
/// The text data of a component property value: `textValue`, or `textDataValue` in the variable form.
|
|
fn prop_text_data<'k, 's>(v: &'k Kv<'s>) -> Option<&'k Kv<'s>> {
|
|
v.get("textValue").or_else(|| v.get("textDataValue"))
|
|
}
|
|
|
|
/// The main an instance swap value names: `guidValue`, or `symbolIdValue` in the variable form.
|
|
pub(super) fn prop_guid(v: &Kv) -> Option<String> {
|
|
v.get("guidValue").and_then(guid).or_else(|| v.get("symbolIdValue")?.get("guid").and_then(guid))
|
|
}
|
|
|
|
/// The value a component property assignment sets: `value`, or the variable form `varValue` holds
|
|
/// when `value` is empty, as current files write many booleans and instance swaps.
|
|
pub(super) fn assigned<'k, 's>(assignment: &'k Kv<'s>) -> Option<&'k Kv<'s>> {
|
|
assignment.get("value").filter(|v| !matches!(v, Kv::Object(fields) if fields.is_empty())).or_else(|| assignment.get("varValue")?.get("value"))
|
|
}
|
|
|
|
/// The property a layer's `field` is bound to (`parameterConsumptionMap`).
|
|
pub(super) fn prop_ref(v: &View, field: &str) -> Option<String> {
|
|
v.get("parameterConsumptionMap")?.get("entries")?.list().iter().find_map(|e| {
|
|
if e.get("variableField").and_then(Kv::str) != Some(field) {
|
|
return None;
|
|
}
|
|
e.get("variableData")?.get("value")?.get("propRefValue")?.get("defId").and_then(guid)
|
|
})
|
|
}
|