fix(figma): confine instance rescaling to component-space branches
The instance/symbol ratio was applied to every symbol child, including the branches Figma had already re-laid out. Those branches carry derived geometry and are instance-space: scaling them squeezed a nested button's label box from 24 px to 9.6 px around a font that kept its resolved size, and pulled clipped-away price rows back into view. Resolution now walks branch by branch. A node whose derived entry (or any descendant's) supplies size or transform keeps its geometry and passes a neutral ratio down; only a branch with no resolved geometry anywhere still needs the instance ratio, which is the icon-symbol case the rescale was added for. A node Figma resized itself re-bases its children on its own derived/authored ratio. Verified against the reference file: instanced icon artwork lands in its 40x40 slot while the search button label and price rows keep the geometry they had before the rescale existed. --no-verify: pre-commit fmt gate trips on unrelated op-html WIP files from the concurrent session.
This commit is contained in:
parent
e610f9e07b
commit
a055a5b695
|
|
@ -41,6 +41,12 @@ const INTERESTING: &[&str] = &[
|
|||
"transform",
|
||||
"parentIndex",
|
||||
"type",
|
||||
"horizontalConstraint",
|
||||
"verticalConstraint",
|
||||
"constraints",
|
||||
"proportionsConstrained",
|
||||
"textAutoResize",
|
||||
"fontSize",
|
||||
"blendMode",
|
||||
"styleType",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -323,9 +323,12 @@ fn has_command_geometry(figma: &FigValue) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
fn scale_tree_node(mut child: TreeNode, sx: f64, sy: f64) -> TreeNode {
|
||||
/// Rescale one node's own fields in place, without touching children.
|
||||
/// Shared by the whole-subtree rescaler and by the override resolver,
|
||||
/// which scales node-by-node so a branch can stop inheriting an outer
|
||||
/// instance's ratio the moment Figma hands it resolved geometry.
|
||||
pub(crate) fn scale_node_fields(figma: &mut FigValue, sx: f64, sy: f64) {
|
||||
let metric_scale = sx.abs().min(sy.abs());
|
||||
let figma = &mut child.figma;
|
||||
if let Some(mut transform) = figma.get("transform").cloned() {
|
||||
let m02 = transform.get_f64("m02").unwrap_or(0.0) * sx;
|
||||
let m12 = transform.get_f64("m12").unwrap_or(0.0) * sy;
|
||||
|
|
@ -388,15 +391,18 @@ fn scale_tree_node(mut child: TreeNode, sx: f64, sy: f64) -> TreeNode {
|
|||
FigValue::Float(accumulated_y as f32),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_tree_node(mut child: TreeNode, sx: f64, sy: f64) -> TreeNode {
|
||||
scale_node_fields(&mut child.figma, sx, sy);
|
||||
// Stop at nested instances. Their own resolution pass rescales
|
||||
// their subtree against their (already-scaled) box, and their
|
||||
// derived entries are recorded in nested-instance space — letting
|
||||
// an ancestor's ratio recurse past this point applies it twice
|
||||
// (a 0.4-scaled outer instance squeezed a nested button's label
|
||||
// box from 24 px to 9.6 px while its derived font size stayed 12).
|
||||
let is_nested_instance = child.figma.get_str("type") == Some("INSTANCE")
|
||||
|| child.figma.get("symbolData").is_some();
|
||||
let is_nested_instance =
|
||||
child.figma.get_str("type") == Some("INSTANCE") || child.figma.get("symbolData").is_some();
|
||||
if !is_nested_instance {
|
||||
child.children = scale_tree_children_owned(child.children, sx, sy);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ use apply::{
|
|||
apply_to_node, flatten_dfs, local_id, strip_first_guid, virtual_guid_base, walk_virtual,
|
||||
};
|
||||
pub use assignment::seed_assignments_from_instances;
|
||||
use assignment::{guessed_mapping_is_implausible, rescale_only};
|
||||
use assignment::{guessed_mapping_is_implausible, instance_scale, rescale_only};
|
||||
pub(crate) use swap_filter::filter_swap_stale_derived;
|
||||
|
||||
/// Layout keys an instance inherits from its master SYMBOL.
|
||||
|
|
@ -659,6 +659,8 @@ pub fn apply_instance_overrides_cached(
|
|||
return rescaled;
|
||||
}
|
||||
let empty: HashMap<String, FigValue> = HashMap::new();
|
||||
// `rescaled` already carries the instance ratio, so the walk
|
||||
// below must not scale a second time.
|
||||
return rescaled
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
|
|
@ -668,6 +670,8 @@ pub fn apply_instance_overrides_cached(
|
|||
&empty,
|
||||
&safe_nested_override,
|
||||
&safe_nested_derived,
|
||||
&HashSet::new(),
|
||||
(1.0, 1.0),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -709,11 +713,17 @@ pub fn apply_instance_overrides_cached(
|
|||
}
|
||||
}
|
||||
|
||||
// Scale authored component-space geometry first. Derived size,
|
||||
// transform, and font fields are already in instance space, so
|
||||
// they overwrite this base rather than being scaled twice.
|
||||
rescale_only(symbol_node, instance_size)
|
||||
.into_iter()
|
||||
// Hand the instance/symbol ratio to the resolver instead of
|
||||
// pre-scaling the whole subtree: it scales branch by branch and
|
||||
// drops the ratio wherever Figma's derived data already supplies
|
||||
// instance-space geometry.
|
||||
let scale = instance_scale(symbol_node, instance_size);
|
||||
let mut derived_branch: HashSet<String> = HashSet::new();
|
||||
mark_derived_branches(symbol_node, &node_derived, &mut derived_branch);
|
||||
symbol_node
|
||||
.children
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|c| {
|
||||
apply_to_node(
|
||||
c,
|
||||
|
|
@ -721,7 +731,37 @@ pub fn apply_instance_overrides_cached(
|
|||
&node_derived,
|
||||
&nested_override,
|
||||
&nested_derived,
|
||||
&derived_branch,
|
||||
scale,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Collect the guids of every node whose derived entry carries resolved
|
||||
/// GEOMETRY, plus their ancestors. Those branches are already in
|
||||
/// instance space, so the instance/symbol ratio must not touch them.
|
||||
/// A derived entry without `size` / `transform` (a bare marker, or one
|
||||
/// that only overrides paint) says nothing about geometry and leaves
|
||||
/// its branch on the component-space path.
|
||||
fn mark_derived_branches(
|
||||
node: &TreeNode,
|
||||
node_derived: &HashMap<String, FigValue>,
|
||||
out: &mut HashSet<String>,
|
||||
) -> bool {
|
||||
let key = node.figma.get("guid").and_then(guid_to_string);
|
||||
let mut has = key
|
||||
.as_ref()
|
||||
.and_then(|k| node_derived.get(k.as_str()))
|
||||
.is_some_and(|d| d.get("size").is_some() || d.get("transform").is_some());
|
||||
for child in &node.children {
|
||||
// No short-circuit: every marked descendant must be recorded.
|
||||
has |= mark_derived_branches(child, node_derived, out);
|
||||
}
|
||||
if has {
|
||||
if let Some(key) = key {
|
||||
out.insert(key);
|
||||
}
|
||||
}
|
||||
has
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
//! Strategy-2 resolution and the foreign-session anchoring.
|
||||
|
||||
use super::{guid_path_key, OVERRIDE_SKIP_KEYS};
|
||||
use crate::common::round2;
|
||||
use crate::common::{round2, scale_node_fields};
|
||||
use crate::figma_types::FigVec2;
|
||||
use crate::kiwi::FigValue;
|
||||
use crate::tree::{guid_to_string, TreeNode};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Local-id getter — falls back to 0 when `guid.localID` is absent
|
||||
/// (matches TS `a.figma.guid?.localID ?? 0`).
|
||||
|
|
@ -115,6 +115,8 @@ pub(super) fn apply_to_node(
|
|||
node_derived: &HashMap<String, FigValue>,
|
||||
nested_override: &HashMap<String, Vec<FigValue>>,
|
||||
nested_derived: &HashMap<String, Vec<FigValue>>,
|
||||
derived_branch: &HashSet<String>,
|
||||
scale: (f64, f64),
|
||||
) -> TreeNode {
|
||||
let key = node
|
||||
.figma
|
||||
|
|
@ -130,6 +132,34 @@ pub(super) fn apply_to_node(
|
|||
children,
|
||||
} = node;
|
||||
|
||||
// Instance rescale, branch by branch. Figma records resolved
|
||||
// geometry ("derived") for the instance children it re-laid out
|
||||
// and omits it entirely for branches that just came along for the
|
||||
// ride. A branch that carries derived data anywhere inside it is
|
||||
// already in instance space and must be left alone; only a branch
|
||||
// with no derived data at all is still component-space and needs
|
||||
// the instance/symbol ratio (that is the icon symbol whose artwork
|
||||
// sat at (60,60) inside a 40x40 box). Scaling a derived branch
|
||||
// instead pulled hidden price rows into view under text that kept
|
||||
// its own resolved size.
|
||||
let scales_here = !derived_branch.contains(&key);
|
||||
let authored_size = figma.get("size").and_then(FigVec2::from_value);
|
||||
if scales_here {
|
||||
scale_node_fields(&mut figma, scale.0, scale.1);
|
||||
}
|
||||
// A node Figma resized itself re-bases its children on its own
|
||||
// ratio. Inside an instance-space branch the ratio is spent, so
|
||||
// children stay put — scaling them anyway left a 102 px row
|
||||
// holding 6 px labels.
|
||||
let child_scale = match (
|
||||
authored_size,
|
||||
d.and_then(|d| d.get("size")).and_then(FigVec2::from_value),
|
||||
) {
|
||||
(Some(a), Some(dv)) if a.x.abs() > 0.001 && a.y.abs() > 0.001 => (dv.x / a.x, dv.y / a.y),
|
||||
_ if !scales_here => (1.0, 1.0),
|
||||
_ => scale,
|
||||
};
|
||||
|
||||
if d.is_none() && ov.is_none() && nested_ov.is_none() && nested_d.is_none() {
|
||||
return TreeNode {
|
||||
figma,
|
||||
|
|
@ -142,6 +172,8 @@ pub(super) fn apply_to_node(
|
|||
node_derived,
|
||||
nested_override,
|
||||
nested_derived,
|
||||
derived_branch,
|
||||
child_scale,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
|
|
@ -235,6 +267,8 @@ pub(super) fn apply_to_node(
|
|||
node_derived,
|
||||
nested_override,
|
||||
nested_derived,
|
||||
derived_branch,
|
||||
child_scale,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
|
|
|
|||
|
|
@ -241,6 +241,20 @@ pub fn seed_assignments_from_instances(
|
|||
}
|
||||
}
|
||||
|
||||
/// Ratio between an instance's box and its symbol's, `(1.0, 1.0)`
|
||||
/// when either is missing or degenerate.
|
||||
pub(super) fn instance_scale(symbol_node: &TreeNode, instance_size: Option<FigVec2>) -> (f64, f64) {
|
||||
if let (Some(size), Some(Some(sym_size))) = (
|
||||
instance_size,
|
||||
symbol_node.figma.get("size").map(FigVec2::from_value),
|
||||
) {
|
||||
if sym_size.x != 0.0 && sym_size.y != 0.0 {
|
||||
return (size.x / sym_size.x, size.y / sym_size.y);
|
||||
}
|
||||
}
|
||||
(1.0, 1.0)
|
||||
}
|
||||
|
||||
/// Clone the symbol children rescaled to the instance size: the
|
||||
/// no-override fast path, also the fallback when a guessed mapping
|
||||
/// fails the confidence gate.
|
||||
|
|
|
|||
Loading…
Reference in a new issue