M3: reorder paints, effects and layout grids in the Design panel; grid and instance property variable bindings (#81)

Fill, stroke, effect and layout grid rows drag to reorder by their grip (a handle shows over the
row, an insertion line where it lands, the row's label follows the pointer like a dragged layer;
Escape cancels; lists that differ across the selection have nothing to drag). New commands
`fill.move`, `stroke.move` and `grid.move {ids?, from, to}` join `effect.move` (lists bottom first,
one undo step; items keep everything, linked list styles are unlinked).

Variable bindings on list items follow their items when a list is reordered or shrinks:
paint colours bind per paint (`fills/<i>/color`, `strokes/<i>/color`; `fill`/`stroke` stay the
first paint's), so a moved bound paint keeps its variable and its neighbours are never recoloured.
`fill.remove`, `stroke.remove` and `grid.remove` refuse negative indices.

Layout grids get settings in the Design panel (size; count, type, width or height, margin or
offset, gutter; colour) whose numbers bind to number variables (`layoutGrids/<i>/<field>`),
re-applied on variable and mode changes. Instance boolean and text properties bind to boolean and
string variables (`componentProperties/<name>`, from the instance's properties panel), kept across
instance rebuilds, following property renames and dropped on delete, reset and detach.
Inspect names the variable of each paint.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 15:21:05 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent d16878588f
commit 4565c4ab93
22 changed files with 1277 additions and 216 deletions
+12 -8
View File
@@ -6,6 +6,7 @@ use std::collections::BTreeSet;
use figcraft_doc::*;
use serde_json::{Value, json};
use super::lists::BoundList;
use super::{CommandSpec, ok};
use crate::params::*;
use crate::{Result, Session, bad, spec};
@@ -237,14 +238,17 @@ pub fn field_scope(doc: &Document, node: NodeId, field: &str) -> Option<&'static
"characters" => "TEXT_CONTENT",
"fontFamily" => "FONT_FAMILY",
"fontStyle" => "FONT_STYLE",
"stroke" | "strokes/0/color" => "STROKE_COLOR",
f if f.starts_with("effects/") && f.ends_with("/color") => "EFFECT_COLOR",
f if f.starts_with("effects/") => "EFFECT_FLOAT",
"fill" | "fills" | "fills/0" | "fills/0/color" => match doc.node(node).map(|n| &n.kind) {
Some(NodeKind::Text(_)) => "TEXT_FILL",
Some(k) if k.is_frame_like() => "FRAME_FILL",
_ => "SHAPE_FILL",
f => match super::lists::list_binding(f)? {
(BoundList::Strokes, _, _) => "STROKE_COLOR",
(BoundList::Effects, _, "color") => "EFFECT_COLOR",
(BoundList::Effects, _, _) => "EFFECT_FLOAT",
(BoundList::Fills, _, _) => match doc.node(node).map(|n| &n.kind) {
Some(NodeKind::Text(_)) => "TEXT_FILL",
Some(k) if k.is_frame_like() => "FRAME_FILL",
_ => "SHAPE_FILL",
},
// Layout grid fields take any number variable.
(BoundList::Grids, _, _) => return None,
},
_ => return None,
})
}