M8: 50k-layer zoomed-out renders and faster save/load (#63)

Canvas previews zoomed out to 50% or less draw levels of detail
(crates/render/src/lod.rs): subtrees under 2 device px are splatted as
their averaged paint colours (box filter, no paths or effects), text
under 4 px is greeked into bars, small solid rectangles are splatted and
rectangular clip layers are composited without the blend pipeline.
Image paints and masks keep the exact path. Exports, thumbnails and
every other render stay exact (Options::lod is off by default); render
bands are assembled with a row copy instead of a pixmap draw.

.figcraft documents are serialized straight into the compressor and
parsed straight from the text (a header pass reads version and image
index without building a value tree); layer and text fields at their
default values are left out (63 -> 22 MB of JSON at 50k layers, older
files still load); JSON members use deflate level 3.

At 50k layers: zoomed-to-fit render ~58 -> ~8 ms, save ~1.0 s -> ~0.09 s,
load ~0.86 s -> ~0.16 s.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 10:27:51 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1184dd4177
commit 5f54fecca2
14 changed files with 1148 additions and 95 deletions
+50 -30
View File
@@ -321,72 +321,72 @@ pub struct Node {
pub name: String,
#[serde(flatten)]
pub kind: NodeKind,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<NodeId>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub children: Vec<NodeId>,
#[serde(default = "yes")]
#[serde(default = "yes", skip_serializing_if = "is_yes")]
pub visible: bool,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub locked: bool,
#[serde(default = "one")]
#[serde(default = "one", skip_serializing_if = "is_one")]
pub opacity: f32,
#[serde(default = "pass")]
#[serde(default = "pass", skip_serializing_if = "is_pass")]
pub blend: BlendMode,
/// Transform relative to the parent (the page for top-level nodes).
#[serde(default = "identity")]
#[serde(default = "identity", skip_serializing_if = "is_identity")]
pub transform: Affine,
pub width: f64,
pub height: f64,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub fills: Vec<Paint>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub strokes: Vec<Paint>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub stroke: StrokeProps,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub effects: Vec<Effect>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub corners: Corners,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub constraints: Constraints,
/// Auto layout (frame-like nodes only).
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub layout: Option<AutoLayout>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub child_layout: ChildLayout,
/// Clip content (frames).
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub clip: bool,
/// This node masks the siblings above it.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub is_mask: bool,
/// How a mask masks: by its alpha, its outline, or its luminance.
#[serde(default, skip_serializing_if = "MaskType::is_alpha")]
pub mask_type: MaskType,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub grids: Vec<LayoutGrid>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub exports: Vec<ExportSetting>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub interactions: Vec<Interaction>,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub overflow: Overflow,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub scroll_position: ScrollPosition,
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub styles: StyleRefs,
/// Variable bindings: field name ("fills/0/color", "width", "cornerRadius", "characters", …) → id.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub bound: BTreeMap<String, VariableId>,
/// Explicit variable modes (collection → mode) set on this container.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub var_modes: BTreeMap<CollectionId, ModeId>,
/// Lock aspect ratio when resizing from the panel.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub constrain_proportions: bool,
/// Inside an instance: the node of the main component this one mirrors.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub source: Option<NodeId>,
/// A nested instance inside a main component whose properties show on the main's instances.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
@@ -397,15 +397,15 @@ pub struct Node {
pub dev_status: DevStatus,
/// Inside an instance: property groups overridden locally ("fills", "characters", …; "slot":
/// this slot holds the instance's own content).
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub overrides: BTreeSet<String>,
/// Component property references: which property controls this node's
/// "visible" (boolean), "characters" (text) or "mainComponent" (instance swap), or which slot
/// property this frame is ("slot").
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub prop_refs: BTreeMap<String, String>,
/// Free-form plugin / agent data.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub plugin_data: BTreeMap<String, String>,
}
@@ -422,6 +422,26 @@ fn identity() -> Affine {
Affine::IDENTITY
}
// Fields at their default value are left out of the JSON (the file is several times smaller and
// faster to write and read); reading fills them back in from the same defaults.
/// Whether `v` is its type's default (what `#[serde(default)]` reads back for a missing field).
pub(crate) fn is_default<T: Default + PartialEq>(v: &T) -> bool {
*v == T::default()
}
fn is_yes(v: &bool) -> bool {
*v == yes()
}
fn is_one(v: &f32) -> bool {
*v == one()
}
fn is_pass(v: &BlendMode) -> bool {
*v == pass()
}
fn is_identity(v: &Affine) -> bool {
*v == identity()
}
impl Node {
/// A node with Figma's defaults for its kind.
pub fn new(id: NodeId, kind: NodeKind, name: impl Into<String>, width: f64, height: f64) -> Self {