mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 04:46:50 -06:00
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:
+50
-30
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user