M1: sections and frames: resize to fit, section colours, ready-for-dev status (#23)

- Object ▸ Resize to fit (Alt+Shift+Cmd+R, `object.resizeToFit`): frames, sections and
  components shrink or grow to their visible contents without moving them on the page; auto
  layout frames switch to hug instead.
- Sections get colour swatches (light tints) in the Design panel.
- Frames and sections get a handoff status (`devStatus`: ready for dev / completed) set from
  the panel and shown as a badge next to their name on the canvas; `node.get` reports it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 04:20:23 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent aa0e8b4a7c
commit bf3a45d938
14 changed files with 260 additions and 5 deletions
+54
View File
@@ -16,6 +16,13 @@ pub fn specs() -> Vec<CommandSpec> {
spec!("object.ungroup", "Ungroup selection", "{ids?}", true, ungroup),
spec!("object.frameSelection", "Frame selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Frame)),
spec!("object.removeFrame", "Remove frame", "{ids?}", true, ungroup),
spec!(
"object.resizeToFit",
"Resize to fit",
"{ids?} (frames, sections and components shrink or grow to their contents; the contents stay put)",
true,
resize_to_fit
),
spec!("object.mask", "Use as mask", "{ids?}", true, mask),
spec!("object.releaseMask", "Release mask", "{ids?}", true, |s, p| {
for i in req_targets("object.releaseMask", s, p)? {
@@ -477,3 +484,50 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
}
flatten(s, &json!({"ids": ids}))
}
/// Fit containers to their visible children's bounds, keeping the children where they are on the
/// page (auto layout frames hug instead); rotated containers fit in their own axes.
fn resize_to_fit(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "object.resizeToFit";
let ids = req_targets(cmd, s, p)?;
let doc = s.doc_mut()?;
let mut fitted = Vec::new();
for id in ids {
let Some(n) = doc.node(id).cloned() else { continue };
let container = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. });
if !container || n.children.is_empty() || matches!(n.kind, NodeKind::Instance { .. }) {
continue;
}
// Auto layout frames fit by hugging their contents.
if n.layout.is_some() {
let node = doc.get_mut(id)?;
node.child_layout.sizing_h = Sizing::Hug;
node.child_layout.sizing_v = Sizing::Hug;
fitted.push(id);
continue;
}
// Children's bounds in the container's own coordinates.
let mut b: Option<figcraft_geom::Rect> = None;
for c in n.children.iter().filter_map(|c| doc.node(*c)).filter(|c| c.visible) {
let r = figcraft_geom::transform_rect_bbox(c.transform, c.local_rect());
if r.x0.is_finite() && r.y0.is_finite() && r.x1.is_finite() && r.y1.is_finite() {
b = Some(b.map_or(r, |u| u.union(r)));
}
}
let Some(b) = b else { continue };
let (w, h) = (b.width().max(0.01), b.height().max(0.01));
// Move the container by the offset, and its children back by it.
let shift = Affine::translate((b.x0, b.y0));
let node = doc.get_mut(id)?;
node.transform *= shift;
node.width = w;
node.height = h;
for c in &n.children {
if let Ok(cn) = doc.get_mut(*c) {
cn.transform = shift.inverse() * cn.transform;
}
}
fitted.push(id);
}
Ok(json!({"ids": fitted}))
}