From 23f57c86607669ad28b98be2e6792ab389220bcb Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 28 Oct 2026 16:00:01 +0000 Subject: [PATCH] refactor(engine): simplify command handling for strict clippy --- crates/engine/src/cmd/arrange.rs | 4 +- crates/engine/src/cmd/comment.rs | 4 +- crates/engine/src/cmd/component.rs | 11 +- crates/engine/src/cmd/component/instance.rs | 7 +- crates/engine/src/cmd/component/props.rs | 7 +- crates/engine/src/cmd/component/variants.rs | 5 +- crates/engine/src/cmd/create.rs | 4 +- crates/engine/src/cmd/edit.rs | 7 +- crates/engine/src/cmd/edit/clipboard.rs | 5 +- crates/engine/src/cmd/edit/props.rs | 5 +- crates/engine/src/cmd/edit/select.rs | 4 +- crates/engine/src/cmd/file.rs | 7 +- crates/engine/src/cmd/file/load.rs | 2 +- crates/engine/src/cmd/file/version.rs | 2 +- crates/engine/src/cmd/image.rs | 2 +- crates/engine/src/cmd/layout.rs | 2 +- crates/engine/src/cmd/library.rs | 9 +- crates/engine/src/cmd/lists.rs | 4 +- crates/engine/src/cmd/page.rs | 4 +- crates/engine/src/cmd/props.rs | 1117 ++++++++++--------- crates/engine/src/cmd/proto.rs | 10 +- crates/engine/src/cmd/proto_actions.rs | 6 +- crates/engine/src/cmd/section.rs | 2 +- crates/engine/src/cmd/structure.rs | 7 +- crates/engine/src/cmd/styles.rs | 7 +- crates/engine/src/cmd/text.rs | 7 +- crates/engine/src/cmd/variables.rs | 7 +- crates/engine/src/cmd/variables_io.rs | 4 +- crates/engine/src/cmd/view.rs | 4 +- 29 files changed, 698 insertions(+), 568 deletions(-) diff --git a/crates/engine/src/cmd/arrange.rs b/crates/engine/src/cmd/arrange.rs index 8f8e40d25..4806afb31 100644 --- a/crates/engine/src/cmd/arrange.rs +++ b/crates/engine/src/cmd/arrange.rs @@ -1,12 +1,12 @@ //! Arrange: align, distribute, tidy, z-order, flips, rotations and numeric transforms. -use figcraft_doc::*; +use figcraft_doc::{Document, EffectKind, LetterSpacing, LineHeight, Node, NodeId, NodeKind, Sizing, TextAutoResize, TextStyle}; use figcraft_geom::{Affine, Point, Rect}; use serde_json::{Value, json}; use super::edit::{roots, z_sorted}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, get, num, num_or, req_id, req_num, req_targets, string}; use crate::{Result, Session, bad, spec}; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/comment.rs b/crates/engine/src/cmd/comment.rs index d1d360f46..8cff0c42c 100644 --- a/crates/engine/src/cmd/comment.rs +++ b/crates/engine/src/cmd/comment.rs @@ -4,11 +4,11 @@ //! message). `author` is who is acting (default: the name from Preferences, `pref.userName`): only //! a message's author edits or deletes it, and only the thread's author deletes the thread. -use figcraft_doc::*; +use figcraft_doc::{Comment, Document, Message, Node, NodeId, NodeKind, geom}; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, get, id, id_of, num, req_index, req_num, req_string, string}; use crate::{Session, bad, spec}; /// The name comments are signed with until the user sets theirs. diff --git a/crates/engine/src/cmd/component.rs b/crates/engine/src/cmd/component.rs index 5abe79b8d..0f59f41e4 100644 --- a/crates/engine/src/cmd/component.rs +++ b/crates/engine/src/cmd/component.rs @@ -2,13 +2,18 @@ use std::collections::{BTreeMap, BTreeSet}; -use figcraft_doc::*; +use figcraft_doc::{Document, Node, NodeId, NodeKind, PropDef, VariableType, VariableValue}; use serde_json::{Value, json}; use super::edit::{roots, z_sorted}; use super::{CommandSpec, ok}; -use crate::components::*; -use crate::params::*; +use crate::components::{ + OVERRIDE_GROUPS, PROP_BINDING, SLOT, copy_group, create_instance, create_instance_at, default_props, default_variant, exposed_copies, + find_variant, inside_instance, instances_of, is_slot, keep_text_overrides, owning_component, owning_instance, prop_default_value, + rebase_instance, root_instance, set_boolean_default, slot_edited, slot_name, slots_of, takes_value, unlink_layers, variant_conflicts, + variant_name, variant_options, variant_props, variant_set, +}; +use crate::params::{boolean, get, id, id_of, num, req_id, req_num, req_string, req_targets, string, targets}; use crate::{EngineError, Result, Session, bad, spec}; mod instance; diff --git a/crates/engine/src/cmd/component/instance.rs b/crates/engine/src/cmd/component/instance.rs index ffc5ec1c8..f6600c10e 100644 --- a/crates/engine/src/cmd/component/instance.rs +++ b/crates/engine/src/cmd/component/instance.rs @@ -1,5 +1,10 @@ use super::props::{Owner, add_prop_for, delete_prop, prop_defs}; -use super::*; +use super::{ + BTreeMap, BTreeSet, Document, EngineError, NodeId, NodeKind, OVERRIDE_GROUPS, PROP_BINDING, PropDef, Result, SLOT, Session, Value, bad, boolean, + copy_group, create_instance_at, default_props, get, id_of, inside_instance, is_slot, json, keep_text_overrides, main_of, ok, owning_component, + owning_instance, rebase_instance, req_id, req_num, req_targets, root_instance, slot_edited, slot_name, slots_of, string, targets, unlink_layers, + variant_options, +}; pub(super) fn detach(s: &mut Session, p: &Value) -> Result { let cmd = "component.detach"; diff --git a/crates/engine/src/cmd/component/props.rs b/crates/engine/src/cmd/component/props.rs index 3f2d70020..bb3dba4cb 100644 --- a/crates/engine/src/cmd/component/props.rs +++ b/crates/engine/src/cmd/component/props.rs @@ -1,6 +1,11 @@ use super::instance::preferred_arg; use super::variants::{fit_set, wrap_in_set}; -use super::*; +use super::{ + BTreeMap, Document, NodeId, NodeKind, PROP_BINDING, PropDef, Result, SLOT, Session, Value, VariableType, VariableValue, bad, boolean, + default_variant, find_variant, get, id, id_of, inside_instance, instances_of, json, main_of, ok, owning_component, prop_default_value, + req_string, req_targets, set_boolean_default, slot_edited, slots_of, string, takes_value, targets, variant_conflicts, variant_name, + variant_options, variant_props, variant_set, +}; /// Whether instance `id` has a component property `name` that a `kind` variable can drive (a /// boolean property takes a boolean variable, a text or variant property a string one). diff --git a/crates/engine/src/cmd/component/variants.rs b/crates/engine/src/cmd/component/variants.rs index f6e7ac6cd..5f9e218ac 100644 --- a/crates/engine/src/cmd/component/variants.rs +++ b/crates/engine/src/cmd/component/variants.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + BTreeMap, BTreeSet, Document, EngineError, Node, NodeId, NodeKind, Result, Session, Value, bad, inside_instance, json, owning_instance, + req_targets, roots, set_name_from, string, targets, variant_name, variant_options, variant_props, variant_set, z_sorted, +}; /// Wrap components in a new component set (named after the first) placed around them. pub(super) fn wrap_in_set(cmd: &str, doc: &mut Document, ids: &[NodeId]) -> Result { diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 414a2310e..2e64ce042 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -1,12 +1,12 @@ //! Creating nodes. use figcraft_color::{ImageScaleMode, Paint}; -use figcraft_doc::*; +use figcraft_doc::{Document, Node, NodeId, NodeKind, StrokeCap, TextAutoResize, TextData, VectorNetwork}; use figcraft_geom::Point; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, f64_of, get, id, num, num_or, opt_id, req_targets, string}; use crate::{Result, Session, bad, spec}; /// Frame presets shown by the Frame tool (category, name, width, height). diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 7ec63c698..2d51b958e 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -2,12 +2,15 @@ use std::collections::{BTreeMap, BTreeSet}; -use figcraft_doc::*; +use figcraft_doc::{Document, Effect, ExportSetting, ImageData, LayoutGrid, Node, NodeId, NodeKind}; use figcraft_geom::Affine; use serde_json::{Map, Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{ + blend, boolean, color, get, id, id_of, num, num_or, opt_index, paint_of, parse, req_id, req_index, req_num, req_string, req_targets, string, + targets, +}; use crate::{BatchStep, EngineError, Result, Session, bad, spec}; mod clipboard; diff --git a/crates/engine/src/cmd/edit/clipboard.rs b/crates/engine/src/cmd/edit/clipboard.rs index d0e7b2dcf..0df975324 100644 --- a/crates/engine/src/cmd/edit/clipboard.rs +++ b/crates/engine/src/cmd/edit/clipboard.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + Affine, BTreeMap, BTreeSet, Document, EngineError, ImageData, Node, NodeId, NodeKind, Result, Session, Value, bad, delete, id, json, num, ok, + req_targets, roots, string, targets, z_sorted, +}; /// The copied form of the selection. pub fn copy_to_clipboard(s: &mut Session, cmd: &str, ids: &[NodeId]) -> Result<()> { diff --git a/crates/engine/src/cmd/edit/props.rs b/crates/engine/src/cmd/edit/props.rs index f793b50ce..91855c2e8 100644 --- a/crates/engine/src/cmd/edit/props.rs +++ b/crates/engine/src/cmd/edit/props.rs @@ -1,4 +1,7 @@ -use super::*; +use super::{ + BTreeMap, Effect, EngineError, Map, Node, NodeId, NodeKind, Result, Session, Value, bad, blend, boolean, color, get, json, num, ok, opt_index, + paint_of, parse, req_id, req_index, req_string, req_targets, roots, string, targets, z_sorted, +}; pub(super) fn batch_rename(s: &mut Session, p: &Value) -> Result { let cmd = "object.batchRename"; diff --git a/crates/engine/src/cmd/edit/select.rs b/crates/engine/src/cmd/edit/select.rs index 03bba8907..ca1da3bbc 100644 --- a/crates/engine/src/cmd/edit/select.rs +++ b/crates/engine/src/cmd/edit/select.rs @@ -1,4 +1,6 @@ -use super::*; +use super::{ + Affine, BTreeMap, BTreeSet, Node, NodeId, Result, Session, Value, bad, boolean, json, num, num_or, ok, req_num, req_string, req_targets, roots, +}; pub(super) fn layers_at(s: &mut Session, p: &Value) -> Result { let (x, y) = (req_num("select.layersAt", p, "x")?, req_num("select.layersAt", p, "y")?); diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index fed4bff10..7c9012933 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -3,12 +3,15 @@ use std::sync::Arc; use base64::Engine as _; -use figcraft_doc::*; +use figcraft_doc::{ + BOARD_PAGE, Document, ExportFormat, ExportSetting, ExportSize, FileKind, LineHeight, Node, NodeId, NodeKind, PrintSettings, StyleKind, TextStyle, + VdCompat, +}; use figcraft_format::Version; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{bool_of, boolean, get, id, num, num_or, req_index, req_string, string, targets}; use crate::{DocState, EngineError, Result, Session, bad, spec}; mod export; diff --git a/crates/engine/src/cmd/file/load.rs b/crates/engine/src/cmd/file/load.rs index 071c651ec..a033475e8 100644 --- a/crates/engine/src/cmd/file/load.rs +++ b/crates/engine/src/cmd/file/load.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{DocState, Document, EngineError, LineHeight, NodeId, NodeKind, Result, StyleKind, TextStyle, Value, bad, get, json}; /// The recovery copy at `path` as a document with unsaved changes and no path. pub(crate) fn recovered(path: &str) -> Result { diff --git a/crates/engine/src/cmd/file/version.rs b/crates/engine/src/cmd/file/version.rs index 3d311a444..a46fab8ed 100644 --- a/crates/engine/src/cmd/file/version.rs +++ b/crates/engine/src/cmd/file/version.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Arc, DocState, Document, EngineError, Result, Session, Value, Version, bad, get, json, req_index, string}; pub(super) fn version_json(index: usize, v: &Version) -> Value { let mut j = json!({"id": v.id, "index": index, "name": v.name, "description": v.description, "time": v.time}); diff --git a/crates/engine/src/cmd/image.rs b/crates/engine/src/cmd/image.rs index bb11f0117..c82e30d1d 100644 --- a/crates/engine/src/cmd/image.rs +++ b/crates/engine/src/cmd/image.rs @@ -5,7 +5,7 @@ use figcraft_doc::{Document, ImagePreset}; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{f64_of, get, num, req_index, req_string, req_targets, string, targets}; use crate::{Result, Session, bad, spec}; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index 5a0a40003..e22ca4984 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -6,7 +6,7 @@ use serde_json::Value; use super::props::sizing; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{get, num, req_num, req_targets, string}; use crate::{Result, Session, bad, spec}; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 7892102dd..0c7edbb2b 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -5,16 +5,19 @@ use std::collections::BTreeSet; use std::sync::Arc; -use figcraft_doc::*; +use figcraft_doc::{Collection, Document, ImportedComponent, ImportedRecord, LibraryLink, LibraryManifest, NodeId, PublishedLibrary, VariableId}; use serde_json::{Value, json}; use figcraft_collab::message::{LibraryReply, LibraryReq}; use super::CommandSpec; -use crate::library::*; +use crate::library::{ + Pending, Selection, absolute_path, accept_updates, asset_root, default_selection, find_link, import, import_styles, import_variables, + is_remote_variable, pending, prune, publish, publishable, published_collections, published_variables, +}; use crate::library_server::{archive, is_server_link, read_archive, shown_path}; use crate::live::{NetReply, NetRequest, PublishOptions}; -use crate::params::*; +use crate::params::{boolean, get, id_of, num, req_string, string}; use crate::{EngineError, Result, Session, bad, spec}; /// Longest library description kept (chars). diff --git a/crates/engine/src/cmd/lists.rs b/crates/engine/src/cmd/lists.rs index 287c9ddd0..e53291b6d 100644 --- a/crates/engine/src/cmd/lists.rs +++ b/crates/engine/src/cmd/lists.rs @@ -6,11 +6,11 @@ //! Every list is stored bottom first: index 0 paints (or applies) first, the last one is on top; //! the Design panel shows them in reverse, the topmost row first. -use figcraft_doc::*; +use figcraft_doc::{Document, LayoutGrid, Node, NodeId}; use serde_json::Value; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{req_index, req_targets}; use crate::{Result, Session, spec}; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/page.rs b/crates/engine/src/cmd/page.rs index 08e65b0b8..44c7b3677 100644 --- a/crates/engine/src/cmd/page.rs +++ b/crates/engine/src/cmd/page.rs @@ -1,11 +1,11 @@ //! Pages. use figcraft_doc::proto::Flow; -use figcraft_doc::*; +use figcraft_doc::{Guide, NodeId, NodeKind}; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, color, id, req_index, req_num, req_string, string}; use crate::{Session, bad, spec}; /// After a page edit: the file keeps a page that isn't a divider, and the canvas shows one (the diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs index 30a103932..469fcb01a 100644 --- a/crates/engine/src/cmd/props.rs +++ b/crates/engine/src/cmd/props.rs @@ -1,11 +1,15 @@ //! Property editing: `node.set` and the shorthand keys every surface uses. use figcraft_color::{Color, Paint}; -use figcraft_doc::*; +use figcraft_doc::{ + AutoLayout, Brush, ChildLayout, DecorationStyle, DevStatus, Document, Effect, GridTrack, LayoutMode, LetterSpacing, LineHeight, ListStyle, + MAX_LIST_INDENT, MaskType, Node, NodeId, NodeKind, Sizing, StrokeCap, StrokeProps, TextAutoResize, TextCase, TextData, TextDecoration, + WidthPoint, WidthPreset, WidthProfile, style_weight, valid_axis_tag, valid_feature_tag, +}; use figcraft_geom::{Affine, Point}; use serde_json::{Map, Value, json}; -use crate::params::*; +use crate::params::{bool_of, color_of, f64_of, get, paints_of}; use crate::{Result, bad}; /// The per-side stroke weight keys, in `StrokeProps::sides` order (top, right, bottom, left). @@ -409,6 +413,587 @@ fn fix_axis(doc: &mut Document, id: NodeId, is_w: bool) -> Result<()> { Ok(()) } +/// The `geometry` properties of `apply`; `false` when `k` is not one of them. +fn apply_geometry( + cmd: &str, + doc: &mut Document, + id: NodeId, + props: &Map, + k: &str, + v: &Value, + path_offset: &mut (f64, f64), +) -> Result { + match k { + "path" => *path_offset = set_path(cmd, doc, id, v, props)?, + // A new path takes the size asked for as its scale (`set_path`); the size still fixes + // its auto layout axis. + "width" | "height" | "w" | "h" if props.contains_key("path") => fix_axis(doc, id, k.starts_with('w'))?, + "x" | "y" => { + let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?; + let val = val + if k == "x" { path_offset.0 } else { path_offset.1 }; + // X/Y are the node's own origin (its rotated top-left for rotated nodes), in the + // frame it is in (groups have no space of their own). + let n = doc.get(id)?; + let group = n.parent.filter(|p| doc.node(*p).is_some_and(|pn| pn.kind.bounds_follow_children())); + let space = group.map_or(Affine::IDENTITY, |g| doc.frame_relative(g)); + let c = (space * n.transform).as_coeffs(); + let (mut tx, mut ty) = (c[4], c[5]); + if k == "x" { + tx = val; + } else { + ty = val; + } + doc.get_mut(id)?.transform = space.inverse() * Affine::new([c[0], c[1], c[2], c[3], tx, ty]); + } + "width" | "height" | "w" | "h" => { + let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); + let is_w = k.starts_with('w'); + // With the proportions locked (the Layout panel's link) and only one side set, the + // other side follows the node's ratio; both sides in one `node.set` stay as typed. + let (nw, nh) = { + let n = doc.get(id)?; + let (ow, oh) = (n.width, n.height); + let (other, other_short) = if is_w { ("height", "h") } else { ("width", "w") }; + let linked = n.constrain_proportions && !props.contains_key(other) && !props.contains_key(other_short) && ow > 1e-9 && oh > 1e-9; + let derived = if is_w { oh * val / ow } else { ow * val / oh }; + match (is_w, linked && derived.is_finite()) { + (true, true) => (val, derived), + (false, true) => (derived, val), + (true, false) => (val, oh), + (false, false) => (ow, val), + } + }; + resize_node(doc, id, nw, nh)?; + // Explicit size makes text fixed in that dimension. + if let Some(t) = doc.get_mut(id)?.kind.text_mut() { + t.fix_size(is_w, !is_w); + } + fix_axis(doc, id, is_w)?; + } + "rotation" => { + let deg = f64_of(v).ok_or_else(|| bad(cmd, "`rotation` must be degrees"))?; + let n = doc.get_mut(id)?; + n.transform = figcraft_geom::set_rotation_about_center(n.transform, n.width, n.height, deg); + } + _ => return Ok(false), + } + Ok(true) +} + +/// The `look` properties of `apply`; `false` when `k` is not one of them. +fn apply_look(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> Result { + match k { + "name" => doc.get_mut(id)?.name = string_val(v).ok_or_else(|| bad(cmd, "`name` must be a string"))?, + "visible" => doc.get_mut(id)?.visible = v.as_bool().ok_or_else(|| bad(cmd, "`visible` must be a boolean"))?, + "locked" => doc.get_mut(id)?.locked = v.as_bool().ok_or_else(|| bad(cmd, "`locked` must be a boolean"))?, + "opacity" => { + let o = f64_of(v).ok_or_else(|| bad(cmd, "`opacity` must be a number"))?; + let o = if o > 1.0 { o / 100.0 } else { o }; + doc.get_mut(id)?.opacity = o.clamp(0.0, 1.0) as f32; + } + "blend" | "blendMode" => { + let b = v.as_str().and_then(figcraft_color::BlendMode::from_name).ok_or_else(|| bad(cmd, "unknown blend mode"))?; + doc.get_mut(id)?.blend = b; + } + "fills" => set_paints(doc.get_mut(id)?, true, paints_of(v).ok_or_else(|| bad(cmd, "`fills` must be paints or colours"))?), + "fill" => { + if paint_none(v) { + set_paints(doc.get_mut(id)?, true, vec![]); + } else { + let c = color_of(v).ok_or_else(|| bad(cmd, "`fill` must be a colour, \"none\" or null"))?; + set_paints(doc.get_mut(id)?, true, vec![Paint::solid(c)]); + } + } + "strokes" => set_paints(doc.get_mut(id)?, false, paints_of(v).ok_or_else(|| bad(cmd, "`strokes` must be paints or colours"))?), + "stroke" => { + if paint_none(v) { + set_paints(doc.get_mut(id)?, false, vec![]); + } else { + let c = color_of(v).ok_or_else(|| bad(cmd, "`stroke` must be a colour, \"none\" or null"))?; + set_paints(doc.get_mut(id)?, false, vec![Paint::solid(c)]); + } + } + "strokeWeight" => doc.get_mut(id)?.stroke.weight = f64_of(v).ok_or_else(|| bad(cmd, "`strokeWeight` must be a number"))?.max(0.0), + "strokeAlign" => doc.get_mut(id)?.stroke.align = enum_of(v).ok_or_else(|| bad(cmd, "strokeAlign: inside|center|outside"))?, + "strokeSides" => { + doc.get_mut(id)?.stroke.sides = if v.is_null() { None } else { Some(four(v).ok_or_else(|| bad(cmd, "strokeSides: [t,r,b,l]"))?) } + } + "strokeTopWeight" | "strokeRightWeight" | "strokeBottomWeight" | "strokeLeftWeight" => { + let i = STROKE_SIDES.iter().position(|x| *x == k).unwrap_or(0); + let w = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); + let st = &mut doc.get_mut(id)?.stroke; + // One side set on its own turns on individual strokes, the others at the weight. + let mut sides = st.sides.unwrap_or([st.weight; 4]); + if let Some(slot) = sides.get_mut(i) { + *slot = w; + } + st.sides = Some(sides); + } + "strokeCap" => { + let c: StrokeCap = enum_of(v).ok_or_else(|| bad(cmd, "unknown stroke cap"))?; + let n = doc.get_mut(id)?; + n.stroke.cap_start = c; + n.stroke.cap_end = c; + } + "startCap" => doc.get_mut(id)?.stroke.cap_start = enum_of(v).ok_or_else(|| bad(cmd, "unknown stroke cap"))?, + "endCap" => doc.get_mut(id)?.stroke.cap_end = enum_of(v).ok_or_else(|| bad(cmd, "unknown stroke cap"))?, + "strokeJoin" => doc.get_mut(id)?.stroke.join = enum_of(v).ok_or_else(|| bad(cmd, "strokeJoin: miter|bevel|round"))?, + "miterLimit" => doc.get_mut(id)?.stroke.miter_limit = f64_of(v).unwrap_or(4.0).clamp(1.0, 100.0), + "miterAngle" => { + let deg = f64_of(v).ok_or_else(|| bad(cmd, "`miterAngle` must be degrees"))?; + doc.get_mut(id)?.stroke.miter_limit = StrokeProps::miter_limit_for(deg); + } + "dashes" => { + let d: Vec = v.as_array().map(|a| a.iter().filter_map(f64_of).map(|x| x.max(0.0)).collect()).unwrap_or_default(); + doc.get_mut(id)?.stroke.dashes = d; + } + "widthProfile" => doc.get_mut(id)?.stroke.width_profile = width_profile_of(v).ok_or_else(|| bad(cmd, WIDTH_PROFILE_HELP))?, + "brush" | "strokeBrush" => doc.get_mut(id)?.stroke.brush = brush_of(v).ok_or_else(|| bad(cmd, BRUSH_HELP))?, + _ => return Ok(false), + } + Ok(true) +} + +/// The `corners` properties of `apply`; `false` when `k` is not one of them. +fn apply_corners(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> Result { + match k { + "cornerRadius" => { + let r = four(v).ok_or_else(|| bad(cmd, "cornerRadius: n or [tl,tr,br,bl]"))?.map(|x| x.max(0.0)); + let n = doc.get_mut(id)?; + n.corners.radii = r; + // A vector's corner radius rounds every point (edit mode then sets them one by one). + if let NodeKind::Vector { network } = &mut n.kind { + network.vertices.iter_mut().for_each(|p| p.radius = r[0]); + crate::vectoredit::refit(doc, id)?; + } + } + "topLeftRadius" | "topRightRadius" | "bottomRightRadius" | "bottomLeftRadius" => { + let i = ["topLeftRadius", "topRightRadius", "bottomRightRadius", "bottomLeftRadius"].iter().position(|x| *x == k).unwrap_or(0); + let r = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); + if let Some(slot) = doc.get_mut(id)?.corners.radii.get_mut(i) { + *slot = r; + } + } + "cornerSmoothing" => doc.get_mut(id)?.corners.smoothing = f64_of(v).unwrap_or(0.0).clamp(0.0, 1.0), + "effects" => doc.get_mut(id)?.effects = effects_from(cmd, v)?, + "clip" | "clipsContent" => doc.get_mut(id)?.clip = v.as_bool().ok_or_else(|| bad(cmd, "`clip` must be a boolean"))?, + "isMask" => doc.get_mut(id)?.is_mask = v.as_bool().unwrap_or(false), + "devStatus" => { + let st = if v.is_null() { + Some(DevStatus::None) + } else { + v.as_str().and_then(|s| DevStatus::ALL.into_iter().find(|d| d.id().eq_ignore_ascii_case(s))) + }; + let st = st.ok_or_else(|| bad(cmd, "devStatus: none|readyForDev|completed"))?; + if !st.is_none() && !crate::devinspect::has_status(doc, id) { + return Err(bad(cmd, "devStatus: only sections and frames at the top of a page or section take one")); + } + let n = doc.get_mut(id)?; + n.dev_status = st; + // Marking a layer (again) starts from its current contents. + n.dev_changed = false; + } + "maskType" => { + let t = v.as_str().and_then(|s| MaskType::ALL.into_iter().find(|m| m.id().eq_ignore_ascii_case(s))); + doc.get_mut(id)?.mask_type = t.ok_or_else(|| bad(cmd, "maskType: alpha|vector|luminance"))?; + } + _ => return Ok(false), + } + Ok(true) +} + +/// The `layout` properties of `apply`; `false` when `k` is not one of them. +fn apply_layout(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> Result { + match k { + "layout" | "layoutMode" => { + let n = doc.get_mut(id)?; + if !n.kind.is_frame_like() { + return Err(bad(cmd, "auto layout needs a frame")); + } + match v { + Value::Null => { + n.layout = None; + drop_layout_bindings(n); + } + Value::String(s) if s.eq_ignore_ascii_case("none") => { + n.layout = None; + drop_layout_bindings(n); + } + Value::String(s) => { + let mode = match s.to_ascii_lowercase().as_str() { + "horizontal" | "row" => LayoutMode::Horizontal, + "vertical" | "column" => LayoutMode::Vertical, + "grid" => LayoutMode::Grid, + _ => return Err(bad(cmd, "layout: none|horizontal|vertical|grid")), + }; + ensure_layout(n).mode = mode; + } + Value::Object(_) => n.layout = Some(serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("layout: {e}")))?), + _ => return Err(bad(cmd, "layout: none|horizontal|vertical|grid|{...}")), + } + } + "gap" | "itemSpacing" | "columnGap" => { + layout_mut(cmd, k, doc.get_mut(id)?)?.gap = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))? + } + "crossGap" | "counterAxisSpacing" | "rowGap" => { + layout_mut(cmd, k, doc.get_mut(id)?)?.cross_gap = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))? + } + "padding" => { + let p = four(v).ok_or_else(|| bad(cmd, "padding: n or [t,r,b,l]"))?; + layout_mut(cmd, k, doc.get_mut(id)?)?.padding = p.map(|x| x.max(0.0)); + } + "paddingTop" | "paddingRight" | "paddingBottom" | "paddingLeft" => { + let i = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"].iter().position(|x| *x == k).unwrap_or(0); + let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); + if let Some(slot) = layout_mut(cmd, k, doc.get_mut(id)?)?.padding.get_mut(i) { + *slot = val; + } + } + "paddingHorizontal" => { + let val = f64_of(v).unwrap_or(0.0).max(0.0); + let l = layout_mut(cmd, k, doc.get_mut(id)?)?; + l.padding[1] = val; + l.padding[3] = val; + } + "paddingVertical" => { + let val = f64_of(v).unwrap_or(0.0).max(0.0); + let l = layout_mut(cmd, k, doc.get_mut(id)?)?; + l.padding[0] = val; + l.padding[2] = val; + } + "mainAlign" | "primaryAxisAlignItems" => { + layout_mut(cmd, k, doc.get_mut(id)?)?.main_align = + enum_of(v).ok_or_else(|| bad(cmd, "mainAlign: min|center|max|spaceBetween|spaceAround|spaceEvenly"))? + } + "crossAlign" | "counterAxisAlignItems" => { + layout_mut(cmd, k, doc.get_mut(id)?)?.cross_align = enum_of(v).ok_or_else(|| bad(cmd, "crossAlign: min|center|max|baseline"))? + } + "contentAlign" | "counterAxisAlignContent" => { + layout_mut(cmd, k, doc.get_mut(id)?)?.content_align = enum_of(v).ok_or_else(|| bad(cmd, "contentAlign: auto|spaceBetween"))? + } + "wrap" => layout_mut(cmd, k, doc.get_mut(id)?)?.wrap = v.as_bool().unwrap_or(false), + "gridColumns" | "gridRows" => { + let count = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.clamp(1.0, 1000.0) as u32; + let l = layout_mut(cmd, k, doc.get_mut(id)?)?; + // Tracks past the new count go away (new tracks are 1fr). + let (n, tracks) = if k == "gridColumns" { (&mut l.grid_columns, &mut l.column_tracks) } else { (&mut l.grid_rows, &mut l.row_tracks) }; + *n = count; + tracks.truncate(count as usize); + } + "strokesIncluded" => layout_mut(cmd, k, doc.get_mut(id)?)?.strokes_included = v.as_bool().unwrap_or(false), + "reverseZ" => layout_mut(cmd, k, doc.get_mut(id)?)?.reverse_z = v.as_bool().unwrap_or(false), + _ => return Ok(false), + } + Ok(true) +} + +/// The `sizing` properties of `apply`; `false` when `k` is not one of them. +fn apply_sizing(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> Result { + match k { + "sizingH" | "layoutSizingHorizontal" => { + let s = sizing(v).ok_or_else(|| bad(cmd, "sizing: fixed|hug|fill"))?; + set_sizing(cmd, doc, id, s, true)?; + } + "sizingV" | "layoutSizingVertical" => { + let s = sizing(v).ok_or_else(|| bad(cmd, "sizing: fixed|hug|fill"))?; + set_sizing(cmd, doc, id, s, false)?; + } + "absolute" | "ignoreAutoLayout" => { + let on = bool_of(v).ok_or_else(|| bad(cmd, "`absolute` must be a boolean"))?; + let cl = &mut doc.get_mut(id)?.child_layout; + cl.absolute = on; + // An absolute child can't fill: it keeps its current size. + if on { + for s in [&mut cl.sizing_h, &mut cl.sizing_v] { + if *s == Sizing::Fill { + *s = Sizing::Fixed; + } + } + } + } + "minWidth" | "maxWidth" | "minHeight" | "maxHeight" => { + let val = match v { + Value::Null => None, + other => Some(f64_of(other).ok_or_else(|| bad(cmd, format!("`{k}` must be a number or null")))?.clamp(0.0, 1e7)), + }; + set_limit(&mut doc.get_mut(id)?.child_layout, k, val); + } + "columnTracks" | "gridColumnTracks" | "rowTracks" | "gridRowTracks" => { + let a = v.as_array().ok_or_else(|| bad(cmd, format!("`{k}` must be an array of tracks")))?; + let tracks = a.iter().map(track_of).collect::>>().ok_or_else(|| bad(cmd, TRACK_HELP))?; + let l = layout_mut(cmd, k, doc.get_mut(id)?)?; + if k.contains("olumn") { + l.grid_columns = (tracks.len() as u32).clamp(1, 1000); + l.column_tracks = tracks; + } else { + l.grid_rows = (tracks.len() as u32).clamp(1, 1000); + l.row_tracks = tracks; + } + } + "gridColumn" | "gridColumnAnchorIndex" | "gridRow" | "gridRowAnchorIndex" => { + let val = match v { + Value::Null => None, + Value::String(s) if s.eq_ignore_ascii_case("auto") => None, + other => { + Some(f64_of(other).ok_or_else(|| bad(cmd, format!("`{k}` must be a 0-based index, \"auto\" or null")))?.clamp(0.0, 999.0) as u32) + } + }; + let cl = &mut doc.get_mut(id)?.child_layout; + if k.starts_with("gridColumn") { + cl.grid_column = val; + } else { + cl.grid_row = val; + } + } + "gridAlignH" | "gridChildHorizontalAlign" | "gridAlignV" | "gridChildVerticalAlign" => { + let val = match v { + Value::Null => None, + Value::String(s) if s.eq_ignore_ascii_case("auto") => None, + other => Some(enum_of(other).ok_or_else(|| bad(cmd, format!("`{k}`: min|center|max|auto")))?), + }; + let cl = &mut doc.get_mut(id)?.child_layout; + if k.contains('H') { + cl.grid_align_h = val; + } else { + cl.grid_align_v = val; + } + } + "gridColumnSpan" | "gridRowSpan" => { + let span = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.clamp(1.0, 1000.0) as u32; + let cl = &mut doc.get_mut(id)?.child_layout; + if k == "gridColumnSpan" { + cl.grid_column_span = span; + } else { + cl.grid_row_span = span; + } + } + "constraints" => { + let n = doc.get_mut(id)?; + if let Some(h) = v.get("horizontal") { + n.constraints.horizontal = enum_of(h).ok_or_else(|| bad(cmd, "horizontal: left|right|leftRight|center|scale"))?; + } + if let Some(vv) = v.get("vertical") { + n.constraints.vertical = enum_of(vv).ok_or_else(|| bad(cmd, "vertical: top|bottom|topBottom|center|scale"))?; + } + } + _ => return Ok(false), + } + Ok(true) +} + +/// The `meta` properties of `apply`; `false` when `k` is not one of them. +fn apply_meta(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> Result { + match k { + "grids" | "layoutGrids" => doc.get_mut(id)?.grids = serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("grids: {e}")))?, + "exports" | "exportSettings" => doc.get_mut(id)?.exports = serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("exports: {e}")))?, + "interactions" => doc.get_mut(id)?.interactions = serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("interactions: {e}")))?, + "overflow" => doc.get_mut(id)?.overflow = enum_of(v).ok_or_else(|| bad(cmd, "overflow: none|horizontal|vertical|both"))?, + "scrollPosition" => doc.get_mut(id)?.scroll_position = enum_of(v).ok_or_else(|| bad(cmd, "scrollPosition: scrolls|fixed|sticky"))?, + "constrainProportions" => doc.get_mut(id)?.constrain_proportions = v.as_bool().unwrap_or(false), + _ => return Ok(false), + } + Ok(true) +} + +/// The `text` properties of `apply`; `false` when `k` is not one of them. +fn apply_text(cmd: &str, doc: &mut Document, id: NodeId, props: &Map, k: &str, v: &Value) -> Result { + match k { + "characters" | "text" => { + let s = string_val(v).ok_or_else(|| bad(cmd, "`characters` must be a string"))?; + let n = doc.get_mut(id)?; + let t = text_mut(cmd, n)?; + let len = t.char_count(); + t.replace_range(0, len, &normalize_line_endings(&s)); + n.overrides.insert("characters".into()); + } + "fontFamily" + | "fontStyle" + | "fontWeight" + | "fontSize" + | "lineHeight" + | "letterSpacing" + | "textDecoration" + | "textCase" + | "hyperlink" + | "textFills" + | "fontVariations" + | "variationSettings" + | "textDecorationStyle" + | "textDecorationThickness" + | "textDecorationOffset" + | "textDecorationSkipInk" + | "textDecorationColor" => { + let n = doc.get_mut(id)?; + let t = text_mut(cmd, n)?; + let (start, end) = range_of(props, t.char_count()); + apply_text_style(cmd, t, start, end, k, v)?; + } + "textAlign" | "textAlignHorizontal" => { + text_mut(cmd, doc.get_mut(id)?)?.align_h = enum_of(v).ok_or_else(|| bad(cmd, "textAlign: left|center|right|justified"))? + } + "textAlignVertical" => { + text_mut(cmd, doc.get_mut(id)?)?.align_v = enum_of(v).ok_or_else(|| bad(cmd, "textAlignVertical: top|middle|bottom"))? + } + "textAutoResize" => { + text_mut(cmd, doc.get_mut(id)?)?.auto_resize = + enum_of(v).ok_or_else(|| bad(cmd, "textAutoResize: widthAndHeight|height|none|truncate"))? + } + "paragraphSpacing" => text_mut(cmd, doc.get_mut(id)?)?.paragraph_spacing = f64_of(v).unwrap_or(0.0).max(0.0), + "paragraphIndent" => text_mut(cmd, doc.get_mut(id)?)?.paragraph_indent = f64_of(v).unwrap_or(0.0).max(0.0), + "listStyle" | "indentation" => { + let t = text_mut(cmd, doc.get_mut(id)?)?; + let (start, end) = range_of(props, t.char_count()); + if k == "listStyle" { + let l: ListStyle = enum_of(v).ok_or_else(|| bad(cmd, "listStyle: none|bulleted|numbered"))?; + t.set_para_lists(start, end, |p| p.list = l); + } else { + let d = f64_of(v).filter(|d| d.is_finite()).ok_or_else(|| bad(cmd, "indentation: a list level 0-8"))?; + t.set_para_lists(start, end, |p| p.indent = d.clamp(0.0, f64::from(MAX_LIST_INDENT)) as u8); + } + } + "listSpacing" => text_mut(cmd, doc.get_mut(id)?)?.list_spacing = f64_of(v).filter(|s| s.is_finite()).unwrap_or(0.0).clamp(0.0, 10_000.0), + "hangingPunctuation" | "hangingList" => { + let on = v.as_bool().ok_or_else(|| bad(cmd, format!("`{k}` must be true or false")))?; + let t = text_mut(cmd, doc.get_mut(id)?)?; + *if k == "hangingList" { &mut t.hanging_list } else { &mut t.hanging_punctuation } = on; + } + "leadingTrim" => text_mut(cmd, doc.get_mut(id)?)?.leading_trim = enum_of(v).ok_or_else(|| bad(cmd, "leadingTrim: none|capHeight"))?, + "maxLines" => text_mut(cmd, doc.get_mut(id)?)?.max_lines = f64_of(v).unwrap_or(0.0).clamp(0.0, 10_000.0) as u32, + "features" | "textFeatures" | "fontFeatures" | "openTypeFeatures" => { + let f = features_of(cmd, v)?; + let t = text_mut(cmd, doc.get_mut(id)?)?; + t.features.clear(); + for (k, on) in f { + if let Some(on) = on { + t.features.insert(k, on); + } + } + } + "start" | "end" => {} + _ => return Ok(false), + } + Ok(true) +} + +/// The `shape` properties of `apply`; `false` when `k` is not one of them. +fn apply_shape(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -> Result { + match k { + "pointCount" => { + let c = f64_of(v).ok_or_else(|| bad(cmd, "`pointCount` must be a number"))?.round(); + match &mut doc.get_mut(id)?.kind { + // Stars have 3 to 60 points; polygons keep the geometry's 120 (silent). + NodeKind::Star { count, .. } => *count = c.clamp(3.0, 60.0) as u32, + NodeKind::Polygon { count } => *count = c.clamp(3.0, 120.0) as u32, + _ => return Err(bad(cmd, "pointCount applies to polygons and stars")), + } + } + "innerRadius" | "ratio" => { + let r = f64_of(v).unwrap_or(0.38); + let r = if r > 1.0 { r / 100.0 } else { r }; + match &mut doc.get_mut(id)?.kind { + NodeKind::Star { inner_ratio, .. } => *inner_ratio = r.clamp(0.0, 1.0), + _ => return Err(bad(cmd, "innerRadius applies to stars")), + } + } + "arcStart" | "arcSweep" | "arcInner" => { + let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?; + match &mut doc.get_mut(id)?.kind { + NodeKind::Ellipse { start, sweep, inner } => match k { + "arcStart" => *start = val.to_radians(), + "arcSweep" => *sweep = val.clamp(-360.0, 360.0).to_radians(), + _ => *inner = (if val > 1.0 { val / 100.0 } else { val }).clamp(0.0, 0.99), + }, + _ => return Err(bad(cmd, "arc applies to ellipses")), + } + } + "description" => match &mut doc.get_mut(id)?.kind { + NodeKind::Component { description, .. } | NodeKind::ComponentSet { description } => *description = string_val(v).unwrap_or_default(), + _ => return Err(bad(cmd, "description applies to components")), + }, + "pluginData" => { + let n = doc.get_mut(id)?; + if let Some(o) = v.as_object() { + for (pk, pv) in o { + if pv.is_null() { + n.plugin_data.remove(pk); + } else { + n.plugin_data.insert(pk.clone(), string_val(pv).unwrap_or_else(|| pv.to_string())); + } + } + } + } + "fillStyle" | "strokeStyle" | "effectStyle" | "textStyle" | "gridStyle" => { + let sid = string_val(v); + crate::cmd::styles::apply_style_ref(doc, id, k, sid.as_deref())?; + } + "boundVariables" => { + let n = doc.get_mut(id)?; + if let Some(o) = v.as_object() { + for (field, var) in o { + match var.as_str() { + Some(vid) => { + if n.bound.get(field).map(String::as_str) != Some(vid) { + crate::cmd::styles::rebind_paint_alpha(&mut n.fills, &mut n.strokes, field, true); + } + n.bound.insert(field.clone(), vid.to_string()); + } + None => { + if n.bound.remove(field).is_some() { + crate::cmd::styles::rebind_paint_alpha(&mut n.fills, &mut n.strokes, field, false); + } + } + } + } + } + } + "variableModes" => { + let n = doc.get_mut(id)?; + if let Some(o) = v.as_object() { + for (coll, mode) in o { + match mode.as_str() { + Some(m) => { + n.var_modes.insert(coll.clone(), m.to_string()); + } + None => { + n.var_modes.remove(coll); + } + } + } + } + } + "booleanOperation" if !matches!(doc.get(id)?.kind, NodeKind::BooleanOp { .. }) => set_operand_op(doc, id, v, cmd)?, + "booleanOperation" => { + if let NodeKind::BooleanOp { op } = &mut doc.get_mut(id)?.kind { + *op = enum_of(v).ok_or_else(|| bad(cmd, BOOL_OP_HELP))?; + } + } + "operandOperation" => set_operand_op(doc, id, v, cmd)?, + "sectionContentsHidden" => { + if let NodeKind::Section { contents_hidden } = &mut doc.get_mut(id)?.kind { + *contents_hidden = v.as_bool().unwrap_or(false); + } + } + "background" => { + let c = color_of(v).ok_or_else(|| bad(cmd, "`background` must be a colour"))?; + match &mut doc.get_mut(id)?.kind { + NodeKind::Page { background, .. } => *background = c, + _ => return Err(bad(cmd, "background applies to pages")), + } + } + "componentProperties" => { + if let NodeKind::Instance { props: p, .. } = &mut doc.get_mut(id)?.kind + && let Some(o) = v.as_object() + { + for (pk, pv) in o { + p.insert(pk.clone(), pv.clone()); + } + } + } + _ => return Ok(false), + } + Ok(true) +} + /// Apply a property map to one node. `x`/`y` are relative to the parent. pub fn apply(cmd: &str, doc: &mut Document, id: NodeId, props: &Map) -> Result<()> { let mut unknown = Vec::new(); @@ -418,524 +1003,18 @@ pub fn apply(cmd: &str, doc: &mut Document, id: NodeId, props: &Map path_offset = set_path(cmd, doc, id, v, props)?, - // A new path takes the size asked for as its scale (`set_path`); the size still fixes - // its auto layout axis. - "width" | "height" | "w" | "h" if props.contains_key("path") => fix_axis(doc, id, k.starts_with('w'))?, - "x" | "y" => { - let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?; - let val = val + if k == "x" { path_offset.0 } else { path_offset.1 }; - // X/Y are the node's own origin (its rotated top-left for rotated nodes), in the - // frame it is in (groups have no space of their own). - let n = doc.get(id)?; - let group = n.parent.filter(|p| doc.node(*p).is_some_and(|pn| pn.kind.bounds_follow_children())); - let space = group.map_or(Affine::IDENTITY, |g| doc.frame_relative(g)); - let c = (space * n.transform).as_coeffs(); - let (mut tx, mut ty) = (c[4], c[5]); - if k == "x" { - tx = val; - } else { - ty = val; - } - doc.get_mut(id)?.transform = space.inverse() * Affine::new([c[0], c[1], c[2], c[3], tx, ty]); - } - "width" | "height" | "w" | "h" => { - let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); - let is_w = k.starts_with('w'); - // With the proportions locked (the Layout panel's link) and only one side set, the - // other side follows the node's ratio; both sides in one `node.set` stay as typed. - let (nw, nh) = { - let n = doc.get(id)?; - let (ow, oh) = (n.width, n.height); - let (other, other_short) = if is_w { ("height", "h") } else { ("width", "w") }; - let linked = n.constrain_proportions && !props.contains_key(other) && !props.contains_key(other_short) && ow > 1e-9 && oh > 1e-9; - let derived = if is_w { oh * val / ow } else { ow * val / oh }; - match (is_w, linked && derived.is_finite()) { - (true, true) => (val, derived), - (false, true) => (derived, val), - (true, false) => (val, oh), - (false, false) => (ow, val), - } - }; - resize_node(doc, id, nw, nh)?; - // Explicit size makes text fixed in that dimension. - if let Some(t) = doc.get_mut(id)?.kind.text_mut() { - t.fix_size(is_w, !is_w); - } - fix_axis(doc, id, is_w)?; - } - "rotation" => { - let deg = f64_of(v).ok_or_else(|| bad(cmd, "`rotation` must be degrees"))?; - let n = doc.get_mut(id)?; - n.transform = figcraft_geom::set_rotation_about_center(n.transform, n.width, n.height, deg); - } - "name" => doc.get_mut(id)?.name = string_val(v).ok_or_else(|| bad(cmd, "`name` must be a string"))?, - "visible" => doc.get_mut(id)?.visible = v.as_bool().ok_or_else(|| bad(cmd, "`visible` must be a boolean"))?, - "locked" => doc.get_mut(id)?.locked = v.as_bool().ok_or_else(|| bad(cmd, "`locked` must be a boolean"))?, - "opacity" => { - let o = f64_of(v).ok_or_else(|| bad(cmd, "`opacity` must be a number"))?; - let o = if o > 1.0 { o / 100.0 } else { o }; - doc.get_mut(id)?.opacity = o.clamp(0.0, 1.0) as f32; - } - "blend" | "blendMode" => { - let b = v.as_str().and_then(figcraft_color::BlendMode::from_name).ok_or_else(|| bad(cmd, "unknown blend mode"))?; - doc.get_mut(id)?.blend = b; - } - "fills" => set_paints(doc.get_mut(id)?, true, paints_of(v).ok_or_else(|| bad(cmd, "`fills` must be paints or colours"))?), - "fill" => { - if paint_none(v) { - set_paints(doc.get_mut(id)?, true, vec![]); - } else { - let c = color_of(v).ok_or_else(|| bad(cmd, "`fill` must be a colour, \"none\" or null"))?; - set_paints(doc.get_mut(id)?, true, vec![Paint::solid(c)]); - } - } - "strokes" => set_paints(doc.get_mut(id)?, false, paints_of(v).ok_or_else(|| bad(cmd, "`strokes` must be paints or colours"))?), - "stroke" => { - if paint_none(v) { - set_paints(doc.get_mut(id)?, false, vec![]); - } else { - let c = color_of(v).ok_or_else(|| bad(cmd, "`stroke` must be a colour, \"none\" or null"))?; - set_paints(doc.get_mut(id)?, false, vec![Paint::solid(c)]); - } - } - "strokeWeight" => doc.get_mut(id)?.stroke.weight = f64_of(v).ok_or_else(|| bad(cmd, "`strokeWeight` must be a number"))?.max(0.0), - "strokeAlign" => doc.get_mut(id)?.stroke.align = enum_of(v).ok_or_else(|| bad(cmd, "strokeAlign: inside|center|outside"))?, - "strokeSides" => { - doc.get_mut(id)?.stroke.sides = if v.is_null() { None } else { Some(four(v).ok_or_else(|| bad(cmd, "strokeSides: [t,r,b,l]"))?) } - } - "strokeTopWeight" | "strokeRightWeight" | "strokeBottomWeight" | "strokeLeftWeight" => { - let i = STROKE_SIDES.iter().position(|x| *x == k).unwrap_or(0); - let w = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); - let st = &mut doc.get_mut(id)?.stroke; - // One side set on its own turns on individual strokes, the others at the weight. - let mut sides = st.sides.unwrap_or([st.weight; 4]); - if let Some(slot) = sides.get_mut(i) { - *slot = w; - } - st.sides = Some(sides); - } - "strokeCap" => { - let c: StrokeCap = enum_of(v).ok_or_else(|| bad(cmd, "unknown stroke cap"))?; - let n = doc.get_mut(id)?; - n.stroke.cap_start = c; - n.stroke.cap_end = c; - } - "startCap" => doc.get_mut(id)?.stroke.cap_start = enum_of(v).ok_or_else(|| bad(cmd, "unknown stroke cap"))?, - "endCap" => doc.get_mut(id)?.stroke.cap_end = enum_of(v).ok_or_else(|| bad(cmd, "unknown stroke cap"))?, - "strokeJoin" => doc.get_mut(id)?.stroke.join = enum_of(v).ok_or_else(|| bad(cmd, "strokeJoin: miter|bevel|round"))?, - "miterLimit" => doc.get_mut(id)?.stroke.miter_limit = f64_of(v).unwrap_or(4.0).clamp(1.0, 100.0), - "miterAngle" => { - let deg = f64_of(v).ok_or_else(|| bad(cmd, "`miterAngle` must be degrees"))?; - doc.get_mut(id)?.stroke.miter_limit = StrokeProps::miter_limit_for(deg); - } - "dashes" => { - let d: Vec = v.as_array().map(|a| a.iter().filter_map(f64_of).map(|x| x.max(0.0)).collect()).unwrap_or_default(); - doc.get_mut(id)?.stroke.dashes = d; - } - "widthProfile" => doc.get_mut(id)?.stroke.width_profile = width_profile_of(v).ok_or_else(|| bad(cmd, WIDTH_PROFILE_HELP))?, - "brush" | "strokeBrush" => doc.get_mut(id)?.stroke.brush = brush_of(v).ok_or_else(|| bad(cmd, BRUSH_HELP))?, - "cornerRadius" => { - let r = four(v).ok_or_else(|| bad(cmd, "cornerRadius: n or [tl,tr,br,bl]"))?.map(|x| x.max(0.0)); - let n = doc.get_mut(id)?; - n.corners.radii = r; - // A vector's corner radius rounds every point (edit mode then sets them one by one). - if let NodeKind::Vector { network } = &mut n.kind { - network.vertices.iter_mut().for_each(|p| p.radius = r[0]); - crate::vectoredit::refit(doc, id)?; - } - } - "topLeftRadius" | "topRightRadius" | "bottomRightRadius" | "bottomLeftRadius" => { - let i = ["topLeftRadius", "topRightRadius", "bottomRightRadius", "bottomLeftRadius"].iter().position(|x| *x == k).unwrap_or(0); - let r = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); - if let Some(slot) = doc.get_mut(id)?.corners.radii.get_mut(i) { - *slot = r; - } - } - "cornerSmoothing" => doc.get_mut(id)?.corners.smoothing = f64_of(v).unwrap_or(0.0).clamp(0.0, 1.0), - "effects" => doc.get_mut(id)?.effects = effects_from(cmd, v)?, - "clip" | "clipsContent" => doc.get_mut(id)?.clip = v.as_bool().ok_or_else(|| bad(cmd, "`clip` must be a boolean"))?, - "isMask" => doc.get_mut(id)?.is_mask = v.as_bool().unwrap_or(false), - "devStatus" => { - let st = if v.is_null() { - Some(DevStatus::None) - } else { - v.as_str().and_then(|s| DevStatus::ALL.into_iter().find(|d| d.id().eq_ignore_ascii_case(s))) - }; - let st = st.ok_or_else(|| bad(cmd, "devStatus: none|readyForDev|completed"))?; - if !st.is_none() && !crate::devinspect::has_status(doc, id) { - return Err(bad(cmd, "devStatus: only sections and frames at the top of a page or section take one")); - } - let n = doc.get_mut(id)?; - n.dev_status = st; - // Marking a layer (again) starts from its current contents. - n.dev_changed = false; - } - "maskType" => { - let t = v.as_str().and_then(|s| MaskType::ALL.into_iter().find(|m| m.id().eq_ignore_ascii_case(s))); - doc.get_mut(id)?.mask_type = t.ok_or_else(|| bad(cmd, "maskType: alpha|vector|luminance"))?; - } - "layout" | "layoutMode" => { - let n = doc.get_mut(id)?; - if !n.kind.is_frame_like() { - return Err(bad(cmd, "auto layout needs a frame")); - } - match v { - Value::Null => { - n.layout = None; - drop_layout_bindings(n); - } - Value::String(s) if s.eq_ignore_ascii_case("none") => { - n.layout = None; - drop_layout_bindings(n); - } - Value::String(s) => { - let mode = match s.to_ascii_lowercase().as_str() { - "horizontal" | "row" => LayoutMode::Horizontal, - "vertical" | "column" => LayoutMode::Vertical, - "grid" => LayoutMode::Grid, - _ => return Err(bad(cmd, "layout: none|horizontal|vertical|grid")), - }; - ensure_layout(n).mode = mode; - } - Value::Object(_) => n.layout = Some(serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("layout: {e}")))?), - _ => return Err(bad(cmd, "layout: none|horizontal|vertical|grid|{...}")), - } - } - "gap" | "itemSpacing" | "columnGap" => { - layout_mut(cmd, k, doc.get_mut(id)?)?.gap = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))? - } - "crossGap" | "counterAxisSpacing" | "rowGap" => { - layout_mut(cmd, k, doc.get_mut(id)?)?.cross_gap = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))? - } - "padding" => { - let p = four(v).ok_or_else(|| bad(cmd, "padding: n or [t,r,b,l]"))?; - layout_mut(cmd, k, doc.get_mut(id)?)?.padding = p.map(|x| x.max(0.0)); - } - "paddingTop" | "paddingRight" | "paddingBottom" | "paddingLeft" => { - let i = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"].iter().position(|x| *x == k).unwrap_or(0); - let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.max(0.0); - if let Some(slot) = layout_mut(cmd, k, doc.get_mut(id)?)?.padding.get_mut(i) { - *slot = val; - } - } - "paddingHorizontal" => { - let val = f64_of(v).unwrap_or(0.0).max(0.0); - let l = layout_mut(cmd, k, doc.get_mut(id)?)?; - l.padding[1] = val; - l.padding[3] = val; - } - "paddingVertical" => { - let val = f64_of(v).unwrap_or(0.0).max(0.0); - let l = layout_mut(cmd, k, doc.get_mut(id)?)?; - l.padding[0] = val; - l.padding[2] = val; - } - "mainAlign" | "primaryAxisAlignItems" => { - layout_mut(cmd, k, doc.get_mut(id)?)?.main_align = - enum_of(v).ok_or_else(|| bad(cmd, "mainAlign: min|center|max|spaceBetween|spaceAround|spaceEvenly"))? - } - "crossAlign" | "counterAxisAlignItems" => { - layout_mut(cmd, k, doc.get_mut(id)?)?.cross_align = enum_of(v).ok_or_else(|| bad(cmd, "crossAlign: min|center|max|baseline"))? - } - "contentAlign" | "counterAxisAlignContent" => { - layout_mut(cmd, k, doc.get_mut(id)?)?.content_align = enum_of(v).ok_or_else(|| bad(cmd, "contentAlign: auto|spaceBetween"))? - } - "wrap" => layout_mut(cmd, k, doc.get_mut(id)?)?.wrap = v.as_bool().unwrap_or(false), - "gridColumns" | "gridRows" => { - let count = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.clamp(1.0, 1000.0) as u32; - let l = layout_mut(cmd, k, doc.get_mut(id)?)?; - // Tracks past the new count go away (new tracks are 1fr). - let (n, tracks) = - if k == "gridColumns" { (&mut l.grid_columns, &mut l.column_tracks) } else { (&mut l.grid_rows, &mut l.row_tracks) }; - *n = count; - tracks.truncate(count as usize); - } - "strokesIncluded" => layout_mut(cmd, k, doc.get_mut(id)?)?.strokes_included = v.as_bool().unwrap_or(false), - "reverseZ" => layout_mut(cmd, k, doc.get_mut(id)?)?.reverse_z = v.as_bool().unwrap_or(false), - "sizingH" | "layoutSizingHorizontal" => { - let s = sizing(v).ok_or_else(|| bad(cmd, "sizing: fixed|hug|fill"))?; - set_sizing(cmd, doc, id, s, true)?; - } - "sizingV" | "layoutSizingVertical" => { - let s = sizing(v).ok_or_else(|| bad(cmd, "sizing: fixed|hug|fill"))?; - set_sizing(cmd, doc, id, s, false)?; - } - "absolute" | "ignoreAutoLayout" => { - let on = bool_of(v).ok_or_else(|| bad(cmd, "`absolute` must be a boolean"))?; - let cl = &mut doc.get_mut(id)?.child_layout; - cl.absolute = on; - // An absolute child can't fill: it keeps its current size. - if on { - for s in [&mut cl.sizing_h, &mut cl.sizing_v] { - if *s == Sizing::Fill { - *s = Sizing::Fixed; - } - } - } - } - "minWidth" | "maxWidth" | "minHeight" | "maxHeight" => { - let val = match v { - Value::Null => None, - other => Some(f64_of(other).ok_or_else(|| bad(cmd, format!("`{k}` must be a number or null")))?.clamp(0.0, 1e7)), - }; - set_limit(&mut doc.get_mut(id)?.child_layout, k, val); - } - "columnTracks" | "gridColumnTracks" | "rowTracks" | "gridRowTracks" => { - let a = v.as_array().ok_or_else(|| bad(cmd, format!("`{k}` must be an array of tracks")))?; - let tracks = a.iter().map(track_of).collect::>>().ok_or_else(|| bad(cmd, TRACK_HELP))?; - let l = layout_mut(cmd, k, doc.get_mut(id)?)?; - if k.contains("olumn") { - l.grid_columns = (tracks.len() as u32).clamp(1, 1000); - l.column_tracks = tracks; - } else { - l.grid_rows = (tracks.len() as u32).clamp(1, 1000); - l.row_tracks = tracks; - } - } - "gridColumn" | "gridColumnAnchorIndex" | "gridRow" | "gridRowAnchorIndex" => { - let val = match v { - Value::Null => None, - Value::String(s) if s.eq_ignore_ascii_case("auto") => None, - other => { - Some(f64_of(other).ok_or_else(|| bad(cmd, format!("`{k}` must be a 0-based index, \"auto\" or null")))?.clamp(0.0, 999.0) - as u32) - } - }; - let cl = &mut doc.get_mut(id)?.child_layout; - if k.starts_with("gridColumn") { - cl.grid_column = val; - } else { - cl.grid_row = val; - } - } - "gridAlignH" | "gridChildHorizontalAlign" | "gridAlignV" | "gridChildVerticalAlign" => { - let val = match v { - Value::Null => None, - Value::String(s) if s.eq_ignore_ascii_case("auto") => None, - other => Some(enum_of(other).ok_or_else(|| bad(cmd, format!("`{k}`: min|center|max|auto")))?), - }; - let cl = &mut doc.get_mut(id)?.child_layout; - if k.contains('H') { - cl.grid_align_h = val; - } else { - cl.grid_align_v = val; - } - } - "gridColumnSpan" | "gridRowSpan" => { - let span = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?.clamp(1.0, 1000.0) as u32; - let cl = &mut doc.get_mut(id)?.child_layout; - if k == "gridColumnSpan" { - cl.grid_column_span = span; - } else { - cl.grid_row_span = span; - } - } - "constraints" => { - let n = doc.get_mut(id)?; - if let Some(h) = v.get("horizontal") { - n.constraints.horizontal = enum_of(h).ok_or_else(|| bad(cmd, "horizontal: left|right|leftRight|center|scale"))?; - } - if let Some(vv) = v.get("vertical") { - n.constraints.vertical = enum_of(vv).ok_or_else(|| bad(cmd, "vertical: top|bottom|topBottom|center|scale"))?; - } - } - "grids" | "layoutGrids" => doc.get_mut(id)?.grids = serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("grids: {e}")))?, - "exports" | "exportSettings" => { - doc.get_mut(id)?.exports = serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("exports: {e}")))? - } - "interactions" => { - doc.get_mut(id)?.interactions = serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("interactions: {e}")))? - } - "overflow" => doc.get_mut(id)?.overflow = enum_of(v).ok_or_else(|| bad(cmd, "overflow: none|horizontal|vertical|both"))?, - "scrollPosition" => doc.get_mut(id)?.scroll_position = enum_of(v).ok_or_else(|| bad(cmd, "scrollPosition: scrolls|fixed|sticky"))?, - "constrainProportions" => doc.get_mut(id)?.constrain_proportions = v.as_bool().unwrap_or(false), - "characters" | "text" => { - let s = string_val(v).ok_or_else(|| bad(cmd, "`characters` must be a string"))?; - let n = doc.get_mut(id)?; - let t = text_mut(cmd, n)?; - let len = t.char_count(); - t.replace_range(0, len, &normalize_line_endings(&s)); - n.overrides.insert("characters".into()); - } - "fontFamily" - | "fontStyle" - | "fontWeight" - | "fontSize" - | "lineHeight" - | "letterSpacing" - | "textDecoration" - | "textCase" - | "hyperlink" - | "textFills" - | "fontVariations" - | "variationSettings" - | "textDecorationStyle" - | "textDecorationThickness" - | "textDecorationOffset" - | "textDecorationSkipInk" - | "textDecorationColor" => { - let n = doc.get_mut(id)?; - let t = text_mut(cmd, n)?; - let (start, end) = range_of(props, t.char_count()); - apply_text_style(cmd, t, start, end, k, v)?; - } - "textAlign" | "textAlignHorizontal" => { - text_mut(cmd, doc.get_mut(id)?)?.align_h = enum_of(v).ok_or_else(|| bad(cmd, "textAlign: left|center|right|justified"))? - } - "textAlignVertical" => { - text_mut(cmd, doc.get_mut(id)?)?.align_v = enum_of(v).ok_or_else(|| bad(cmd, "textAlignVertical: top|middle|bottom"))? - } - "textAutoResize" => { - text_mut(cmd, doc.get_mut(id)?)?.auto_resize = - enum_of(v).ok_or_else(|| bad(cmd, "textAutoResize: widthAndHeight|height|none|truncate"))? - } - "paragraphSpacing" => text_mut(cmd, doc.get_mut(id)?)?.paragraph_spacing = f64_of(v).unwrap_or(0.0).max(0.0), - "paragraphIndent" => text_mut(cmd, doc.get_mut(id)?)?.paragraph_indent = f64_of(v).unwrap_or(0.0).max(0.0), - "listStyle" | "indentation" => { - let t = text_mut(cmd, doc.get_mut(id)?)?; - let (start, end) = range_of(props, t.char_count()); - if k == "listStyle" { - let l: ListStyle = enum_of(v).ok_or_else(|| bad(cmd, "listStyle: none|bulleted|numbered"))?; - t.set_para_lists(start, end, |p| p.list = l); - } else { - let d = f64_of(v).filter(|d| d.is_finite()).ok_or_else(|| bad(cmd, "indentation: a list level 0-8"))?; - t.set_para_lists(start, end, |p| p.indent = d.clamp(0.0, f64::from(MAX_LIST_INDENT)) as u8); - } - } - "listSpacing" => text_mut(cmd, doc.get_mut(id)?)?.list_spacing = f64_of(v).filter(|s| s.is_finite()).unwrap_or(0.0).clamp(0.0, 10_000.0), - "hangingPunctuation" | "hangingList" => { - let on = v.as_bool().ok_or_else(|| bad(cmd, format!("`{k}` must be true or false")))?; - let t = text_mut(cmd, doc.get_mut(id)?)?; - *if k == "hangingList" { &mut t.hanging_list } else { &mut t.hanging_punctuation } = on; - } - "leadingTrim" => text_mut(cmd, doc.get_mut(id)?)?.leading_trim = enum_of(v).ok_or_else(|| bad(cmd, "leadingTrim: none|capHeight"))?, - "maxLines" => text_mut(cmd, doc.get_mut(id)?)?.max_lines = f64_of(v).unwrap_or(0.0).clamp(0.0, 10_000.0) as u32, - "features" | "textFeatures" | "fontFeatures" | "openTypeFeatures" => { - let f = features_of(cmd, v)?; - let t = text_mut(cmd, doc.get_mut(id)?)?; - t.features.clear(); - for (k, on) in f { - if let Some(on) = on { - t.features.insert(k, on); - } - } - } - "start" | "end" => {} - "pointCount" => { - let c = f64_of(v).ok_or_else(|| bad(cmd, "`pointCount` must be a number"))?.round(); - match &mut doc.get_mut(id)?.kind { - // Stars have 3 to 60 points; polygons keep the geometry's 120 (silent). - NodeKind::Star { count, .. } => *count = c.clamp(3.0, 60.0) as u32, - NodeKind::Polygon { count } => *count = c.clamp(3.0, 120.0) as u32, - _ => return Err(bad(cmd, "pointCount applies to polygons and stars")), - } - } - "innerRadius" | "ratio" => { - let r = f64_of(v).unwrap_or(0.38); - let r = if r > 1.0 { r / 100.0 } else { r }; - match &mut doc.get_mut(id)?.kind { - NodeKind::Star { inner_ratio, .. } => *inner_ratio = r.clamp(0.0, 1.0), - _ => return Err(bad(cmd, "innerRadius applies to stars")), - } - } - "arcStart" | "arcSweep" | "arcInner" => { - let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?; - match &mut doc.get_mut(id)?.kind { - NodeKind::Ellipse { start, sweep, inner } => match k { - "arcStart" => *start = val.to_radians(), - "arcSweep" => *sweep = val.clamp(-360.0, 360.0).to_radians(), - _ => *inner = (if val > 1.0 { val / 100.0 } else { val }).clamp(0.0, 0.99), - }, - _ => return Err(bad(cmd, "arc applies to ellipses")), - } - } - "description" => match &mut doc.get_mut(id)?.kind { - NodeKind::Component { description, .. } | NodeKind::ComponentSet { description } => *description = string_val(v).unwrap_or_default(), - _ => return Err(bad(cmd, "description applies to components")), - }, - "pluginData" => { - let n = doc.get_mut(id)?; - if let Some(o) = v.as_object() { - for (pk, pv) in o { - if pv.is_null() { - n.plugin_data.remove(pk); - } else { - n.plugin_data.insert(pk.clone(), string_val(pv).unwrap_or_else(|| pv.to_string())); - } - } - } - } - "fillStyle" | "strokeStyle" | "effectStyle" | "textStyle" | "gridStyle" => { - let sid = string_val(v); - crate::cmd::styles::apply_style_ref(doc, id, k, sid.as_deref())?; - } - "boundVariables" => { - let n = doc.get_mut(id)?; - if let Some(o) = v.as_object() { - for (field, var) in o { - match var.as_str() { - Some(vid) => { - if n.bound.get(field).map(String::as_str) != Some(vid) { - crate::cmd::styles::rebind_paint_alpha(&mut n.fills, &mut n.strokes, field, true); - } - n.bound.insert(field.clone(), vid.to_string()); - } - None => { - if n.bound.remove(field).is_some() { - crate::cmd::styles::rebind_paint_alpha(&mut n.fills, &mut n.strokes, field, false); - } - } - } - } - } - } - "variableModes" => { - let n = doc.get_mut(id)?; - if let Some(o) = v.as_object() { - for (coll, mode) in o { - match mode.as_str() { - Some(m) => { - n.var_modes.insert(coll.clone(), m.to_string()); - } - None => { - n.var_modes.remove(coll); - } - } - } - } - } - "booleanOperation" if !matches!(doc.get(id)?.kind, NodeKind::BooleanOp { .. }) => set_operand_op(doc, id, v, cmd)?, - "booleanOperation" => { - if let NodeKind::BooleanOp { op } = &mut doc.get_mut(id)?.kind { - *op = enum_of(v).ok_or_else(|| bad(cmd, BOOL_OP_HELP))?; - } - } - "operandOperation" => set_operand_op(doc, id, v, cmd)?, - "sectionContentsHidden" => { - if let NodeKind::Section { contents_hidden } = &mut doc.get_mut(id)?.kind { - *contents_hidden = v.as_bool().unwrap_or(false); - } - } - "background" => { - let c = color_of(v).ok_or_else(|| bad(cmd, "`background` must be a colour"))?; - match &mut doc.get_mut(id)?.kind { - NodeKind::Page { background, .. } => *background = c, - _ => return Err(bad(cmd, "background applies to pages")), - } - } - "componentProperties" => { - if let NodeKind::Instance { props: p, .. } = &mut doc.get_mut(id)?.kind - && let Some(o) = v.as_object() - { - for (pk, pv) in o { - p.insert(pk.clone(), pv.clone()); - } - } - } - other => unknown.push(other.to_string()), + if apply_geometry(cmd, doc, id, props, k, v, &mut path_offset)? + || apply_look(cmd, doc, id, k, v)? + || apply_corners(cmd, doc, id, k, v)? + || apply_layout(cmd, doc, id, k, v)? + || apply_sizing(cmd, doc, id, k, v)? + || apply_meta(cmd, doc, id, k, v)? + || apply_text(cmd, doc, id, props, k, v)? + || apply_shape(cmd, doc, id, k, v)? + { + continue; } + unknown.push(k.to_string()); } if !unknown.is_empty() { return Err(bad(cmd, format!("unknown properties: {} (known: {KEYS})", unknown.join(", ")))); diff --git a/crates/engine/src/cmd/proto.rs b/crates/engine/src/cmd/proto.rs index ce7219f44..c451eeb25 100644 --- a/crates/engine/src/cmd/proto.rs +++ b/crates/engine/src/cmd/proto.rs @@ -1,11 +1,14 @@ //! Prototype commands: interactions, flows, settings, presenting. -use figcraft_doc::proto::*; -use figcraft_doc::*; +use figcraft_doc::proto::{ + Action, DeviceRotation, Easing, Expr, Flow, Interaction, MAX_DELAY_MS, MAX_EXPR_DEPTH, OverlayPosition, PrototypeSettings, Transition, Trigger, + walk_mut, +}; +use figcraft_doc::{Document, NodeId, NodeKind, VariableValue, Variables}; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, color, f64_of, get, id, num, parse, req_id, req_num, req_targets, string}; use crate::{Result, Session, bad, spec}; pub fn specs() -> Vec { @@ -498,6 +501,7 @@ fn set(s: &mut Session, p: &Value) -> Result { mod tests { use super::*; use crate::params::id_of; + use figcraft_doc::proto::{Direction, Overflow, ScrollPosition}; fn frame(s: &mut Session, x: f64) -> NodeId { let r = s.execute("node.create", &json!({"type": "frame", "x": x, "y": 0, "width": 100, "height": 200})).unwrap(); diff --git a/crates/engine/src/cmd/proto_actions.rs b/crates/engine/src/cmd/proto_actions.rs index d288a7d71..95a53a9d4 100644 --- a/crates/engine/src/cmd/proto_actions.rs +++ b/crates/engine/src/cmd/proto_actions.rs @@ -10,12 +10,12 @@ use std::collections::BTreeMap; -use figcraft_doc::proto::*; -use figcraft_doc::*; +use figcraft_doc::proto::{Action, ConditionalBlock, Expr, ExprOp, MAX_ACTION_DEPTH, walk}; +use figcraft_doc::{Variable, VariableType, VariableValue, Variables}; use serde_json::{Value, json}; use super::CommandSpec; -use crate::params::*; +use crate::params::{bool_of, f64_of, get, id, parse, req_index, req_targets, string}; use crate::{Result, Session, bad, spec}; /// Most actions in one list (an interaction's, or a block's). diff --git a/crates/engine/src/cmd/section.rs b/crates/engine/src/cmd/section.rs index 08a5a7cc8..28b02c531 100644 --- a/crates/engine/src/cmd/section.rs +++ b/crates/engine/src/cmd/section.rs @@ -7,7 +7,7 @@ use serde_json::{Value, json}; use super::CommandSpec; use crate::devinspect::{dev_items, has_status}; -use crate::params::*; +use crate::params::{boolean, get, req_targets, string, targets}; use crate::{Result, Session, bad, spec}; /// Longest change reason kept. diff --git a/crates/engine/src/cmd/structure.rs b/crates/engine/src/cmd/structure.rs index 700825f2d..05823cabf 100644 --- a/crates/engine/src/cmd/structure.rs +++ b/crates/engine/src/cmd/structure.rs @@ -1,13 +1,16 @@ //! Structure: groups, frames, masks, boolean groups, flatten, outline stroke, auto layout, pages. use figcraft_color::{Color, Paint}; -use figcraft_doc::*; +use figcraft_doc::{ + AutoLayout, BoolOp, Corners, Document, LayoutMode, Node, NodeId, NodeKind, Region, Segment, Sizing, StrokeAlign, StrokeJoin, StrokeProps, + VectorNetwork, WindingRule, proto, +}; use figcraft_geom::{Affine, BezPath, Rect, Shape}; use serde_json::{Value, json}; use super::edit::{roots, z_sorted}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{f64_of, get, id_of, num_or, req_id, req_index, req_num, req_targets, string, targets}; use crate::{EngineError, Result, Session, bad, spec}; /// The layer structure commands: group, frame, mask, boolean operations, flatten, auto layout diff --git a/crates/engine/src/cmd/styles.rs b/crates/engine/src/cmd/styles.rs index 9c2922d21..0066602ae 100644 --- a/crates/engine/src/cmd/styles.rs +++ b/crates/engine/src/cmd/styles.rs @@ -3,11 +3,14 @@ use std::collections::BTreeMap; use figcraft_color::{Color, Paint, PaintKind}; -use figcraft_doc::*; +use figcraft_doc::{ + Collection, CollectionId, Document, Effect, EffectKind, Mode, ModeId, Node, NodeId, NodeKind, Style, StyleId, StyleKind, TextData, TextStyle, + Variable, VariableId, VariableType, VariableValue, Variables, trim_num, +}; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{color_of, f64_of, get, id, num, paints_of, req_string, req_targets, string, targets}; use crate::{EngineError, Result, Session, bad, spec}; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 3822c5c5f..e8433fa5e 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -1,11 +1,14 @@ //! Text menu and text editing commands. -use figcraft_doc::*; +use figcraft_doc::{ + LetterSpacing, LineHeight, ListStyle, NodeId, NodeKind, TextAlignH, TextAutoResize, TextCase, TextData, TextDecoration, TextPath, TextStyle, + VariableId, +}; use serde_json::{Map, Value, json}; use super::props::{styled_name, weight_name}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, get, id, num, req_id, req_num, req_string, req_targets, string, targets}; use crate::{Result, Session, bad, spec, textedit}; fn finite(v: f64) -> f64 { diff --git a/crates/engine/src/cmd/variables.rs b/crates/engine/src/cmd/variables.rs index 53b62ee46..155fe5195 100644 --- a/crates/engine/src/cmd/variables.rs +++ b/crates/engine/src/cmd/variables.rs @@ -4,12 +4,15 @@ use std::collections::BTreeSet; -use figcraft_doc::*; +use figcraft_doc::{ + CODE_PLATFORMS, Collection, CollectionId, Document, ModeId, NodeId, NodeKind, Variable, VariableId, VariableType, VariableValue, Variables, + normalize_scopes, scope_known, +}; use serde_json::{Value, json}; use super::lists::BoundList; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, get, num, req_string, string}; use crate::{Result, Session, bad, spec}; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/variables_io.rs b/crates/engine/src/cmd/variables_io.rs index 6bd5bb40e..b9e73f13c 100644 --- a/crates/engine/src/cmd/variables_io.rs +++ b/crates/engine/src/cmd/variables_io.rs @@ -4,13 +4,13 @@ use std::collections::{BTreeMap, BTreeSet}; -use figcraft_doc::*; +use figcraft_doc::{Collection, CollectionId, Document, Mode, ModeId, Variable, VariableId, VariableValue, Variables, trim_num}; use serde_json::{Value, json}; use super::CommandSpec; use crate::codegen::{css_var_name, css_var_of}; use crate::library::{is_remote_collection, local_collections}; -use crate::params::*; +use crate::params::{get, string}; use crate::{EngineError, Result, Session, bad, spec}; pub fn specs() -> Vec { diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index 7364557fc..97b947a59 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -1,10 +1,10 @@ //! View menu, preferences and tools. View state is per document and drivable like everything else. -use figcraft_doc::*; +use figcraft_doc::NodeId; use serde_json::{Value, json}; use super::{CommandSpec, ok}; -use crate::params::*; +use crate::params::{boolean, get, num, num_or, req_index, req_num, req_string, string}; use crate::tools::ToolId; use crate::{EngineError, Result, Session, ViewState, bad, spec};