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
+14
View File
@@ -344,6 +344,7 @@ fn carry_overrides(from: &Node, to: &mut Node, what: Carry) {
if let (NodeKind::Instance { props: a, .. }, NodeKind::Instance { props: b, .. }) = (&from.kind, &mut to.kind) {
*b = a.clone();
}
carry_prop_bindings(from, to);
}
_ => {}
}
@@ -382,6 +383,18 @@ fn path_map(doc: &Document, root: NodeId, src_names: bool) -> BTreeMap<NodeId, S
out
}
/// The binding key prefix of an instance's component property value
/// (`componentProperties/<name>`): a boolean property takes a boolean variable, a text property a
/// string variable.
pub const PROP_BINDING: &str = "componentProperties/";
/// Give `to` the component property bindings of instance `from` (they go with its property
/// values, which an instance keeps across rebuilds).
fn carry_prop_bindings(from: &Node, to: &mut Node) {
to.bound.retain(|k, _| !k.starts_with(PROP_BINDING));
to.bound.extend(from.bound.iter().filter(|(k, _)| k.starts_with(PROP_BINDING)).map(|(k, v)| (k.clone(), v.clone())));
}
/// The override group a property-bound field belongs to.
fn field_group(field: &str) -> &'static str {
match field {
@@ -619,6 +632,7 @@ fn sync_instance_depth(doc: &mut Document, inst: NodeId, depth: usize) {
fresh.children = order;
fresh.transform = root.transform;
fresh.kind = root.kind.clone();
carry_prop_bindings(&root, &mut fresh);
// A nested instance keeps mirroring its layer of the outer main.
fresh.source = if inside_instance(doc, inst) { root.source } else { Some(component) };
fresh.constraints = root.constraints;